documents: table edits keep extra cells and quote marks and say why they refuse; preview takes no edits; lists go on only in list items; a match waits for its text
CI / platforms (windows-latest) (push) Failing after 8s
CI / site (push) Successful in 22s
CI / crate (push) Failing after 11m29s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-12 06:40:36 -04:00
1 parent ce4a1e9cb5
commit edc328aadf
7 files changed
+273 -59

No files matched your search

+35 -4
View File
@@ -22,11 +22,13 @@ impl MarkdownDocument {
} }
MarkdownMode::Preview => self.reveal(line(self, cx), window, cx), MarkdownMode::Preview => self.reveal(line(self, cx), window, cx),
MarkdownMode::Visual => { MarkdownMode::Visual => {
if self.commit(cx).is_some() { if self.commit(cx).is_some() || !self.read_now(cx) {
self.reparse(true, cx); self.reparse(true, cx);
} }
if !self.read_now(cx) { if !self.read_now(cx) {
return log::warn!("markdown document: a match shown while the text is read"); // A long text: shown once read.
self.showing = Some(range);
return cx.notify();
} }
let ix = self.block_at(range.start); let ix = self.block_at(range.start);
let held = self.parsed.blocks.get(ix).map(|block| block.range.clone()); let held = self.parsed.blocks.get(ix).map(|block| block.range.clone());
@@ -48,10 +50,39 @@ impl MarkdownDocument {
} }
} }
/// `edits` to the text as one undo step. /// A match asked before its text was read, now shown.
pub fn replace(&mut self, edits: Vec<(Range<usize>, String)>, cx: &mut Context<Self>) { pub(super) fn show_waiting(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.showing.is_none() || !self.read_now(cx) {
return;
}
let range = self.showing.take().expect("checked above");
let text = self.editor.read(cx).text();
let fits = range.end <= text.len()
&& text.is_char_boundary(range.start)
&& text.is_char_boundary(range.end);
if !fits {
return log::warn!("markdown document: a waiting match outlived its text");
}
self.show_match(range, window, cx);
}
/// `edits` as one undo step; false in preview.
pub fn replace(&mut self, edits: Vec<(Range<usize>, String)>, cx: &mut Context<Self>) -> bool {
if self.mode == MarkdownMode::Preview {
return false;
}
self.commit(cx); self.commit(cx);
let text = self.editor.read(cx).text();
if edits
.iter()
.all(|(range, with)| text[range.clone()] == *with)
{
return true;
}
log::info!("markdown document: {} replaced", edits.len()); log::info!("markdown document: {} replaced", edits.len());
// Its own change: an open block outlives it.
self.writes += 1;
self.editor.update(cx, |editor, cx| editor.edit(edits, cx)); self.editor.update(cx, |editor, cx| editor.edit(edits, cx));
true
} }
} }
+38 -1
View File
@@ -3,8 +3,10 @@
use std::ops::Range; use std::ops::Range;
use gpui::{Context, Window}; use gpui::{Context, Window};
use markdown::mdast;
use super::MarkdownDocument; use super::MarkdownDocument;
use crate::documents::markdown::{span, tree, walk};
/// Spaces one indent step adds. /// Spaces one indent step adds.
const STEP: usize = 4; const STEP: usize = 4;
@@ -39,6 +41,23 @@ pub(super) fn next(line: &str) -> Option<Next> {
Some(Next::Item(format!("{}{mark}{task}", " ".repeat(indent)))) Some(Next::Item(format!("{}{mark}{task}", " ".repeat(indent))))
} }
/// Whether `at` sits in a list item, not code.
fn listed(source: &str, at: usize) -> bool {
let root = tree(source);
let mut nodes = Vec::new();
walk(&root, &mut nodes);
let held: Vec<&mdast::Node> = nodes
.into_iter()
.filter(|node| span(node).start <= at && at <= span(node).end)
.collect();
let item = held
.iter()
.any(|node| matches!(node, mdast::Node::ListItem(_)));
item && !held
.iter()
.any(|node| matches!(node, mdast::Node::Code(_) | mdast::Node::Math(_)))
}
/// A bullet marker, and where its text starts. /// A bullet marker, and where its text starts.
fn bullet(rest: &str) -> Option<(String, usize)> { fn bullet(rest: &str) -> Option<(String, usize)> {
let mark = rest.chars().next().filter(|ch| "-*+".contains(*ch))?; let mark = rest.chars().next().filter(|ch| "-*+".contains(*ch))?;
@@ -117,7 +136,25 @@ impl MarkdownDocument {
if editor.selections().len() != 1 || !caret.is_empty() { if editor.selections().len() != 1 || !caret.is_empty() {
return false; return false;
} }
if !self.read_now(cx) {
self.reparse(true, cx);
}
if !self.read_now(cx) {
log::info!("markdown document: Enter while a long text is read");
return false;
}
let editor = self.editor.read(cx);
let text = editor.text(); let text = editor.text();
let ix = self.block_at(caret.start);
let block = self.parsed.blocks.get(ix).map(|block| block.range.clone());
let Some(block) =
block.filter(|block| block.start <= caret.start && caret.start <= block.end)
else {
return false;
};
if !listed(&text[block.clone()], caret.start - block.start) {
return false;
}
let start = text[..caret.start].rfind('\n').map_or(0, |at| at + 1); let start = text[..caret.start].rfind('\n').map_or(0, |at| at + 1);
let end = text[caret.start..] let end = text[caret.start..]
.find('\n') .find('\n')
@@ -149,7 +186,7 @@ impl MarkdownDocument {
let input = input.read(cx); let input = input.read(cx);
(input.text().to_string(), input.selection()) (input.text().to_string(), input.selection())
}; };
if !caret.is_empty() { if !caret.is_empty() || !listed(&text, caret.start) {
return false; return false;
} }
let start = text[..caret.start].rfind('\n').map_or(0, |at| at + 1); let start = text[..caret.start].rfind('\n').map_or(0, |at| at + 1);
+4 -1
View File
@@ -19,7 +19,7 @@ use gpui::{
pub use parse::DocumentHeading; pub use parse::DocumentHeading;
use parse::{Parsed, alike, parsed}; use parse::{Parsed, alike, parsed};
pub use tables::TableEdit; pub use tables::{TableEdit, TableRefusal};
use super::{MarkdownMode, markdown::markdown_highlights, modes::joint, render::OnLink}; use super::{MarkdownMode, markdown::markdown_highlights, modes::joint, render::OnLink};
use crate::{ use crate::{
@@ -79,6 +79,8 @@ pub struct MarkdownDocument {
asks: u64, asks: u64,
/// Its own write-backs whose changes are still to come. /// Its own write-backs whose changes are still to come.
writes: usize, writes: usize,
/// A match asked for before its text was read.
showing: Option<Range<usize>>,
focus: FocusHandle, focus: FocusHandle,
on_link: Option<OnLink>, on_link: Option<OnLink>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
@@ -131,6 +133,7 @@ impl MarkdownDocument {
top: None, top: None,
asks: 0, asks: 0,
writes: 0, writes: 0,
showing: None,
focus, focus,
on_link: None, on_link: None,
_subscriptions: subscriptions, _subscriptions: subscriptions,
+136 -50
View File
@@ -6,7 +6,10 @@ use gpui::Context;
use markdown::mdast::{self, AlignKind}; use markdown::mdast::{self, AlignKind};
use super::MarkdownDocument; use super::MarkdownDocument;
use crate::documents::markdown::{span, tree, walk}; use crate::documents::{
MarkdownMode,
markdown::{span, tree, walk},
};
/// What a table edit does at the caret. /// What a table edit does at the caret.
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -22,21 +25,47 @@ pub enum TableEdit {
/// A new table: three columns, a head and two rows. /// A new table: three columns, a head and two rows.
const NEW: &str = "| | | |\n| --- | --- | --- |\n| | | |\n| | | |"; const NEW: &str = "| | | |\n| --- | --- | --- |\n| | | |\n| | | |";
/// Why a table edit did not happen.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TableRefusal {
/// No table at the caret.
Outside,
/// The head row and the last column stay.
Kept,
/// The caret is in a cell past the head's columns.
Beyond,
/// Its lines carry different quote or list marks.
Mixed,
/// Preview takes no edits.
Preview,
}
/// The table around `at` after `edit`: its range, text, caret. /// The table around `at` after `edit`: its range, text, caret.
pub(super) fn edit_table( pub(super) fn edit_table(
text: &str, text: &str,
at: usize, at: usize,
edit: TableEdit, edit: TableEdit,
) -> Option<(Range<usize>, String, usize)> { ) -> Result<(Range<usize>, String, usize), TableRefusal> {
let root = tree(text); let root = tree(text);
let mut nodes = Vec::new(); let mut nodes = Vec::new();
walk(&root, &mut nodes); walk(&root, &mut nodes);
let (range, table) = nodes.into_iter().find_map(|node| match node { let (range, table) = nodes
mdast::Node::Table(table) if (span(node).start..=span(node).end).contains(&at) => { .into_iter()
Some((span(node), table)) .find_map(|node| match node {
} mdast::Node::Table(table) if (span(node).start..=span(node).end).contains(&at) => {
_ => None, Some((span(node), table))
})?; }
_ => None,
})
.ok_or(TableRefusal::Outside)?;
let prefix = &text[text[..range.start].rfind('\n').map_or(0, |found| found + 1)..range.start];
if text[range.clone()]
.split('\n')
.skip(1)
.any(|line| !line.starts_with(prefix))
{
return Err(TableRefusal::Mixed);
}
let rows: Vec<&mdast::Node> = table.children.iter().collect(); let rows: Vec<&mdast::Node> = table.children.iter().collect();
let row = rows let row = rows
.iter() .iter()
@@ -48,6 +77,7 @@ pub(super) fn edit_table(
.rposition(|cell| span(cell).start <= at) .rposition(|cell| span(cell).start <= at)
.unwrap_or(0); .unwrap_or(0);
let width = table.align.len(); let width = table.align.len();
// Cells past the head stay as written.
let mut grid: Vec<Vec<String>> = rows let mut grid: Vec<Vec<String>> = rows
.iter() .iter()
.map(|row| { .map(|row| {
@@ -55,21 +85,30 @@ pub(super) fn edit_table(
.iter() .iter()
.map(|cell| written(text, cell)) .map(|cell| written(text, cell))
.collect(); .collect();
cells.resize(width, String::new()); if cells.len() < width {
cells.resize(width, String::new());
}
cells cells
}) })
.collect(); .collect();
let mut aligns = table.align.clone(); let mut aligns = table.align.clone();
let column_edit = matches!(
edit,
TableEdit::ColumnLeft | TableEdit::ColumnRight | TableEdit::DeleteColumn
);
if column_edit && column >= width {
return Err(TableRefusal::Beyond);
}
let (row, column) = match edit { let (row, column) = match edit {
TableEdit::RowAbove | TableEdit::RowBelow => { TableEdit::RowAbove | TableEdit::RowBelow => {
let below = edit == TableEdit::RowBelow || row == 0; let below = edit == TableEdit::RowBelow || row == 0;
let at = if below { row + 1 } else { row }; let at = if below { row + 1 } else { row };
grid.insert(at, vec![String::new(); width]); grid.insert(at, vec![String::new(); width]);
(at, column) (at, column.min(width - 1))
} }
TableEdit::DeleteRow if row > 0 => { TableEdit::DeleteRow if row > 0 => {
grid.remove(row); grid.remove(row);
(row.min(grid.len() - 1), column) (row.min(grid.len() - 1), column.min(width - 1))
} }
TableEdit::ColumnLeft | TableEdit::ColumnRight => { TableEdit::ColumnLeft | TableEdit::ColumnRight => {
let at = column + usize::from(edit == TableEdit::ColumnRight); let at = column + usize::from(edit == TableEdit::ColumnRight);
@@ -85,10 +124,10 @@ pub(super) fn edit_table(
aligns.remove(column); aligns.remove(column);
(row, column.min(width - 2)) (row, column.min(width - 2))
} }
_ => return None, _ => return Err(TableRefusal::Kept),
}; };
let (out, caret) = table_text(&grid, &aligns, (row, column)); let (out, caret) = table_text(&grid, &aligns, (row, column), prefix);
Some((range, out, caret)) Ok((range, out, caret))
} }
/// A cell's source between its pipes. /// A cell's source between its pipes.
@@ -100,8 +139,13 @@ fn written(text: &str, cell: &mdast::Node) -> String {
} }
} }
/// The table's text, and where cell `at`'s text starts. /// The table's text, each line after the first under `prefix`.
fn table_text(grid: &[Vec<String>], aligns: &[AlignKind], at: (usize, usize)) -> (String, usize) { fn table_text(
grid: &[Vec<String>],
aligns: &[AlignKind],
at: (usize, usize),
prefix: &str,
) -> (String, usize) {
let mut out = String::new(); let mut out = String::new();
let mut caret = 0; let mut caret = 0;
for (row, cells) in grid.iter().enumerate() { for (row, cells) in grid.iter().enumerate() {
@@ -116,6 +160,7 @@ fn table_text(grid: &[Vec<String>], aligns: &[AlignKind], at: (usize, usize)) ->
out.push('|'); out.push('|');
if row == 0 { if row == 0 {
out.push('\n'); out.push('\n');
out.push_str(prefix);
for align in aligns { for align in aligns {
out.push_str(match align { out.push_str(match align {
AlignKind::Left => "| :-- ", AlignKind::Left => "| :-- ",
@@ -128,32 +173,42 @@ fn table_text(grid: &[Vec<String>], aligns: &[AlignKind], at: (usize, usize)) ->
} }
if row + 1 < grid.len() { if row + 1 < grid.len() {
out.push('\n'); out.push('\n');
out.push_str(prefix);
} }
} }
(out, caret) (out, caret)
} }
impl MarkdownDocument { impl MarkdownDocument {
/// `edit` on the table at the caret; false outside one. /// `edit` on the table at the caret, or why not.
pub fn edit_table(&mut self, edit: TableEdit, cx: &mut Context<Self>) -> bool { pub fn edit_table(
&mut self,
edit: TableEdit,
cx: &mut Context<Self>,
) -> Result<(), TableRefusal> {
if self.mode == MarkdownMode::Preview {
return Err(TableRefusal::Preview);
}
self.commit(cx); self.commit(cx);
let at = self.editor.read(cx).primary().range().start; let at = self.editor.read(cx).primary().range().start;
let text = self.editor.read(cx).text().to_string(); let text = self.editor.read(cx).text().to_string();
let Some((range, table, caret)) = edit_table(&text, at, edit) else { let (range, table, caret) = edit_table(&text, at, edit).inspect_err(|why| {
log::info!("markdown document: no table edit {edit:?} at the caret"); log::info!("markdown document: table edit {edit:?} refused: {why:?}");
return false; })?;
};
log::info!("markdown document: table edit {edit:?}"); log::info!("markdown document: table edit {edit:?}");
let caret = range.start + caret; let caret = range.start + caret;
self.editor.update(cx, |editor, cx| { self.editor.update(cx, |editor, cx| {
editor.edit([(range, table)], cx); editor.edit([(range, table)], cx);
editor.select(std::iter::once(caret..caret), cx); editor.select(std::iter::once(caret..caret), cx);
}); });
true Ok(())
} }
/// A new table after the caret's line. /// A new table after the caret's line, unless in preview.
pub fn insert_table(&mut self, cx: &mut Context<Self>) { pub fn insert_table(&mut self, cx: &mut Context<Self>) -> Result<(), TableRefusal> {
if self.mode == MarkdownMode::Preview {
return Err(TableRefusal::Preview);
}
self.commit(cx); self.commit(cx);
let text = self.editor.read(cx).text().to_string(); let text = self.editor.read(cx).text().to_string();
let at = self.editor.read(cx).primary().range().start; let at = self.editor.read(cx).primary().range().start;
@@ -176,62 +231,93 @@ impl MarkdownDocument {
editor.edit([(end..end, written)], cx); editor.edit([(end..end, written)], cx);
editor.select(std::iter::once(caret..caret), cx); editor.select(std::iter::once(caret..caret), cx);
}); });
Ok(())
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{TableEdit, edit_table}; use super::{TableEdit, TableRefusal, edit_table};
const TABLE: &str = "Intro\n\n| A | B |\n|:--|--:|\n| 1 | 2 |\n| 3 | 4 |\n\nAfter\n"; const TABLE: &str = "Intro\n\n| A | B |\n|:--|--:|\n| 1 | 2 |\n| 3 | 4 |\n\nAfter\n";
fn edited(at: &str, edit: TableEdit) -> Option<String> { fn edited_in(text: &str, at: &str, edit: TableEdit) -> Result<String, TableRefusal> {
let at = TABLE.find(at).expect("in the table"); let at = text.find(at).expect("in the table");
let (range, table, _) = edit_table(TABLE, at, edit)?; let (range, table, _) = edit_table(text, at, edit)?;
Some(format!( Ok(format!(
"{}{table}{}", "{}{table}{}",
&TABLE[..range.start], &text[..range.start],
&TABLE[range.end..] &text[range.end..]
)) ))
} }
fn edited(at: &str, edit: TableEdit) -> Result<String, TableRefusal> {
edited_in(TABLE, at, edit)
}
#[test] #[test]
fn rows_and_columns_come_and_go() { fn rows_and_columns_come_and_go() {
let around = |table: &str| format!("Intro\n\n{table}\n\nAfter\n"); let around = |table: &str| Ok(format!("Intro\n\n{table}\n\nAfter\n"));
assert_eq!( assert_eq!(
edited("3", TableEdit::RowAbove).as_deref(), edited("3", TableEdit::RowAbove),
Some(around("| A | B |\n| :-- | --: |\n| 1 | 2 |\n| | |\n| 3 | 4 |").as_str()) around("| A | B |\n| :-- | --: |\n| 1 | 2 |\n| | |\n| 3 | 4 |")
); );
assert_eq!( assert_eq!(
edited("A", TableEdit::RowBelow).as_deref(), edited("A", TableEdit::RowBelow),
Some(around("| A | B |\n| :-- | --: |\n| | |\n| 1 | 2 |\n| 3 | 4 |").as_str()) around("| A | B |\n| :-- | --: |\n| | |\n| 1 | 2 |\n| 3 | 4 |")
); );
assert_eq!( assert_eq!(
edited("2", TableEdit::ColumnLeft).as_deref(), edited("2", TableEdit::ColumnLeft),
Some(around("| A | | B |\n| :-- | --- | --: |\n| 1 | | 2 |\n| 3 | | 4 |").as_str()) around("| A | | B |\n| :-- | --- | --: |\n| 1 | | 2 |\n| 3 | | 4 |")
); );
assert_eq!( assert_eq!(
edited("1", TableEdit::DeleteRow).as_deref(), edited("1", TableEdit::DeleteRow),
Some(around("| A | B |\n| :-- | --: |\n| 3 | 4 |").as_str()) around("| A | B |\n| :-- | --: |\n| 3 | 4 |")
); );
assert_eq!( assert_eq!(
edited("B", TableEdit::DeleteColumn).as_deref(), edited("B", TableEdit::DeleteColumn),
Some(around("| A |\n| :-- |\n| 1 |\n| 3 |").as_str()) around("| A |\n| :-- |\n| 1 |\n| 3 |")
); );
} }
#[test] #[test]
fn the_head_stays_and_outside_is_no_table() { fn the_head_stays_and_outside_is_no_table() {
assert_eq!(edited("A", TableEdit::DeleteRow), Err(TableRefusal::Kept));
assert_eq!( assert_eq!(
edited("A", TableEdit::DeleteRow), edit_table(TABLE, 1, TableEdit::RowBelow).err(),
None, Some(TableRefusal::Outside)
"the head row stays"
); );
assert!(edit_table(TABLE, 1, TableEdit::RowBelow).is_none());
let single = "| A |\n| - |\n| 1 |\n"; let single = "| A |\n| - |\n| 1 |\n";
assert!( assert_eq!(
edit_table(single, 2, TableEdit::DeleteColumn).is_none(), edit_table(single, 2, TableEdit::DeleteColumn).err(),
"the last column stays" Some(TableRefusal::Kept)
);
}
#[test]
fn cells_past_the_head_stay_and_take_no_column_edit() {
let text = "| A | B |\n| - | - |\n| 1 | 2 | 3 |\n";
assert_eq!(
edited_in(text, "3", TableEdit::DeleteColumn),
Err(TableRefusal::Beyond)
);
assert_eq!(
edited_in(text, "1", TableEdit::RowBelow),
Ok("| A | B |\n| --- | --- |\n| 1 | 2 | 3 |\n| | |\n".into())
);
}
#[test]
fn a_quoted_table_keeps_its_marks() {
let text = "> | A | B |\n> | - | - |\n> | 1 | 2 |\n";
assert_eq!(
edited_in(text, "1", TableEdit::RowBelow),
Ok("> | A | B |\n> | --- | --- |\n> | 1 | 2 |\n> | | |\n".into())
);
let mixed = "- | A |\n | - |\n | 1 |\n";
assert_eq!(
edited_in(mixed, "1", TableEdit::RowBelow),
Err(TableRefusal::Mixed)
); );
} }
} }
+56 -2
View File
@@ -46,7 +46,7 @@ fn a_table_edit_is_one_undo_step_at_the_caret(cx: &mut TestAppContext) {
let edited = document.update(cx, |document, cx| { let edited = document.update(cx, |document, cx| {
document.edit_table(crate::documents::TableEdit::RowBelow, cx) document.edit_table(crate::documents::TableEdit::RowBelow, cx)
}); });
assert!(edited); assert_eq!(edited, Ok(()));
assert_eq!(text(&document, cx), "| A |\n| --- |\n| 1 |\n| |\n"); assert_eq!(text(&document, cx), "| A |\n| --- |\n| 1 |\n| |\n");
let editor = document.read_with(cx, |document, _| document.editor().clone()); let editor = document.read_with(cx, |document, _| document.editor().clone());
editor.update(cx, |editor, cx| editor.undo(cx)); editor.update(cx, |editor, cx| editor.undo(cx));
@@ -56,7 +56,9 @@ fn a_table_edit_is_one_undo_step_at_the_caret(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn a_new_table_goes_after_the_caret_line(cx: &mut TestAppContext) { fn a_new_table_goes_after_the_caret_line(cx: &mut TestAppContext) {
let (document, cx) = open("Intro\nNext\n", MarkdownMode::Visual, cx); let (document, cx) = open("Intro\nNext\n", MarkdownMode::Visual, cx);
document.update(cx, |document, cx| document.insert_table(cx)); document.update(cx, |document, cx| {
document.insert_table(cx).expect("inserted")
});
assert_eq!( assert_eq!(
text(&document, cx), text(&document, cx),
"Intro\n\n| | | |\n| --- | --- | --- |\n| | | |\n| | | |\n\nNext\n" "Intro\n\n| | | |\n| --- | --- | --- |\n| | | |\n| | | |\n\nNext\n"
@@ -93,3 +95,55 @@ fn every_replacement_is_one_undo_step(cx: &mut TestAppContext) {
editor.update(cx, |editor, cx| editor.undo(cx)); editor.update(cx, |editor, cx| editor.undo(cx));
assert_eq!(text(&document, cx), "a b a"); assert_eq!(text(&document, cx), "a b a");
} }
const FENCED: &str = "```text\n- a\n```\n";
#[gpui::test]
fn enter_in_source_code_is_a_plain_line_break(cx: &mut TestAppContext) {
let (document, cx) = open(FENCED, MarkdownMode::Source, cx);
document.update(cx, |document, cx| document.set_caret(1, 3, cx));
let editor = document.read_with(cx, |document, _| document.editor().clone());
let focus = editor.read_with(cx, gpui::Focusable::focus_handle);
cx.update(|window, cx| window.focus(&focus, cx));
cx.simulate_keystrokes("enter");
assert_eq!(text(&document, cx), "```text\n- a\n\n```\n");
}
#[gpui::test]
fn enter_in_an_open_code_block_is_a_plain_line_break(cx: &mut TestAppContext) {
let (document, cx) = open(FENCED, MarkdownMode::Visual, cx);
press(0, Modifiers::none(), cx);
document.update(cx, |document, cx| {
let input = document.open.as_ref().expect("open").input.clone();
input.update(cx, |input, cx| input.select(11..11, cx));
});
cx.simulate_keystrokes("enter");
assert_eq!(opened(&document, cx), "```text\n- a\n\n```");
}
#[gpui::test]
fn a_match_shown_after_a_replacement_opens_selected(cx: &mut TestAppContext) {
let (document, cx) = open("a b a\n", MarkdownMode::Visual, cx);
document.update_in(cx, |document, window, cx| {
document.replace(vec![(0..1, "x".into())], cx);
document.show_match(4..5, window, cx);
});
cx.run_until_parked();
cx.simulate_input("z");
assert_eq!(text(&document, cx), "x b z\n");
}
#[gpui::test]
fn preview_takes_no_edits(cx: &mut TestAppContext) {
let (document, cx) = open("| A |\n| - |\n| 1 |\n", MarkdownMode::Preview, cx);
let refused = document.update(cx, |document, cx| {
(
document.replace(vec![(2..3, "B".into())], cx),
document.edit_table(crate::documents::TableEdit::RowBelow, cx),
document.insert_table(cx),
)
});
let preview = Err(crate::documents::TableRefusal::Preview);
assert_eq!(refused, (false, preview, preview));
assert_eq!(text(&document, cx), "| A |\n| - |\n| 1 |\n");
}
+1
View File
@@ -139,6 +139,7 @@ impl Render for MarkdownDocument {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
// A top line asked before its text was read. // A top line asked before its text was read.
self.place_top(window, cx); self.place_top(window, cx);
self.show_waiting(window, cx);
let colors = cx.theme().colors.clone(); let colors = cx.theme().colors.clone();
let root = div() let root = div()
.size_full() .size_full()
+3 -1
View File
@@ -34,7 +34,9 @@ pub use blocks::{Align, BlockData, BlockEditor, BlockEvent, BlockKind, Media};
pub use contents::outline; pub use contents::outline;
pub use counts::{ReadingTime, WordCount}; pub use counts::{ReadingTime, WordCount};
pub use diagram::Diagram; pub use diagram::Diagram;
pub use document::{DocumentEvent, DocumentHeading, LARGEST_READ, MarkdownDocument, TableEdit}; pub use document::{
DocumentEvent, DocumentHeading, LARGEST_READ, MarkdownDocument, TableEdit, TableRefusal,
};
pub use epub::{Chapter, EpubReader}; pub use epub::{Chapter, EpubReader};
pub use format::{Format, format}; pub use format::{Format, format};
pub(crate) use history::Change; pub(crate) use history::Change;