From b83b4ad7c7ecaa924072e5f78895b10372e73469 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Oct 2021 16:09:30 -0700 Subject: [PATCH 01/28] Start work on a Buffer API for requesting autoindent on the next parse Co-Authored-By: Nathan Sobo --- crates/buffer/src/language.rs | 40 ++-- crates/buffer/src/lib.rs | 308 +++++++++++++++++++++++++- crates/editor/src/lib.rs | 12 + crates/zed/languages/rust/indents.scm | 10 + crates/zed/src/language.rs | 2 + 5 files changed, 341 insertions(+), 31 deletions(-) create mode 100644 crates/zed/languages/rust/indents.scm diff --git a/crates/buffer/src/language.rs b/crates/buffer/src/language.rs index 1a9a29aac5..e7755070cf 100644 --- a/crates/buffer/src/language.rs +++ b/crates/buffer/src/language.rs @@ -23,8 +23,9 @@ pub struct AutoclosePair { pub struct Language { pub(crate) config: LanguageConfig, pub(crate) grammar: Grammar, - pub(crate) highlight_query: Query, + pub(crate) highlights_query: Query, pub(crate) brackets_query: Query, + pub(crate) indents_query: Query, pub(crate) highlight_map: Mutex, } @@ -68,19 +69,25 @@ impl Language { Self { config, brackets_query: Query::new(grammar, "").unwrap(), - highlight_query: Query::new(grammar, "").unwrap(), + highlights_query: Query::new(grammar, "").unwrap(), + indents_query: Query::new(grammar, "").unwrap(), grammar, highlight_map: Default::default(), } } - pub fn with_highlights_query(mut self, highlights_query_source: &str) -> Result { - self.highlight_query = Query::new(self.grammar, highlights_query_source)?; + pub fn with_highlights_query(mut self, source: &str) -> Result { + self.highlights_query = Query::new(self.grammar, source)?; Ok(self) } - pub fn with_brackets_query(mut self, brackets_query_source: &str) -> Result { - self.brackets_query = Query::new(self.grammar, brackets_query_source)?; + pub fn with_brackets_query(mut self, source: &str) -> Result { + self.brackets_query = Query::new(self.grammar, source)?; + Ok(self) + } + + pub fn with_indents_query(mut self, source: &str) -> Result { + self.indents_query = Query::new(self.grammar, source)?; Ok(self) } @@ -97,7 +104,8 @@ impl Language { } pub fn set_theme(&self, theme: &SyntaxTheme) { - *self.highlight_map.lock() = HighlightMap::new(self.highlight_query.capture_names(), theme); + *self.highlight_map.lock() = + HighlightMap::new(self.highlights_query.capture_names(), theme); } } @@ -110,28 +118,22 @@ mod tests { let grammar = tree_sitter_rust::language(); let registry = LanguageRegistry { languages: vec![ - Arc::new(Language { - config: LanguageConfig { + Arc::new(Language::new( + LanguageConfig { name: "Rust".to_string(), path_suffixes: vec!["rs".to_string()], ..Default::default() }, grammar, - highlight_query: Query::new(grammar, "").unwrap(), - brackets_query: Query::new(grammar, "").unwrap(), - highlight_map: Default::default(), - }), - Arc::new(Language { - config: LanguageConfig { + )), + Arc::new(Language::new( + LanguageConfig { name: "Make".to_string(), path_suffixes: vec!["Makefile".to_string(), "mk".to_string()], ..Default::default() }, grammar, - highlight_query: Query::new(grammar, "").unwrap(), - brackets_query: Query::new(grammar, "").unwrap(), - highlight_map: Default::default(), - }), + )), ], }; diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index a2f34a5d78..edeac8cec6 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -30,10 +30,12 @@ use std::{ any::Any, cell::RefCell, cmp, + collections::BTreeMap, convert::{TryFrom, TryInto}, ffi::OsString, hash::BuildHasher, iter::Iterator, + mem, ops::{Deref, DerefMut, Range}, path::{Path, PathBuf}, str, @@ -149,6 +151,7 @@ impl Drop for QueryCursorHandle { QUERY_CURSORS.lock().push(cursor) } } +const SPACES: &'static str = " "; pub struct Buffer { fragments: SumTree, @@ -162,6 +165,7 @@ pub struct Buffer { history: History, file: Option>, language: Option>, + autoindent_requests: Vec, sync_parse_timeout: Duration, syntax_tree: Mutex>, parsing_in_background: bool, @@ -190,6 +194,13 @@ struct SyntaxTree { version: clock::Global, } +#[derive(Clone, Debug)] +struct AutoindentRequest { + position: Anchor, + indent_size: u8, + force: bool, +} + #[derive(Clone, Debug)] struct Transaction { start: clock::Global, @@ -628,6 +639,7 @@ impl Buffer { parsing_in_background: false, parse_count: 0, sync_parse_timeout: Duration::from_millis(1), + autoindent_requests: Default::default(), language, saved_mtime, selections: HashMap::default(), @@ -878,14 +890,13 @@ impl Buffer { } if let Some(language) = self.language.clone() { - // The parse tree is out of date, so grab the syntax tree to synchronously - // splice all the edits that have happened since the last parse. - let old_tree = self.syntax_tree(); - let parsed_text = self.visible_text.clone(); + let old_snapshot = self.snapshot(); let parsed_version = self.version(); let parse_task = cx.background().spawn({ let language = language.clone(); - async move { Self::parse_text(&parsed_text, old_tree, &language) } + let text = old_snapshot.visible_text.clone(); + let tree = old_snapshot.tree.clone(); + async move { Self::parse_text(&text, tree, &language) } }); match cx @@ -894,13 +905,12 @@ impl Buffer { { Ok(new_tree) => { *self.syntax_tree.lock() = Some(SyntaxTree { - tree: new_tree, + tree: new_tree.clone(), dirty: false, version: parsed_version, }); self.parse_count += 1; - cx.emit(Event::Reparsed); - cx.notify(); + self.did_finish_parsing(new_tree, old_snapshot, language, cx); return true; } Err(parse_task) => { @@ -914,7 +924,7 @@ impl Buffer { }); let parse_again = this.version > parsed_version || language_changed; *this.syntax_tree.lock() = Some(SyntaxTree { - tree: new_tree, + tree: new_tree.clone(), dirty: false, version: parsed_version, }); @@ -925,8 +935,7 @@ impl Buffer { return; } - cx.emit(Event::Reparsed); - cx.notify(); + this.did_finish_parsing(new_tree, old_snapshot, language, cx); }); }) .detach(); @@ -956,6 +965,243 @@ impl Buffer { }) } + fn did_finish_parsing( + &mut self, + new_tree: Tree, + old_snapshot: Snapshot, + language: Arc, + cx: &mut ModelContext, + ) { + let mut autoindent_requests_by_row = BTreeMap::::default(); + let mut autoindent_requests = mem::take(&mut self.autoindent_requests); + for request in autoindent_requests.drain(..) { + let row = request.position.to_point(&*self).row; + autoindent_requests_by_row + .entry(row) + .and_modify(|req| { + req.indent_size = req.indent_size.max(request.indent_size); + req.force |= request.force; + }) + .or_insert(request); + } + self.autoindent_requests = autoindent_requests; + + let mut cursor = QueryCursorHandle::new(); + + self.start_transaction(None).unwrap(); + let mut row_range = None; + for row in autoindent_requests_by_row.keys().copied() { + match &mut row_range { + None => row_range = Some(row..(row + 1)), + Some(range) => { + if range.end == row { + range.end += 1; + } else { + self.perform_autoindent( + range.clone(), + &new_tree, + &old_snapshot, + &autoindent_requests_by_row, + language.as_ref(), + &mut cursor, + cx, + ); + row_range.take(); + } + } + } + } + if let Some(range) = row_range { + self.perform_autoindent( + range, + &new_tree, + &old_snapshot, + &autoindent_requests_by_row, + language.as_ref(), + &mut cursor, + cx, + ); + } + self.end_transaction(None, cx).unwrap(); + + cx.emit(Event::Reparsed); + cx.notify(); + } + + fn perform_autoindent( + &mut self, + row_range: Range, + new_tree: &Tree, + old_snapshot: &Snapshot, + autoindent_requests: &BTreeMap, + language: &Language, + cursor: &mut QueryCursor, + cx: &mut ModelContext, + ) { + let max_row = self.row_count() - 1; + let prev_non_blank_row = self.prev_non_blank_row(row_range.start); + + // Get the "indentation ranges" that intersect this row range. + let indent_capture_ix = language.indents_query.capture_index_for_name("indent"); + let end_capture_ix = language.indents_query.capture_index_for_name("end"); + cursor.set_point_range( + Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0).into() + ..Point::new(row_range.end, 0).into(), + ); + let mut indentation_ranges = Vec::>::new(); + for mat in cursor.matches( + &language.indents_query, + new_tree.root_node(), + TextProvider(&self.visible_text), + ) { + let mut start_row = None; + let mut end_row = None; + for capture in mat.captures { + if Some(capture.index) == indent_capture_ix { + start_row.get_or_insert(capture.node.start_position().row as u32); + end_row.get_or_insert(max_row.min(capture.node.end_position().row as u32 + 1)); + } else if Some(capture.index) == end_capture_ix { + end_row = Some(capture.node.start_position().row as u32); + } + } + if let Some((start_row, end_row)) = start_row.zip(end_row) { + let range = start_row..end_row; + match indentation_ranges.binary_search_by_key(&range.start, |r| r.start) { + Err(ix) => indentation_ranges.insert(ix, range), + Ok(ix) => { + let prev_range = &mut indentation_ranges[ix]; + prev_range.end = prev_range.end.max(range.end); + } + } + } + } + + #[derive(Debug)] + struct Adjustment { + row: u32, + indent: bool, + outdent: bool, + } + + let mut adjustments = Vec::::new(); + 'outer: for range in &indentation_ranges { + let mut indent_row = range.start; + loop { + indent_row += 1; + if indent_row > max_row { + continue 'outer; + } + if row_range.contains(&indent_row) || !self.is_line_blank(indent_row) { + break; + } + } + + let mut outdent_row = range.end; + loop { + outdent_row += 1; + if outdent_row > max_row { + break; + } + if row_range.contains(&outdent_row) || !self.is_line_blank(outdent_row) { + break; + } + } + + match adjustments.binary_search_by_key(&indent_row, |a| a.row) { + Ok(ix) => adjustments[ix].indent = true, + Err(ix) => adjustments.insert( + ix, + Adjustment { + row: indent_row, + indent: true, + outdent: false, + }, + ), + } + match adjustments.binary_search_by_key(&outdent_row, |a| a.row) { + Ok(ix) => adjustments[ix].outdent = true, + Err(ix) => adjustments.insert( + ix, + Adjustment { + row: outdent_row, + indent: false, + outdent: true, + }, + ), + } + } + + let mut adjustments = adjustments.iter().peekable(); + for row in row_range { + if let Some(request) = autoindent_requests.get(&row) { + while let Some(adjustment) = adjustments.peek() { + if adjustment.row < row { + adjustments.next(); + } else { + if adjustment.row == row { + match (adjustment.indent, adjustment.outdent) { + (true, false) => self.indent_line(row, request.indent_size, cx), + (false, true) => self.outdent_line(row, request.indent_size, cx), + _ => {} + } + } + break; + } + } + } + } + } + + fn prev_non_blank_row(&self, mut row: u32) -> Option { + while row > 0 { + row -= 1; + if !self.is_line_blank(row) { + return Some(row); + } + } + None + } + + fn next_non_blank_row(&self, mut row: u32) -> Option { + let row_count = self.row_count(); + row += 1; + while row < row_count { + if !self.is_line_blank(row) { + return Some(row); + } + row += 1; + } + None + } + + fn indent_line(&mut self, row: u32, size: u8, cx: &mut ModelContext) { + self.edit( + [Point::new(row, 0)..Point::new(row, 0)], + &SPACES[..(size as usize)], + cx, + ) + } + + fn outdent_line(&mut self, row: u32, size: u8, cx: &mut ModelContext) { + let mut end_column = 0; + for c in self.chars_at(Point::new(row, 0)) { + if c == ' ' { + end_column += 1; + if end_column == size as u32 { + break; + } + } else { + break; + } + } + self.edit([Point::new(row, 0)..Point::new(row, end_column)], "", cx); + } + + fn is_line_blank(&self, row: u32) -> bool { + self.text_for_range(Point::new(row, 0)..Point::new(row, self.line_len(row))) + .all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none()) + } + pub fn range_for_syntax_ancestor(&self, range: Range) -> Option> { if let Some(tree) = self.syntax_tree() { let root = tree.root_node(); @@ -997,6 +1243,18 @@ impl Buffer { .min_by_key(|(open_range, close_range)| close_range.end - open_range.start) } + pub fn request_autoindent_for_line(&mut self, row: u32, indent_size: u8, force: bool) { + assert!( + self.history.transaction_depth > 0, + "autoindent can only be requested during a transaction" + ); + self.autoindent_requests.push(AutoindentRequest { + position: self.anchor_before(Point::new(row, 0)), + indent_size, + force, + }); + } + fn diff(&self, new_text: Arc, cx: &AppContext) -> Task { // TODO: it would be nice to not allocate here. let old_text = self.text(); @@ -2162,6 +2420,7 @@ impl Clone for Buffer { parsing_in_background: false, sync_parse_timeout: self.sync_parse_timeout, parse_count: self.parse_count, + autoindent_requests: self.autoindent_requests.clone(), deferred_replicas: self.deferred_replicas.clone(), replica_id: self.replica_id, remote_id: self.remote_id.clone(), @@ -2227,7 +2486,7 @@ impl Snapshot { let chunks = self.visible_text.chunks_in_range(range.clone()); if let Some((language, tree)) = self.language.as_ref().zip(self.tree.as_ref()) { let captures = self.query_cursor.set_byte_range(range.clone()).captures( - &language.highlight_query, + &language.highlights_query, tree.root_node(), TextProvider(&self.visible_text), ); @@ -4016,6 +4275,29 @@ mod tests { }); } + #[gpui::test] + async fn test_request_autoindent(mut cx: gpui::TestAppContext) { + let rust_lang = rust_lang(); + let buffer = cx.add_model(|cx| { + let text = "fn a() {}".into(); + Buffer::from_history(0, History::new(text), None, Some(rust_lang.clone()), cx) + }); + + buffer + .condition(&cx, |buffer, _| !buffer.is_parsing()) + .await; + + buffer.update(&mut cx, |buffer, cx| { + buffer.start_transaction(None).unwrap(); + buffer.edit([8..8], "\n\n", cx); + buffer.request_autoindent_for_line(1, 4, true); + assert_eq!(buffer.text(), "fn a() {\n\n}"); + + buffer.end_transaction(None, cx).unwrap(); + assert_eq!(buffer.text(), "fn a() {\n \n}"); + }); + } + #[derive(Clone)] struct Envelope { message: T, @@ -4104,6 +4386,8 @@ mod tests { }, tree_sitter_rust::language(), ) + .with_indents_query(r#" (_ "{" "}" @end) @indent "#) + .unwrap() .with_brackets_query(r#" ("{" @open "}" @close) "#) .unwrap(), ) diff --git a/crates/editor/src/lib.rs b/crates/editor/src/lib.rs index 856aa37799..3fa4bdfa27 100644 --- a/crates/editor/src/lib.rs +++ b/crates/editor/src/lib.rs @@ -745,6 +745,7 @@ impl Editor { if !self.skip_autoclose_end(text, cx) { self.start_transaction(cx); self.insert(text, cx); + self.request_autoindent(cx); self.autoclose_pairs(cx); self.end_transaction(cx); } @@ -792,6 +793,17 @@ impl Editor { self.end_transaction(cx); } + fn request_autoindent(&mut self, cx: &mut ViewContext) { + let selections = self.selections(cx); + let tab_size = self.build_settings.borrow()(cx).tab_size as u8; + self.buffer.update(cx, |buffer, _| { + for selection in selections.iter() { + let row = selection.head().to_point(&*buffer).row; + buffer.request_autoindent_for_line(row, tab_size, true); + } + }); + } + fn autoclose_pairs(&mut self, cx: &mut ViewContext) { let selections = self.selections(cx); let new_autoclose_pair_state = self.buffer.update(cx, |buffer, cx| { diff --git a/crates/zed/languages/rust/indents.scm b/crates/zed/languages/rust/indents.scm new file mode 100644 index 0000000000..8893ba41b7 --- /dev/null +++ b/crates/zed/languages/rust/indents.scm @@ -0,0 +1,10 @@ +[ + (where_clause) + (field_expression) + (call_expression) +] @indent + +(_ "[" "]" @end) @indent +(_ "<" ">" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index 774d8c6502..5a542ffc48 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -22,6 +22,8 @@ fn rust() -> Language { .unwrap() .with_brackets_query(load_query("rust/brackets.scm").as_ref()) .unwrap() + .with_indents_query(load_query("rust/indents.scm").as_ref()) + .unwrap() } fn load_query(path: &str) -> Cow<'static, str> { From add1467d32b80632f08b696fc6770e674e206c31 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Oct 2021 22:16:18 -0700 Subject: [PATCH 02/28] Generalize strategy for processing indentation ranges * Take into account the ranges' start and end columns, not just the rows * Generalize the approach to dedenting --- crates/buffer/src/lib.rs | 203 +++++++++++++++++---------------------- 1 file changed, 88 insertions(+), 115 deletions(-) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index edeac8cec6..fffb6179b9 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -151,8 +151,6 @@ impl Drop for QueryCursorHandle { QUERY_CURSORS.lock().push(cursor) } } -const SPACES: &'static str = " "; - pub struct Buffer { fragments: SumTree, visible_text: Rope, @@ -972,8 +970,8 @@ impl Buffer { language: Arc, cx: &mut ModelContext, ) { - let mut autoindent_requests_by_row = BTreeMap::::default(); let mut autoindent_requests = mem::take(&mut self.autoindent_requests); + let mut autoindent_requests_by_row = BTreeMap::::default(); for request in autoindent_requests.drain(..) { let row = request.position.to_point(&*self).row; autoindent_requests_by_row @@ -986,10 +984,9 @@ impl Buffer { } self.autoindent_requests = autoindent_requests; - let mut cursor = QueryCursorHandle::new(); - - self.start_transaction(None).unwrap(); let mut row_range = None; + let mut cursor = QueryCursorHandle::new(); + self.start_transaction(None).unwrap(); for row in autoindent_requests_by_row.keys().copied() { match &mut row_range { None => row_range = Some(row..(row + 1)), @@ -1038,7 +1035,6 @@ impl Buffer { cursor: &mut QueryCursor, cx: &mut ModelContext, ) { - let max_row = self.row_count() - 1; let prev_non_blank_row = self.prev_non_blank_row(row_range.start); // Get the "indentation ranges" that intersect this row range. @@ -1048,107 +1044,77 @@ impl Buffer { Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0).into() ..Point::new(row_range.end, 0).into(), ); - let mut indentation_ranges = Vec::>::new(); + let mut indentation_ranges = Vec::<(Range, &'static str)>::new(); for mat in cursor.matches( &language.indents_query, new_tree.root_node(), TextProvider(&self.visible_text), ) { - let mut start_row = None; - let mut end_row = None; + let mut node_kind = ""; + let mut start: Option = None; + let mut end: Option = None; for capture in mat.captures { if Some(capture.index) == indent_capture_ix { - start_row.get_or_insert(capture.node.start_position().row as u32); - end_row.get_or_insert(max_row.min(capture.node.end_position().row as u32 + 1)); + node_kind = capture.node.kind(); + start.get_or_insert(capture.node.start_position().into()); + end.get_or_insert(capture.node.end_position().into()); } else if Some(capture.index) == end_capture_ix { - end_row = Some(capture.node.start_position().row as u32); + end = Some(capture.node.start_position().into()); } } - if let Some((start_row, end_row)) = start_row.zip(end_row) { - let range = start_row..end_row; - match indentation_ranges.binary_search_by_key(&range.start, |r| r.start) { - Err(ix) => indentation_ranges.insert(ix, range), + + if let Some((start, end)) = start.zip(end) { + if start.row == end.row { + continue; + } + + let range = start..end; + match indentation_ranges.binary_search_by_key(&range.start, |r| r.0.start) { + Err(ix) => indentation_ranges.insert(ix, (range, node_kind)), Ok(ix) => { let prev_range = &mut indentation_ranges[ix]; - prev_range.end = prev_range.end.max(range.end); + prev_range.0.end = prev_range.0.end.max(range.end); } } } } - #[derive(Debug)] - struct Adjustment { - row: u32, - indent: bool, - outdent: bool, - } - - let mut adjustments = Vec::::new(); - 'outer: for range in &indentation_ranges { - let mut indent_row = range.start; - loop { - indent_row += 1; - if indent_row > max_row { - continue 'outer; - } - if row_range.contains(&indent_row) || !self.is_line_blank(indent_row) { - break; - } - } - - let mut outdent_row = range.end; - loop { - outdent_row += 1; - if outdent_row > max_row { - break; - } - if row_range.contains(&outdent_row) || !self.is_line_blank(outdent_row) { - break; - } - } - - match adjustments.binary_search_by_key(&indent_row, |a| a.row) { - Ok(ix) => adjustments[ix].indent = true, - Err(ix) => adjustments.insert( - ix, - Adjustment { - row: indent_row, - indent: true, - outdent: false, - }, - ), - } - match adjustments.binary_search_by_key(&outdent_row, |a| a.row) { - Ok(ix) => adjustments[ix].outdent = true, - Err(ix) => adjustments.insert( - ix, - Adjustment { - row: outdent_row, - indent: false, - outdent: true, - }, - ), - } - } - - let mut adjustments = adjustments.iter().peekable(); + let mut prev_row = prev_non_blank_row.unwrap_or(0); + let mut prev_indent_column = + prev_non_blank_row.map_or(0, |prev_row| self.indent_column_for_line(prev_row, cx)); for row in row_range { - if let Some(request) = autoindent_requests.get(&row) { - while let Some(adjustment) = adjustments.peek() { - if adjustment.row < row { - adjustments.next(); - } else { - if adjustment.row == row { - match (adjustment.indent, adjustment.outdent) { - (true, false) => self.indent_line(row, request.indent_size, cx), - (false, true) => self.outdent_line(row, request.indent_size, cx), - _ => {} - } - } - break; - } + let request = autoindent_requests.get(&row).unwrap(); + let row_start = Point::new(row, self.indent_column_for_line(row, cx)); + + eprintln!("autoindent row: {:?}", row); + + let mut increase_from_prev_row = false; + let mut dedent_to_row = u32::MAX; + for (range, node_kind) in &indentation_ranges { + if range.start.row == prev_row && prev_row < row && range.end > row_start { + eprintln!(" indent because of {} {:?}", node_kind, range); + increase_from_prev_row = true; + break; + } + + if range.start.row < prev_row + && (Point::new(prev_row, 0)..=row_start).contains(&range.end) + { + eprintln!(" outdent because of {} {:?}", node_kind, range); + dedent_to_row = dedent_to_row.min(range.start.row); } } + + let mut indent_column = prev_indent_column; + if increase_from_prev_row { + indent_column += request.indent_size as u32; + } else if dedent_to_row < row { + indent_column = self.indent_column_for_line(dedent_to_row, cx); + } + + self.set_indent_column_for_line(row, indent_column, cx); + prev_indent_column = indent_column; + prev_row = row; } } @@ -1162,39 +1128,46 @@ impl Buffer { None } - fn next_non_blank_row(&self, mut row: u32) -> Option { - let row_count = self.row_count(); - row += 1; - while row < row_count { - if !self.is_line_blank(row) { - return Some(row); - } - row += 1; - } - None - } - - fn indent_line(&mut self, row: u32, size: u8, cx: &mut ModelContext) { - self.edit( - [Point::new(row, 0)..Point::new(row, 0)], - &SPACES[..(size as usize)], - cx, - ) - } - - fn outdent_line(&mut self, row: u32, size: u8, cx: &mut ModelContext) { - let mut end_column = 0; + fn indent_column_for_line(&mut self, row: u32, cx: &mut ModelContext) -> u32 { + let mut result = 0; for c in self.chars_at(Point::new(row, 0)) { if c == ' ' { - end_column += 1; - if end_column == size as u32 { - break; - } + result += 1; } else { break; } } - self.edit([Point::new(row, 0)..Point::new(row, end_column)], "", cx); + result + } + + fn set_indent_column_for_line(&mut self, row: u32, column: u32, cx: &mut ModelContext) { + let current_column = self.indent_column_for_line(row, cx); + if column > current_column { + let offset = self.visible_text.to_offset(Point::new(row, 0)); + + // TODO: do this differently. By replacing the preceding newline, + // we force the new indentation to come before any left-biased anchors + // on the line. + let delta = (column - current_column) as usize; + if offset > 0 { + let mut prefix = String::with_capacity(1 + delta); + prefix.push('\n'); + prefix.extend(std::iter::repeat(' ').take(delta)); + self.edit([(offset - 1)..offset], prefix, cx); + } else { + self.edit( + [offset..offset], + std::iter::repeat(' ').take(delta).collect::(), + cx, + ); + } + } else if column < current_column { + self.edit( + [Point::new(row, 0)..Point::new(row, current_column - column)], + "", + cx, + ); + } } fn is_line_blank(&self, row: u32) -> bool { From 77af9ef9022ac257d917322180115ebaa82d6f76 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Oct 2021 22:16:30 -0700 Subject: [PATCH 03/28] Add more rust indent nodes --- crates/zed/languages/rust/indents.scm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/zed/languages/rust/indents.scm b/crates/zed/languages/rust/indents.scm index 8893ba41b7..07863846ae 100644 --- a/crates/zed/languages/rust/indents.scm +++ b/crates/zed/languages/rust/indents.scm @@ -2,6 +2,8 @@ (where_clause) (field_expression) (call_expression) + (assignment_expression) + (let_declaration) ] @indent (_ "[" "]" @end) @indent From d36805c46459cd8611f8ab8fde0824eee4ae9c9c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 7 Oct 2021 16:06:55 +0200 Subject: [PATCH 04/28] Don't push empty transactions onto the undo stack Co-Authored-By: Nathan Sobo --- crates/buffer/src/lib.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index fffb6179b9..7fae4ecdd0 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -318,10 +318,15 @@ impl History { assert_ne!(self.transaction_depth, 0); self.transaction_depth -= 1; if self.transaction_depth == 0 { - let transaction = self.undo_stack.last_mut().unwrap(); - transaction.selections_after = selections; - transaction.last_edit_at = now; - Some(transaction) + if self.undo_stack.last().unwrap().ranges.is_empty() { + self.undo_stack.pop(); + None + } else { + let transaction = self.undo_stack.last_mut().unwrap(); + transaction.selections_after = selections; + transaction.last_edit_at = now; + Some(transaction) + } } else { None } @@ -3933,6 +3938,11 @@ mod tests { assert_eq!(buffer.text(), "ab2cde6"); assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]); + buffer.start_transaction_at(None, now).unwrap(); + buffer.end_transaction_at(None, now, cx).unwrap(); + buffer.undo(cx); + assert_eq!(buffer.text(), "12cde6"); + buffer }); } From 2018537bb83aa30cb71dc712235289b5f4472fa1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 7 Oct 2021 17:31:58 +0200 Subject: [PATCH 05/28] Introduce a `Tab` action to indent line or insert soft tabs Co-Authored-By: Nathan Sobo --- crates/buffer/src/lib.rs | 19 +++++++++++----- crates/editor/src/lib.rs | 49 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index 7fae4ecdd0..19b5707405 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -1086,12 +1086,15 @@ impl Buffer { let mut prev_row = prev_non_blank_row.unwrap_or(0); let mut prev_indent_column = - prev_non_blank_row.map_or(0, |prev_row| self.indent_column_for_line(prev_row, cx)); + prev_non_blank_row.map_or(0, |prev_row| self.indent_column_for_line(prev_row)); for row in row_range { let request = autoindent_requests.get(&row).unwrap(); - let row_start = Point::new(row, self.indent_column_for_line(row, cx)); + let row_start = Point::new(row, self.indent_column_for_line(row)); - eprintln!("autoindent row: {:?}", row); + eprintln!( + "autoindent row: {:?}, prev_indent_column: {:?}", + row, prev_indent_column + ); let mut increase_from_prev_row = false; let mut dedent_to_row = u32::MAX; @@ -1114,7 +1117,7 @@ impl Buffer { if increase_from_prev_row { indent_column += request.indent_size as u32; } else if dedent_to_row < row { - indent_column = self.indent_column_for_line(dedent_to_row, cx); + indent_column = self.indent_column_for_line(dedent_to_row); } self.set_indent_column_for_line(row, indent_column, cx); @@ -1133,7 +1136,7 @@ impl Buffer { None } - fn indent_column_for_line(&mut self, row: u32, cx: &mut ModelContext) -> u32 { + pub fn indent_column_for_line(&self, row: u32) -> u32 { let mut result = 0; for c in self.chars_at(Point::new(row, 0)) { if c == ' ' { @@ -1146,7 +1149,7 @@ impl Buffer { } fn set_indent_column_for_line(&mut self, row: u32, column: u32, cx: &mut ModelContext) { - let current_column = self.indent_column_for_line(row, cx); + let current_column = self.indent_column_for_line(row); if column > current_column { let offset = self.visible_text.to_offset(Point::new(row, 0)); @@ -1346,6 +1349,10 @@ impl Buffer { self.visible_text.chars_at(offset) } + pub fn chars_for_range(&self, range: Range) -> impl Iterator + '_ { + self.text_for_range(range).flat_map(str::chars) + } + pub fn bytes_at(&self, position: T) -> impl Iterator + '_ { let offset = position.to_offset(self); self.visible_text.bytes_at(offset) diff --git a/crates/editor/src/lib.rs b/crates/editor/src/lib.rs index 3fa4bdfa27..27cb92afa2 100644 --- a/crates/editor/src/lib.rs +++ b/crates/editor/src/lib.rs @@ -38,6 +38,7 @@ action!(Cancel); action!(Backspace); action!(Delete); action!(Input, String); +action!(Tab); action!(DeleteLine); action!(DeleteToPreviousWordBoundary); action!(DeleteToNextWordBoundary); @@ -101,7 +102,7 @@ pub fn init(cx: &mut MutableAppContext) { Input("\n".into()), Some("Editor && mode == auto_height"), ), - Binding::new("tab", Input("\t".into()), Some("Editor")), + Binding::new("tab", Tab, Some("Editor")), Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")), Binding::new( "alt-backspace", @@ -195,6 +196,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::handle_input); cx.add_action(Editor::backspace); cx.add_action(Editor::delete); + cx.add_action(Editor::tab); cx.add_action(Editor::delete_line); cx.add_action(Editor::delete_to_previous_word_boundary); cx.add_action(Editor::delete_to_next_word_boundary); @@ -962,6 +964,51 @@ impl Editor { self.end_transaction(cx); } + pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { + self.start_transaction(cx); + let tab_size = self.build_settings.borrow()(cx).tab_size; + let mut selections = self.selections(cx).to_vec(); + self.buffer.update(cx, |buffer, cx| { + let mut last_indented_row = None; + for selection in &mut selections { + let mut range = selection.point_range(buffer); + if range.is_empty() { + let char_column = buffer + .chars_for_range(Point::new(range.start.row, 0)..range.start) + .count(); + let chars_to_next_tab_stop = tab_size - (char_column % tab_size); + buffer.edit( + [range.start..range.start], + " ".repeat(chars_to_next_tab_stop), + cx, + ); + range.start.column += chars_to_next_tab_stop as u32; + + let head = buffer.anchor_before(range.start); + selection.start = head.clone(); + selection.end = head; + } else { + for row in range.start.row..=range.end.row { + if last_indented_row != Some(row) { + let char_column = buffer.indent_column_for_line(row) as usize; + let chars_to_next_tab_stop = tab_size - (char_column % tab_size); + let row_start = Point::new(row, 0); + buffer.edit( + [row_start..row_start], + " ".repeat(chars_to_next_tab_stop), + cx, + ); + last_indented_row = Some(row); + } + } + } + } + }); + + self.update_selections(selections, true, cx); + self.end_transaction(cx); + } + pub fn delete_line(&mut self, _: &DeleteLine, cx: &mut ViewContext) { self.start_transaction(cx); From 54932a805086d9a8974b944448f57d235e7307c8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 7 Oct 2021 19:09:14 +0200 Subject: [PATCH 06/28] WIP Co-Authored-By: Max Brunsfeld --- crates/buffer/src/lib.rs | 126 ++++++++++++++++++++++++--------------- 1 file changed, 78 insertions(+), 48 deletions(-) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index 19b5707405..ef3dcef929 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -151,6 +151,7 @@ impl Drop for QueryCursorHandle { QUERY_CURSORS.lock().push(cursor) } } + pub struct Buffer { fragments: SumTree, visible_text: Rope, @@ -192,6 +193,30 @@ struct SyntaxTree { version: clock::Global, } +impl SyntaxTree { + fn interpolate(&mut self, buffer: &Buffer) { + let mut delta = 0_isize; + for edit in buffer.edits_since(self.version.clone()) { + let start_offset = (edit.old_bytes.start as isize + delta) as usize; + let start_point = buffer.visible_text.to_point(start_offset); + self.tree.edit(&InputEdit { + start_byte: start_offset, + old_end_byte: start_offset + edit.deleted_bytes(), + new_end_byte: start_offset + edit.inserted_bytes(), + start_position: start_point.into(), + old_end_position: (start_point + edit.deleted_lines()).into(), + new_end_position: buffer + .visible_text + .to_point(start_offset + edit.inserted_bytes()) + .into(), + }); + delta += edit.inserted_bytes() as isize - edit.deleted_bytes() as isize; + self.dirty = true; + } + self.version = buffer.version(); + } +} + #[derive(Clone, Debug)] struct AutoindentRequest { position: Anchor, @@ -850,27 +875,9 @@ impl Buffer { self.parse_count } - pub fn syntax_tree(&self) -> Option { + fn syntax_tree(&self) -> Option { if let Some(syntax_tree) = self.syntax_tree.lock().as_mut() { - let mut delta = 0_isize; - for edit in self.edits_since(syntax_tree.version.clone()) { - let start_offset = (edit.old_bytes.start as isize + delta) as usize; - let start_point = self.visible_text.to_point(start_offset); - syntax_tree.tree.edit(&InputEdit { - start_byte: start_offset, - old_end_byte: start_offset + edit.deleted_bytes(), - new_end_byte: start_offset + edit.inserted_bytes(), - start_position: start_point.into(), - old_end_position: (start_point + edit.deleted_lines()).into(), - new_end_position: self - .visible_text - .to_point(start_offset + edit.inserted_bytes()) - .into(), - }); - delta += edit.inserted_bytes() as isize - edit.deleted_bytes() as isize; - syntax_tree.dirty = true; - } - syntax_tree.version = self.version(); + syntax_tree.interpolate(self); Some(syntax_tree.tree.clone()) } else { None @@ -893,13 +900,16 @@ impl Buffer { } if let Some(language) = self.language.clone() { - let old_snapshot = self.snapshot(); + let old_tree = self.syntax_tree.lock().as_mut().map(|tree| { + tree.interpolate(self); + tree.clone() + }); + let text = self.visible_text.clone(); let parsed_version = self.version(); let parse_task = cx.background().spawn({ let language = language.clone(); - let text = old_snapshot.visible_text.clone(); - let tree = old_snapshot.tree.clone(); - async move { Self::parse_text(&text, tree, &language) } + let old_tree = old_tree.as_ref().map(|t| t.tree.clone()); + async move { Self::parse_text(&text, old_tree, &language) } }); match cx @@ -907,13 +917,13 @@ impl Buffer { .block_with_timeout(self.sync_parse_timeout, parse_task) { Ok(new_tree) => { - *self.syntax_tree.lock() = Some(SyntaxTree { - tree: new_tree.clone(), - dirty: false, - version: parsed_version, - }); - self.parse_count += 1; - self.did_finish_parsing(new_tree, old_snapshot, language, cx); + self.did_finish_parsing( + old_tree.map(|t| t.tree), + new_tree, + parsed_version, + language, + cx, + ); return true; } Err(parse_task) => { @@ -926,19 +936,22 @@ impl Buffer { !Arc::ptr_eq(curr_language, &language) }); let parse_again = this.version > parsed_version || language_changed; - *this.syntax_tree.lock() = Some(SyntaxTree { - tree: new_tree.clone(), - dirty: false, - version: parsed_version, + let old_tree = old_tree.map(|mut old_tree| { + old_tree.interpolate(this); + old_tree.tree }); - this.parse_count += 1; this.parsing_in_background = false; + this.did_finish_parsing( + old_tree, + new_tree, + parsed_version, + language, + cx, + ); if parse_again && this.reparse(cx) { return; } - - this.did_finish_parsing(new_tree, old_snapshot, language, cx); }); }) .detach(); @@ -970,8 +983,28 @@ impl Buffer { fn did_finish_parsing( &mut self, + old_tree: Option, new_tree: Tree, - old_snapshot: Snapshot, + new_version: clock::Global, + language: Arc, + cx: &mut ModelContext, + ) { + self.perform_autoindent(old_tree, &new_tree, language, cx); + + self.parse_count += 1; + *self.syntax_tree.lock() = Some(SyntaxTree { + tree: new_tree, + dirty: false, + version: new_version, + }); + cx.emit(Event::Reparsed); + cx.notify(); + } + + fn perform_autoindent( + &mut self, + old_tree: Option, + new_tree: &Tree, language: Arc, cx: &mut ModelContext, ) { @@ -999,10 +1032,10 @@ impl Buffer { if range.end == row { range.end += 1; } else { - self.perform_autoindent( + self.perform_autoindent_for_rows( range.clone(), + old_tree.as_ref(), &new_tree, - &old_snapshot, &autoindent_requests_by_row, language.as_ref(), &mut cursor, @@ -1014,10 +1047,10 @@ impl Buffer { } } if let Some(range) = row_range { - self.perform_autoindent( + self.perform_autoindent_for_rows( range, + old_tree.as_ref(), &new_tree, - &old_snapshot, &autoindent_requests_by_row, language.as_ref(), &mut cursor, @@ -1025,16 +1058,13 @@ impl Buffer { ); } self.end_transaction(None, cx).unwrap(); - - cx.emit(Event::Reparsed); - cx.notify(); } - fn perform_autoindent( + fn perform_autoindent_for_rows( &mut self, row_range: Range, + old_tree: Option<&Tree>, new_tree: &Tree, - old_snapshot: &Snapshot, autoindent_requests: &BTreeMap, language: &Language, cursor: &mut QueryCursor, From 2f295382c461e61bed24000d7e9bf240c9ab7c4d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 7 Oct 2021 10:46:40 -0700 Subject: [PATCH 07/28] Implement selective auto-indent by comparing old and new suggestions Co-Authored-By: Antonio Scandurra --- crates/buffer/src/lib.rs | 58 ++++++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index ef3dcef929..ceed666870 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -1023,7 +1023,8 @@ impl Buffer { self.autoindent_requests = autoindent_requests; let mut row_range = None; - let mut cursor = QueryCursorHandle::new(); + let mut cursor1 = QueryCursorHandle::new(); + let mut cursor2 = QueryCursorHandle::new(); self.start_transaction(None).unwrap(); for row in autoindent_requests_by_row.keys().copied() { match &mut row_range { @@ -1038,7 +1039,8 @@ impl Buffer { &new_tree, &autoindent_requests_by_row, language.as_ref(), - &mut cursor, + &mut cursor1, + &mut cursor2, cx, ); row_range.take(); @@ -1053,7 +1055,8 @@ impl Buffer { &new_tree, &autoindent_requests_by_row, language.as_ref(), - &mut cursor, + &mut cursor1, + &mut cursor2, cx, ); } @@ -1067,9 +1070,48 @@ impl Buffer { new_tree: &Tree, autoindent_requests: &BTreeMap, language: &Language, - cursor: &mut QueryCursor, + cursor1: &mut QueryCursor, + cursor2: &mut QueryCursor, cx: &mut ModelContext, ) { + let new_suggestions = self.suggest_autoindents( + autoindent_requests, + row_range.clone(), + new_tree, + language, + cursor1, + ); + + if let Some(old_tree) = old_tree { + let old_suggestions = self.suggest_autoindents( + autoindent_requests, + row_range.clone(), + old_tree, + language, + cursor2, + ); + let suggestions = old_suggestions.zip(new_suggestions).collect::>(); + let requests = autoindent_requests.range(row_range); + for ((row, request), (old_suggestion, new_suggestion)) in requests.zip(suggestions) { + if request.force || new_suggestion != old_suggestion { + self.set_indent_column_for_line(*row, new_suggestion, cx); + } + } + } else { + for (row, new_suggestion) in row_range.zip(new_suggestions.collect::>()) { + self.set_indent_column_for_line(row, new_suggestion, cx); + } + } + } + + fn suggest_autoindents<'a>( + &'a self, + autoindent_requests: &'a BTreeMap, + row_range: Range, + tree: &Tree, + language: &Language, + cursor: &mut QueryCursor, + ) -> impl Iterator + 'a { let prev_non_blank_row = self.prev_non_blank_row(row_range.start); // Get the "indentation ranges" that intersect this row range. @@ -1082,7 +1124,7 @@ impl Buffer { let mut indentation_ranges = Vec::<(Range, &'static str)>::new(); for mat in cursor.matches( &language.indents_query, - new_tree.root_node(), + tree.root_node(), TextProvider(&self.visible_text), ) { let mut node_kind = ""; @@ -1117,7 +1159,7 @@ impl Buffer { let mut prev_row = prev_non_blank_row.unwrap_or(0); let mut prev_indent_column = prev_non_blank_row.map_or(0, |prev_row| self.indent_column_for_line(prev_row)); - for row in row_range { + row_range.map(move |row| { let request = autoindent_requests.get(&row).unwrap(); let row_start = Point::new(row, self.indent_column_for_line(row)); @@ -1150,10 +1192,10 @@ impl Buffer { indent_column = self.indent_column_for_line(dedent_to_row); } - self.set_indent_column_for_line(row, indent_column, cx); prev_indent_column = indent_column; prev_row = row; - } + indent_column + }) } fn prev_non_blank_row(&self, mut row: u32) -> Option { From b5d3ffb16c3383ffd031cb91bd2f6d4ab1e93898 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 7 Oct 2021 12:43:26 -0700 Subject: [PATCH 08/28] Fix collection of row ranges in Buffer::perform_autoindent --- crates/buffer/src/lib.rs | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index ceed666870..a3e993d339 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -1022,31 +1022,28 @@ impl Buffer { } self.autoindent_requests = autoindent_requests; - let mut row_range = None; + let mut row_range: Option> = None; let mut cursor1 = QueryCursorHandle::new(); let mut cursor2 = QueryCursorHandle::new(); self.start_transaction(None).unwrap(); for row in autoindent_requests_by_row.keys().copied() { - match &mut row_range { - None => row_range = Some(row..(row + 1)), - Some(range) => { - if range.end == row { - range.end += 1; - } else { - self.perform_autoindent_for_rows( - range.clone(), - old_tree.as_ref(), - &new_tree, - &autoindent_requests_by_row, - language.as_ref(), - &mut cursor1, - &mut cursor2, - cx, - ); - row_range.take(); - } + if let Some(range) = &mut row_range { + if range.end == row { + range.end += 1; + continue; } + self.perform_autoindent_for_rows( + range.clone(), + old_tree.as_ref(), + &new_tree, + &autoindent_requests_by_row, + language.as_ref(), + &mut cursor1, + &mut cursor2, + cx, + ); } + row_range = Some(row..(row + 1)); } if let Some(range) = row_range { self.perform_autoindent_for_rows( From d77025540acaa39593bb8499d792f470e4b50b8f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 7 Oct 2021 12:46:08 -0700 Subject: [PATCH 09/28] Remove no-longer-used `dirty` field on `buffer::SyntaxTree` This became unnecessary when we reworked our reparsing logic to block from 1ms on each parse. --- crates/buffer/src/lib.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index a3e993d339..7805844a05 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -189,7 +189,6 @@ pub struct SelectionSet { #[derive(Clone)] struct SyntaxTree { tree: Tree, - dirty: bool, version: clock::Global, } @@ -211,7 +210,6 @@ impl SyntaxTree { .into(), }); delta += edit.inserted_bytes() as isize - edit.deleted_bytes() as isize; - self.dirty = true; } self.version = buffer.version(); } @@ -990,11 +988,9 @@ impl Buffer { cx: &mut ModelContext, ) { self.perform_autoindent(old_tree, &new_tree, language, cx); - self.parse_count += 1; *self.syntax_tree.lock() = Some(SyntaxTree { tree: new_tree, - dirty: false, version: new_version, }); cx.emit(Event::Reparsed); From c0a75abcd231bda6a741b59af1ea612068036ce4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 7 Oct 2021 13:55:59 -0700 Subject: [PATCH 10/28] Preserve indentation for lines that are both indented and outdented Co-Authored-By: Nathan Sobo --- crates/buffer/src/lib.rs | 62 +++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index 7805844a05..6598d154b1 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -192,29 +192,6 @@ struct SyntaxTree { version: clock::Global, } -impl SyntaxTree { - fn interpolate(&mut self, buffer: &Buffer) { - let mut delta = 0_isize; - for edit in buffer.edits_since(self.version.clone()) { - let start_offset = (edit.old_bytes.start as isize + delta) as usize; - let start_point = buffer.visible_text.to_point(start_offset); - self.tree.edit(&InputEdit { - start_byte: start_offset, - old_end_byte: start_offset + edit.deleted_bytes(), - new_end_byte: start_offset + edit.inserted_bytes(), - start_position: start_point.into(), - old_end_position: (start_point + edit.deleted_lines()).into(), - new_end_position: buffer - .visible_text - .to_point(start_offset + edit.inserted_bytes()) - .into(), - }); - delta += edit.inserted_bytes() as isize - edit.deleted_bytes() as isize; - } - self.version = buffer.version(); - } -} - #[derive(Clone, Debug)] struct AutoindentRequest { position: Anchor, @@ -875,7 +852,7 @@ impl Buffer { fn syntax_tree(&self) -> Option { if let Some(syntax_tree) = self.syntax_tree.lock().as_mut() { - syntax_tree.interpolate(self); + self.interpolate_tree(syntax_tree); Some(syntax_tree.tree.clone()) } else { None @@ -899,7 +876,7 @@ impl Buffer { if let Some(language) = self.language.clone() { let old_tree = self.syntax_tree.lock().as_mut().map(|tree| { - tree.interpolate(self); + self.interpolate_tree(tree); tree.clone() }); let text = self.visible_text.clone(); @@ -935,7 +912,7 @@ impl Buffer { }); let parse_again = this.version > parsed_version || language_changed; let old_tree = old_tree.map(|mut old_tree| { - old_tree.interpolate(this); + this.interpolate_tree(&mut old_tree); old_tree.tree }); this.parsing_in_background = false; @@ -979,6 +956,27 @@ impl Buffer { }) } + fn interpolate_tree(&self, tree: &mut SyntaxTree) { + let mut delta = 0_isize; + for edit in self.edits_since(tree.version.clone()) { + let start_offset = (edit.old_bytes.start as isize + delta) as usize; + let start_point = self.visible_text.to_point(start_offset); + tree.tree.edit(&InputEdit { + start_byte: start_offset, + old_end_byte: start_offset + edit.deleted_bytes(), + new_end_byte: start_offset + edit.inserted_bytes(), + start_position: start_point.into(), + old_end_position: (start_point + edit.deleted_lines()).into(), + new_end_position: self + .visible_text + .to_point(start_offset + edit.inserted_bytes()) + .into(), + }); + delta += edit.inserted_bytes() as isize - edit.deleted_bytes() as isize; + } + tree.version = self.version(); + } + fn did_finish_parsing( &mut self, old_tree: Option, @@ -1167,10 +1165,8 @@ impl Buffer { if range.start.row == prev_row && prev_row < row && range.end > row_start { eprintln!(" indent because of {} {:?}", node_kind, range); increase_from_prev_row = true; - break; } - - if range.start.row < prev_row + if range.start.row < row && (Point::new(prev_row, 0)..=row_start).contains(&range.end) { eprintln!(" outdent because of {} {:?}", node_kind, range); @@ -1179,10 +1175,12 @@ impl Buffer { } let mut indent_column = prev_indent_column; - if increase_from_prev_row { + if dedent_to_row < row { + if !increase_from_prev_row { + indent_column = self.indent_column_for_line(dedent_to_row); + } + } else if increase_from_prev_row { indent_column += request.indent_size as u32; - } else if dedent_to_row < row { - indent_column = self.indent_column_for_line(dedent_to_row); } prev_indent_column = indent_column; From 451f0e7adbfd89dd41d1469ab46f39cb6019a759 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 7 Oct 2021 17:55:23 -0700 Subject: [PATCH 11/28] Replace Buffer::request_autoindent API with ::edit_with_autoindent When computing the "previous autoindent suggestion", we can't just use the old tree and the current text. We need to find out what the suggestion would have been before we made any changes. --- crates/buffer/src/lib.rs | 361 +++++++++++++++++++++------------------ crates/editor/src/lib.rs | 14 +- 2 files changed, 194 insertions(+), 181 deletions(-) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index 6598d154b1..2d145f52e4 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -116,6 +116,9 @@ lazy_static! { static ref QUERY_CURSORS: Mutex> = Default::default(); } +// TODO - Make this configurable +const INDENT_SIZE: u32 = 4; + struct QueryCursorHandle(Option); impl QueryCursorHandle { @@ -195,8 +198,8 @@ struct SyntaxTree { #[derive(Clone, Debug)] struct AutoindentRequest { position: Anchor, - indent_size: u8, - force: bool, + end_position: Option, + prev_suggestion: Option, } #[derive(Clone, Debug)] @@ -875,15 +878,11 @@ impl Buffer { } if let Some(language) = self.language.clone() { - let old_tree = self.syntax_tree.lock().as_mut().map(|tree| { - self.interpolate_tree(tree); - tree.clone() - }); + let old_tree = self.syntax_tree(); let text = self.visible_text.clone(); let parsed_version = self.version(); let parse_task = cx.background().spawn({ let language = language.clone(); - let old_tree = old_tree.as_ref().map(|t| t.tree.clone()); async move { Self::parse_text(&text, old_tree, &language) } }); @@ -892,13 +891,7 @@ impl Buffer { .block_with_timeout(self.sync_parse_timeout, parse_task) { Ok(new_tree) => { - self.did_finish_parsing( - old_tree.map(|t| t.tree), - new_tree, - parsed_version, - language, - cx, - ); + self.did_finish_parsing(new_tree, parsed_version, language, cx); return true; } Err(parse_task) => { @@ -911,18 +904,8 @@ impl Buffer { !Arc::ptr_eq(curr_language, &language) }); let parse_again = this.version > parsed_version || language_changed; - let old_tree = old_tree.map(|mut old_tree| { - this.interpolate_tree(&mut old_tree); - old_tree.tree - }); this.parsing_in_background = false; - this.did_finish_parsing( - old_tree, - new_tree, - parsed_version, - language, - cx, - ); + this.did_finish_parsing(new_tree, parsed_version, language, cx); if parse_again && this.reparse(cx) { return; @@ -979,125 +962,62 @@ impl Buffer { fn did_finish_parsing( &mut self, - old_tree: Option, - new_tree: Tree, - new_version: clock::Global, + tree: Tree, + version: clock::Global, language: Arc, cx: &mut ModelContext, ) { - self.perform_autoindent(old_tree, &new_tree, language, cx); + self.perform_autoindent(&tree, language, cx); self.parse_count += 1; - *self.syntax_tree.lock() = Some(SyntaxTree { - tree: new_tree, - version: new_version, - }); + *self.syntax_tree.lock() = Some(SyntaxTree { tree, version }); cx.emit(Event::Reparsed); cx.notify(); } fn perform_autoindent( &mut self, - old_tree: Option, new_tree: &Tree, language: Arc, cx: &mut ModelContext, ) { let mut autoindent_requests = mem::take(&mut self.autoindent_requests); - let mut autoindent_requests_by_row = BTreeMap::::default(); + let mut prev_suggestions_by_row = BTreeMap::>::default(); for request in autoindent_requests.drain(..) { - let row = request.position.to_point(&*self).row; - autoindent_requests_by_row - .entry(row) - .and_modify(|req| { - req.indent_size = req.indent_size.max(request.indent_size); - req.force |= request.force; - }) - .or_insert(request); + let start_row = request.position.to_point(&*self).row; + let end_row = if let Some(end_position) = request.end_position { + end_position.to_point(&*self).row + } else { + start_row + }; + for row in start_row..=end_row { + let prev_suggestion = request.prev_suggestion; + prev_suggestions_by_row + .entry(row) + .and_modify(|suggestion| *suggestion = suggestion.or(prev_suggestion)) + .or_insert(request.prev_suggestion); + } } self.autoindent_requests = autoindent_requests; - let mut row_range: Option> = None; - let mut cursor1 = QueryCursorHandle::new(); - let mut cursor2 = QueryCursorHandle::new(); + let mut cursor = QueryCursorHandle::new(); self.start_transaction(None).unwrap(); - for row in autoindent_requests_by_row.keys().copied() { - if let Some(range) = &mut row_range { - if range.end == row { - range.end += 1; - continue; + let mut prev_suggestions = prev_suggestions_by_row.iter(); + for row_range in contiguous_ranges(prev_suggestions_by_row.keys().copied()) { + let new_suggestions = self + .suggest_autoindents(row_range.clone(), new_tree, &language, &mut cursor) + .collect::>(); + let old_suggestions = prev_suggestions.by_ref().take(row_range.len()); + for ((row, old_suggestion), new_suggestion) in old_suggestions.zip(new_suggestions) { + if *old_suggestion != Some(new_suggestion) { + self.set_indent_column_for_line(*row, new_suggestion, cx); } - self.perform_autoindent_for_rows( - range.clone(), - old_tree.as_ref(), - &new_tree, - &autoindent_requests_by_row, - language.as_ref(), - &mut cursor1, - &mut cursor2, - cx, - ); } - row_range = Some(row..(row + 1)); - } - if let Some(range) = row_range { - self.perform_autoindent_for_rows( - range, - old_tree.as_ref(), - &new_tree, - &autoindent_requests_by_row, - language.as_ref(), - &mut cursor1, - &mut cursor2, - cx, - ); } self.end_transaction(None, cx).unwrap(); } - fn perform_autoindent_for_rows( - &mut self, - row_range: Range, - old_tree: Option<&Tree>, - new_tree: &Tree, - autoindent_requests: &BTreeMap, - language: &Language, - cursor1: &mut QueryCursor, - cursor2: &mut QueryCursor, - cx: &mut ModelContext, - ) { - let new_suggestions = self.suggest_autoindents( - autoindent_requests, - row_range.clone(), - new_tree, - language, - cursor1, - ); - - if let Some(old_tree) = old_tree { - let old_suggestions = self.suggest_autoindents( - autoindent_requests, - row_range.clone(), - old_tree, - language, - cursor2, - ); - let suggestions = old_suggestions.zip(new_suggestions).collect::>(); - let requests = autoindent_requests.range(row_range); - for ((row, request), (old_suggestion, new_suggestion)) in requests.zip(suggestions) { - if request.force || new_suggestion != old_suggestion { - self.set_indent_column_for_line(*row, new_suggestion, cx); - } - } - } else { - for (row, new_suggestion) in row_range.zip(new_suggestions.collect::>()) { - self.set_indent_column_for_line(row, new_suggestion, cx); - } - } - } - fn suggest_autoindents<'a>( &'a self, - autoindent_requests: &'a BTreeMap, row_range: Range, tree: &Tree, language: &Language, @@ -1151,7 +1071,6 @@ impl Buffer { let mut prev_indent_column = prev_non_blank_row.map_or(0, |prev_row| self.indent_column_for_line(prev_row)); row_range.map(move |row| { - let request = autoindent_requests.get(&row).unwrap(); let row_start = Point::new(row, self.indent_column_for_line(row)); eprintln!( @@ -1159,16 +1078,17 @@ impl Buffer { row, prev_indent_column ); - let mut increase_from_prev_row = false; + let mut indent_from_prev_row = false; let mut dedent_to_row = u32::MAX; for (range, node_kind) in &indentation_ranges { - if range.start.row == prev_row && prev_row < row && range.end > row_start { - eprintln!(" indent because of {} {:?}", node_kind, range); - increase_from_prev_row = true; + if range.start.row >= row { + break; } - if range.start.row < row - && (Point::new(prev_row, 0)..=row_start).contains(&range.end) - { + if range.start.row == prev_row && range.end > row_start { + eprintln!(" indent because of {} {:?}", node_kind, range); + indent_from_prev_row = true; + } + if range.end.row >= prev_row && range.end <= row_start { eprintln!(" outdent because of {} {:?}", node_kind, range); dedent_to_row = dedent_to_row.min(range.start.row); } @@ -1176,11 +1096,11 @@ impl Buffer { let mut indent_column = prev_indent_column; if dedent_to_row < row { - if !increase_from_prev_row { + if !indent_from_prev_row { indent_column = self.indent_column_for_line(dedent_to_row); } - } else if increase_from_prev_row { - indent_column += request.indent_size as u32; + } else if indent_from_prev_row { + indent_column += INDENT_SIZE; } prev_indent_column = indent_column; @@ -1287,18 +1207,6 @@ impl Buffer { .min_by_key(|(open_range, close_range)| close_range.end - open_range.start) } - pub fn request_autoindent_for_line(&mut self, row: u32, indent_size: u8, force: bool) { - assert!( - self.history.transaction_depth > 0, - "autoindent can only be requested during a transaction" - ); - self.autoindent_requests.push(AutoindentRequest { - position: self.anchor_before(Point::new(row, 0)), - indent_size, - force, - }); - } - fn diff(&self, new_text: Arc, cx: &AppContext) -> Task { // TODO: it would be nice to not allocate here. let old_text = self.text(); @@ -1524,20 +1432,41 @@ impl Buffer { I: IntoIterator>, S: ToOffset, T: Into, + { + self.edit_internal(ranges_iter, new_text, false, cx) + } + + pub fn edit_with_autoindent( + &mut self, + ranges_iter: I, + new_text: T, + cx: &mut ModelContext, + ) where + I: IntoIterator>, + S: ToOffset, + T: Into, + { + self.edit_internal(ranges_iter, new_text, true, cx) + } + + pub fn edit_internal( + &mut self, + ranges_iter: I, + new_text: T, + autoindent: bool, + cx: &mut ModelContext, + ) where + I: IntoIterator>, + S: ToOffset, + T: Into, { let new_text = new_text.into(); - let new_text = if new_text.len() > 0 { - Some(new_text) - } else { - None - }; - let has_new_text = new_text.is_some(); // Skip invalid ranges and coalesce contiguous ones. let mut ranges: Vec> = Vec::new(); for range in ranges_iter { let range = range.start.to_offset(&*self)..range.end.to_offset(&*self); - if has_new_text || !range.is_empty() { + if !new_text.is_empty() || !range.is_empty() { if let Some(prev_range) = ranges.last_mut() { if prev_range.end >= range.start { prev_range.end = cmp::max(prev_range.end, range.end); @@ -1549,24 +1478,83 @@ impl Buffer { } } } + if ranges.is_empty() { + return; + } - if !ranges.is_empty() { - self.start_transaction_at(None, Instant::now()).unwrap(); - let timestamp = InsertionTimestamp { - replica_id: self.replica_id, - local: self.local_clock.tick().value, - lamport: self.lamport_clock.tick().value, - }; - let edit = self.apply_local_edit(&ranges, new_text, timestamp); + // When autoindent is enabled, compute the previous indent suggestion for all edited lines. + if autoindent { + if let Some((language, tree)) = self.language.as_ref().zip(self.syntax_tree()) { + let mut cursor = QueryCursorHandle::new(); + let starts_with_newline = new_text.starts_with('\n'); - self.history.push(edit.clone()); - self.history.push_undo(edit.timestamp.local()); - self.last_edit = edit.timestamp.local(); - self.version.observe(edit.timestamp.local()); + let mut autoindent_requests = mem::take(&mut self.autoindent_requests); + let edited_rows = ranges.iter().filter_map(|range| { + let start = range.start.to_point(&*self); + if !starts_with_newline || start.column < self.line_len(start.row) { + Some(start.row) + } else { + None + } + }); + for row_range in contiguous_ranges(edited_rows) { + let suggestions = self + .suggest_autoindents(row_range.clone(), &tree, language, &mut cursor) + .collect::>(); + for (row, suggestion) in row_range.zip(suggestions) { + autoindent_requests.push(AutoindentRequest { + position: self.anchor_before(Point::new(row, 0)), + end_position: None, + prev_suggestion: Some(suggestion), + }) + } + } + self.autoindent_requests = autoindent_requests; + } + } - self.end_transaction_at(None, Instant::now(), cx).unwrap(); - self.send_operation(Operation::Edit(edit), cx); + let new_text = if new_text.len() > 0 { + Some(new_text) + } else { + None }; + + self.start_transaction_at(None, Instant::now()).unwrap(); + let timestamp = InsertionTimestamp { + replica_id: self.replica_id, + local: self.local_clock.tick().value, + lamport: self.lamport_clock.tick().value, + }; + let edit = self.apply_local_edit(&ranges, new_text, timestamp); + + self.history.push(edit.clone()); + self.history.push_undo(edit.timestamp.local()); + self.last_edit = edit.timestamp.local(); + self.version.observe(edit.timestamp.local()); + + if autoindent { + let ranges = edit.ranges.iter().map(|range| { + Anchor { + offset: range.start, + bias: Bias::Left, + version: edit.version.clone(), + }..Anchor { + offset: range.end, + bias: Bias::Right, + version: edit.version.clone(), + } + }); + for range in ranges { + self.autoindent_requests.push(AutoindentRequest { + position: range.start, + end_position: Some(range.end), + prev_suggestion: None, + }) + } + } + + self.end_transaction_at(None, Instant::now(), cx).unwrap(); + self.send_operation(Operation::Edit(edit), cx); } fn did_edit(&self, was_dirty: bool, cx: &mut ModelContext) { @@ -3437,6 +3425,28 @@ impl ToPoint for usize { } } +fn contiguous_ranges(mut values: impl Iterator) -> impl Iterator> { + let mut current_range: Option> = None; + std::iter::from_fn(move || loop { + if let Some(value) = values.next() { + if let Some(range) = &mut current_range { + if value == range.end { + range.end += 1; + continue; + } + } + + let prev_range = current_range.clone(); + current_range = Some(value..(value + 1)); + if prev_range.is_some() { + return prev_range; + } + } else { + return current_range.take(); + } + }) +} + #[cfg(test)] mod tests { use crate::random_char_iter::RandomCharIter; @@ -4329,7 +4339,7 @@ mod tests { } #[gpui::test] - async fn test_request_autoindent(mut cx: gpui::TestAppContext) { + async fn test_edit_with_autoindent(mut cx: gpui::TestAppContext) { let rust_lang = rust_lang(); let buffer = cx.add_model(|cx| { let text = "fn a() {}".into(); @@ -4341,16 +4351,25 @@ mod tests { .await; buffer.update(&mut cx, |buffer, cx| { - buffer.start_transaction(None).unwrap(); - buffer.edit([8..8], "\n\n", cx); - buffer.request_autoindent_for_line(1, 4, true); - assert_eq!(buffer.text(), "fn a() {\n\n}"); - - buffer.end_transaction(None, cx).unwrap(); + buffer.edit_with_autoindent([8..8], "\n\n", cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); + + buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 4)], "b()\n", cx); + assert_eq!(buffer.text(), "fn a() {\n b()\n \n}"); + + buffer.edit_with_autoindent([Point::new(2, 4)..Point::new(2, 4)], ".c", cx); + assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}"); }); } + #[test] + fn test_contiguous_ranges() { + assert_eq!( + contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12].iter().copied()).collect::>(), + &[1..4, 5..7, 9..13] + ); + } + #[derive(Clone)] struct Envelope { message: T, @@ -4439,7 +4458,13 @@ mod tests { }, tree_sitter_rust::language(), ) - .with_indents_query(r#" (_ "{" "}" @end) @indent "#) + .with_indents_query( + r#" + (call_expression) @indent + (field_expression) @indent + (_ "{" "}" @end) @indent + "#, + ) .unwrap() .with_brackets_query(r#" ("{" @open "}" @close) "#) .unwrap(), diff --git a/crates/editor/src/lib.rs b/crates/editor/src/lib.rs index 27cb92afa2..9acb8fc2f7 100644 --- a/crates/editor/src/lib.rs +++ b/crates/editor/src/lib.rs @@ -747,7 +747,6 @@ impl Editor { if !self.skip_autoclose_end(text, cx) { self.start_transaction(cx); self.insert(text, cx); - self.request_autoindent(cx); self.autoclose_pairs(cx); self.end_transaction(cx); } @@ -769,7 +768,7 @@ impl Editor { let mut new_selections = Vec::new(); self.buffer.update(cx, |buffer, cx| { let edit_ranges = old_selections.iter().map(|(_, range)| range.clone()); - buffer.edit(edit_ranges, text, cx); + buffer.edit_with_autoindent(edit_ranges, text, cx); let text_len = text.len() as isize; let mut delta = 0_isize; new_selections = old_selections @@ -795,17 +794,6 @@ impl Editor { self.end_transaction(cx); } - fn request_autoindent(&mut self, cx: &mut ViewContext) { - let selections = self.selections(cx); - let tab_size = self.build_settings.borrow()(cx).tab_size as u8; - self.buffer.update(cx, |buffer, _| { - for selection in selections.iter() { - let row = selection.head().to_point(&*buffer).row; - buffer.request_autoindent_for_line(row, tab_size, true); - } - }); - } - fn autoclose_pairs(&mut self, cx: &mut ViewContext) { let selections = self.selections(cx); let new_autoclose_pair_state = self.buffer.update(cx, |buffer, cx| { From 18e5d75fd3d42e28f5e96296c941a04ec19f59f1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 8 Oct 2021 12:15:21 +0200 Subject: [PATCH 12/28] Bias the start of an autoindent request towards the right for new text When a newline is inserted, this prevents the request from including the line on which the newline got inserted. --- crates/buffer/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index 2d145f52e4..3d4f6bafdd 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -1536,7 +1536,7 @@ impl Buffer { let ranges = edit.ranges.iter().map(|range| { Anchor { offset: range.start, - bias: Bias::Left, + bias: Bias::Right, version: edit.version.clone(), }..Anchor { offset: range.end, From f66b52239ef457ce367fdacefa42fb94dead3d27 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 8 Oct 2021 12:52:48 +0200 Subject: [PATCH 13/28] Cancel out outdents and indents referring to the previous row --- crates/buffer/src/lib.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index 3d4f6bafdd..c250e49447 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -1084,6 +1084,7 @@ impl Buffer { if range.start.row >= row { break; } + if range.start.row == prev_row && range.end > row_start { eprintln!(" indent because of {} {:?}", node_kind, range); indent_from_prev_row = true; @@ -1094,14 +1095,15 @@ impl Buffer { } } - let mut indent_column = prev_indent_column; - if dedent_to_row < row { - if !indent_from_prev_row { - indent_column = self.indent_column_for_line(dedent_to_row); - } + let indent_column = if dedent_to_row == prev_row { + prev_indent_column } else if indent_from_prev_row { - indent_column += INDENT_SIZE; - } + prev_indent_column + INDENT_SIZE + } else if dedent_to_row < prev_row { + self.indent_column_for_line(dedent_to_row) + } else { + prev_indent_column + }; prev_indent_column = indent_column; prev_row = row; From c60bc00c9ef7f38b251fac20ede3f8be7ae95bb7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 8 Oct 2021 17:43:19 +0200 Subject: [PATCH 14/28] :lipstick: Co-Authored-By: Nathan Sobo --- crates/buffer/src/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index c250e49447..f2cf4e2db6 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -1079,7 +1079,7 @@ impl Buffer { ); let mut indent_from_prev_row = false; - let mut dedent_to_row = u32::MAX; + let mut outdent_to_row = u32::MAX; for (range, node_kind) in &indentation_ranges { if range.start.row >= row { break; @@ -1091,16 +1091,16 @@ impl Buffer { } if range.end.row >= prev_row && range.end <= row_start { eprintln!(" outdent because of {} {:?}", node_kind, range); - dedent_to_row = dedent_to_row.min(range.start.row); + outdent_to_row = outdent_to_row.min(range.start.row); } } - let indent_column = if dedent_to_row == prev_row { + let indent_column = if outdent_to_row == prev_row { prev_indent_column } else if indent_from_prev_row { prev_indent_column + INDENT_SIZE - } else if dedent_to_row < prev_row { - self.indent_column_for_line(dedent_to_row) + } else if outdent_to_row < prev_row { + self.indent_column_for_line(outdent_to_row) } else { prev_indent_column }; From b4680144c58fd8bd3b85302390aca606e6e8f918 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 8 Oct 2021 10:25:00 -0600 Subject: [PATCH 15/28] Unconditionally preserve indentation when inserting newlines Co-Authored-By: Antonio Scandurra --- crates/editor/src/lib.rs | 108 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/lib.rs b/crates/editor/src/lib.rs index 9acb8fc2f7..05a7569df7 100644 --- a/crates/editor/src/lib.rs +++ b/crates/editor/src/lib.rs @@ -21,7 +21,7 @@ use smol::Timer; use std::{ cell::RefCell, cmp::{self, Ordering}, - mem, + iter, mem, ops::{Range, RangeInclusive}, rc::Rc, sync::Arc, @@ -38,6 +38,7 @@ action!(Cancel); action!(Backspace); action!(Delete); action!(Input, String); +action!(Newline); action!(Tab); action!(DeleteLine); action!(DeleteToPreviousWordBoundary); @@ -96,7 +97,7 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("ctrl-h", Backspace, Some("Editor")), Binding::new("delete", Delete, Some("Editor")), Binding::new("ctrl-d", Delete, Some("Editor")), - Binding::new("enter", Input("\n".into()), Some("Editor && mode == full")), + Binding::new("enter", Newline, Some("Editor && mode == full")), Binding::new( "alt-enter", Input("\n".into()), @@ -194,6 +195,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::select); cx.add_action(Editor::cancel); cx.add_action(Editor::handle_input); + cx.add_action(Editor::newline); cx.add_action(Editor::backspace); cx.add_action(Editor::delete); cx.add_action(Editor::tab); @@ -752,6 +754,84 @@ impl Editor { } } + pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext) { + self.start_transaction(cx); + let mut old_selections = SmallVec::<[_; 32]>::new(); + { + let selections = self.selections(cx); + let buffer = self.buffer.read(cx); + for selection in selections.iter() { + let start_point = selection.start.to_point(buffer); + let indent = buffer + .indent_column_for_line(start_point.row) + .min(start_point.column); + let start = selection.start.to_offset(buffer); + let end = selection.end.to_offset(buffer); + old_selections.push((selection.id, start..end, indent)); + } + } + + let mut new_selections = Vec::with_capacity(old_selections.len()); + self.buffer.update(cx, |buffer, cx| { + let mut delta = 0_isize; + let mut pending_edit: Option = None; + for (_, range, indent) in &old_selections { + if pending_edit + .as_ref() + .map_or(false, |pending| pending.indent != *indent) + { + let pending = pending_edit.take().unwrap(); + let mut new_text = String::with_capacity(1 + pending.indent as usize); + new_text.push('\n'); + new_text.extend(iter::repeat(' ').take(pending.indent as usize)); + buffer.edit_with_autoindent(pending.ranges, new_text, cx); + delta += pending.delta; + } + + let start = (range.start as isize + delta) as usize; + let end = (range.end as isize + delta) as usize; + let text_len = *indent as usize + 1; + + let pending = pending_edit.get_or_insert_with(Default::default); + pending.delta += text_len as isize - (end - start) as isize; + pending.indent = *indent; + pending.ranges.push(start..end); + } + + let pending = pending_edit.unwrap(); + let mut new_text = String::with_capacity(1 + pending.indent as usize); + new_text.push('\n'); + new_text.extend(iter::repeat(' ').take(pending.indent as usize)); + buffer.edit_with_autoindent(pending.ranges, new_text, cx); + + let mut delta = 0_isize; + new_selections.extend(old_selections.into_iter().map(|(id, range, indent)| { + let start = (range.start as isize + delta) as usize; + let end = (range.end as isize + delta) as usize; + let text_len = indent as usize + 1; + let anchor = buffer.anchor_before(start + text_len); + delta += text_len as isize - (end - start) as isize; + Selection { + id, + start: anchor.clone(), + end: anchor, + reversed: false, + goal: SelectionGoal::None, + } + })) + }); + + self.update_selections(new_selections, true, cx); + self.end_transaction(cx); + + #[derive(Default)] + struct PendingEdit { + indent: u32, + delta: isize, + ranges: SmallVec<[Range; 32]>, + } + } + fn insert(&mut self, text: &str, cx: &mut ViewContext) { self.start_transaction(cx); let mut old_selections = SmallVec::<[_; 32]>::new(); @@ -3554,6 +3634,30 @@ mod tests { assert_eq!(buffer.read(cx).text(), "e t te our"); } + #[gpui::test] + fn test_newline(cx: &mut gpui::MutableAppContext) { + let buffer = cx.add_model(|cx| Buffer::new(0, "aaaa\n bbbb\n", cx)); + let settings = EditorSettings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6), + ], + cx, + ) + .unwrap(); + + view.newline(&Newline, cx); + assert_eq!(view.text(cx), "aa\naa\n \n bb\n bb\n"); + }); + } + #[gpui::test] fn test_backspace(cx: &mut gpui::MutableAppContext) { let buffer = cx.add_model(|cx| { From 810315e04cba09162f53e2b850bc46d27df6a77a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 8 Oct 2021 10:25:16 -0600 Subject: [PATCH 16/28] Don't request auto-indent if there is no language assigned on the buffer Co-Authored-By: Antonio Scandurra --- crates/buffer/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index f2cf4e2db6..897e9bda22 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -1534,7 +1534,7 @@ impl Buffer { self.last_edit = edit.timestamp.local(); self.version.observe(edit.timestamp.local()); - if autoindent { + if autoindent && self.language.is_some() { let ranges = edit.ranges.iter().map(|range| { Anchor { offset: range.start, From b85ae89b7e1515bda26ca46c5db0841e93b2c5f8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 8 Oct 2021 10:32:15 -0600 Subject: [PATCH 17/28] Extend Rust where clauses past their final newline for indentation Co-Authored-By: Antonio Scandurra --- crates/zed/languages/rust/indents.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zed/languages/rust/indents.scm b/crates/zed/languages/rust/indents.scm index 07863846ae..a154dc665b 100644 --- a/crates/zed/languages/rust/indents.scm +++ b/crates/zed/languages/rust/indents.scm @@ -1,5 +1,5 @@ [ - (where_clause) + ((where_clause) _ @end) (field_expression) (call_expression) (assignment_expression) From e78a5642fa76fd7e2378b33aba7a0f4f731d9954 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 8 Oct 2021 16:54:27 -0700 Subject: [PATCH 18/28] Start on new way of comparing old and new indent suggestions Co-Authored-By: Nathan Sobo --- crates/buffer/src/anchor.rs | 70 ++++ crates/buffer/src/lib.rs | 617 +++++++++++++++++++++++------------- crates/buffer/src/rope.rs | 1 + 3 files changed, 472 insertions(+), 216 deletions(-) diff --git a/crates/buffer/src/anchor.rs b/crates/buffer/src/anchor.rs index c678918824..407446e7a2 100644 --- a/crates/buffer/src/anchor.rs +++ b/crates/buffer/src/anchor.rs @@ -1,3 +1,5 @@ +use crate::Point; + use super::{Buffer, Content}; use anyhow::Result; use std::{cmp::Ordering, ops::Range}; @@ -10,6 +12,20 @@ pub struct Anchor { pub version: clock::Global, } +pub struct AnchorMap { + pub(crate) version: clock::Global, + pub(crate) entries: Vec<((usize, Bias), T)>, +} + +pub struct AnchorSet(pub(crate) AnchorMap<()>); + +pub struct AnchorRangeMap { + pub(crate) version: clock::Global, + pub(crate) entries: Vec<(Range<(usize, Bias)>, T)>, +} + +pub struct AnchorRangeSet(pub(crate) AnchorRangeMap<()>); + impl Anchor { pub fn min() -> Self { Self { @@ -62,6 +78,60 @@ impl Anchor { } } +impl AnchorMap { + pub fn to_points<'a>( + &'a self, + content: impl Into> + 'a, + ) -> impl Iterator + 'a { + let content = content.into(); + content + .summaries_for_anchors(self) + .map(move |(sum, value)| (sum.lines, value)) + } + + pub fn version(&self) -> &clock::Global { + &self.version + } +} + +impl AnchorSet { + pub fn to_points<'a>( + &'a self, + content: impl Into> + 'a, + ) -> impl Iterator + 'a { + self.0.to_points(content).map(move |(point, _)| point) + } +} + +impl AnchorRangeMap { + pub fn to_point_ranges<'a>( + &'a self, + content: impl Into> + 'a, + ) -> impl Iterator, &'a T)> + 'a { + let content = content.into(); + content + .summaries_for_anchor_ranges(self) + .map(move |(range, value)| ((range.start.lines..range.end.lines), value)) + } + + pub fn version(&self) -> &clock::Global { + &self.version + } +} + +impl AnchorRangeSet { + pub fn to_point_ranges<'a>( + &'a self, + content: impl Into> + 'a, + ) -> impl Iterator> + 'a { + self.0.to_point_ranges(content).map(|(range, _)| range) + } + + pub fn version(&self) -> &clock::Global { + self.0.version() + } +} + pub trait AnchorRangeExt { fn cmp<'a>(&self, b: &Range, buffer: impl Into>) -> Result; } diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index 897e9bda22..4c40ed98d4 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -30,12 +30,11 @@ use std::{ any::Any, cell::RefCell, cmp, - collections::BTreeMap, + collections::{BTreeMap, VecDeque}, convert::{TryFrom, TryInto}, ffi::OsString, hash::BuildHasher, iter::Iterator, - mem, ops::{Deref, DerefMut, Range}, path::{Path, PathBuf}, str, @@ -167,7 +166,7 @@ pub struct Buffer { history: History, file: Option>, language: Option>, - autoindent_requests: Vec, + autoindent_requests: VecDeque, sync_parse_timeout: Duration, syntax_tree: Mutex>, parsing_in_background: bool, @@ -195,11 +194,16 @@ struct SyntaxTree { version: clock::Global, } -#[derive(Clone, Debug)] struct AutoindentRequest { - position: Anchor, - end_position: Option, - prev_suggestion: Option, + before_edit: Snapshot, + edited: AnchorSet, + inserted: AnchorRangeSet, +} + +impl AutoindentRequest { + fn version_after_edit(&self) -> &clock::Global { + self.inserted.version() + } } #[derive(Clone, Debug)] @@ -967,7 +971,7 @@ impl Buffer { language: Arc, cx: &mut ModelContext, ) { - self.perform_autoindent(&tree, language, cx); + self.perform_autoindent(&tree, &version, language, cx); self.parse_count += 1; *self.syntax_tree.lock() = Some(SyntaxTree { tree, version }); cx.emit(Event::Reparsed); @@ -977,160 +981,91 @@ impl Buffer { fn perform_autoindent( &mut self, new_tree: &Tree, + new_version: &clock::Global, language: Arc, cx: &mut ModelContext, ) { - let mut autoindent_requests = mem::take(&mut self.autoindent_requests); - let mut prev_suggestions_by_row = BTreeMap::>::default(); - for request in autoindent_requests.drain(..) { - let start_row = request.position.to_point(&*self).row; - let end_row = if let Some(end_position) = request.end_position { - end_position.to_point(&*self).row - } else { - start_row - }; - for row in start_row..=end_row { - let prev_suggestion = request.prev_suggestion; - prev_suggestions_by_row - .entry(row) - .and_modify(|suggestion| *suggestion = suggestion.or(prev_suggestion)) - .or_insert(request.prev_suggestion); - } - } - self.autoindent_requests = autoindent_requests; - let mut cursor = QueryCursorHandle::new(); - self.start_transaction(None).unwrap(); - let mut prev_suggestions = prev_suggestions_by_row.iter(); - for row_range in contiguous_ranges(prev_suggestions_by_row.keys().copied()) { - let new_suggestions = self - .suggest_autoindents(row_range.clone(), new_tree, &language, &mut cursor) - .collect::>(); - let old_suggestions = prev_suggestions.by_ref().take(row_range.len()); - for ((row, old_suggestion), new_suggestion) in old_suggestions.zip(new_suggestions) { - if *old_suggestion != Some(new_suggestion) { - self.set_indent_column_for_line(*row, new_suggestion, cx); - } - } - } - self.end_transaction(None, cx).unwrap(); - } - - fn suggest_autoindents<'a>( - &'a self, - row_range: Range, - tree: &Tree, - language: &Language, - cursor: &mut QueryCursor, - ) -> impl Iterator + 'a { - let prev_non_blank_row = self.prev_non_blank_row(row_range.start); - - // Get the "indentation ranges" that intersect this row range. - let indent_capture_ix = language.indents_query.capture_index_for_name("indent"); - let end_capture_ix = language.indents_query.capture_index_for_name("end"); - cursor.set_point_range( - Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0).into() - ..Point::new(row_range.end, 0).into(), - ); - let mut indentation_ranges = Vec::<(Range, &'static str)>::new(); - for mat in cursor.matches( - &language.indents_query, - tree.root_node(), - TextProvider(&self.visible_text), - ) { - let mut node_kind = ""; - let mut start: Option = None; - let mut end: Option = None; - for capture in mat.captures { - if Some(capture.index) == indent_capture_ix { - node_kind = capture.node.kind(); - start.get_or_insert(capture.node.start_position().into()); - end.get_or_insert(capture.node.end_position().into()); - } else if Some(capture.index) == end_capture_ix { - end = Some(capture.node.start_position().into()); - } + while let Some(request) = self.autoindent_requests.front() { + if new_version < request.version_after_edit() { + break; } - if let Some((start, end)) = start.zip(end) { - if start.row == end.row { - continue; - } + let request = self.autoindent_requests.pop_front().unwrap(); - let range = start..end; - match indentation_ranges.binary_search_by_key(&range.start, |r| r.0.start) { - Err(ix) => indentation_ranges.insert(ix, (range, node_kind)), - Ok(ix) => { - let prev_range = &mut indentation_ranges[ix]; - prev_range.0.end = prev_range.0.end.max(range.end); + let old_to_new_rows = request + .edited + .to_points(request.before_edit.content()) + .map(|point| point.row) + .zip( + request + .edited + .to_points(self.content()) + .map(|point| point.row), + ) + .collect::>(); + + let mut old_suggestions = HashMap::default(); + if let Some(old_tree) = &request.before_edit.tree { + let old_edited_ranges = contiguous_ranges(old_to_new_rows.keys().copied()); + for old_edited_range in old_edited_ranges { + let old_content = request.before_edit.content(); + let suggestions = old_content.suggest_autoindents( + old_edited_range.clone(), + old_tree, + &language, + &mut cursor, + ); + for (old_row, suggestion) in old_edited_range.zip(suggestions) { + let indentation_basis = old_to_new_rows + .get(&suggestion.basis_row) + .and_then(|from_row| old_suggestions.get(from_row).copied()) + .unwrap_or_else(|| { + request + .before_edit + .indent_column_for_line(suggestion.basis_row) + }); + let delta = if suggestion.indent { INDENT_SIZE } else { 0 }; + old_suggestions.insert( + *old_to_new_rows.get(&old_row).unwrap(), + indentation_basis + delta, + ); } } } - } - let mut prev_row = prev_non_blank_row.unwrap_or(0); - let mut prev_indent_column = - prev_non_blank_row.map_or(0, |prev_row| self.indent_column_for_line(prev_row)); - row_range.map(move |row| { - let row_start = Point::new(row, self.indent_column_for_line(row)); + // At this point, old_suggestions contains the suggested indentation for all edited lines with respect to the state of the + // buffer before the edit, but keyed by the row for these lines after the edits were applied. - eprintln!( - "autoindent row: {:?}, prev_indent_column: {:?}", - row, prev_indent_column - ); - - let mut indent_from_prev_row = false; - let mut outdent_to_row = u32::MAX; - for (range, node_kind) in &indentation_ranges { - if range.start.row >= row { - break; - } - - if range.start.row == prev_row && range.end > row_start { - eprintln!(" indent because of {} {:?}", node_kind, range); - indent_from_prev_row = true; - } - if range.end.row >= prev_row && range.end <= row_start { - eprintln!(" outdent because of {} {:?}", node_kind, range); - outdent_to_row = outdent_to_row.min(range.start.row); + self.start_transaction(None).unwrap(); + let new_edited_row_ranges = contiguous_ranges(old_to_new_rows.values().copied()); + for new_edited_row_range in new_edited_row_ranges { + let new_content = self.content(); + let suggestions = new_content + .suggest_autoindents( + new_edited_row_range.clone(), + &new_tree, + &language, + &mut cursor, + ) + .collect::>(); + for (new_row, suggestion) in new_edited_row_range.zip(suggestions) { + let delta = if suggestion.indent { INDENT_SIZE } else { 0 }; + let new_indentation = self.indent_column_for_line(suggestion.basis_row) + delta; + if old_suggestions + .get(&new_row) + .map_or(true, |old_indentation| new_indentation != *old_indentation) + { + self.set_indent_column_for_line(new_row, new_indentation, cx); + } } } - - let indent_column = if outdent_to_row == prev_row { - prev_indent_column - } else if indent_from_prev_row { - prev_indent_column + INDENT_SIZE - } else if outdent_to_row < prev_row { - self.indent_column_for_line(outdent_to_row) - } else { - prev_indent_column - }; - - prev_indent_column = indent_column; - prev_row = row; - indent_column - }) - } - - fn prev_non_blank_row(&self, mut row: u32) -> Option { - while row > 0 { - row -= 1; - if !self.is_line_blank(row) { - return Some(row); - } + self.end_transaction(None, cx).unwrap(); } - None } pub fn indent_column_for_line(&self, row: u32) -> u32 { - let mut result = 0; - for c in self.chars_at(Point::new(row, 0)) { - if c == ' ' { - result += 1; - } else { - break; - } - } - result + self.content().indent_column_for_line(row) } fn set_indent_column_for_line(&mut self, row: u32, column: u32, cx: &mut ModelContext) { @@ -1163,11 +1098,6 @@ impl Buffer { } } - fn is_line_blank(&self, row: u32) -> bool { - self.text_for_range(Point::new(row, 0)..Point::new(row, self.line_len(row))) - .all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none()) - } - pub fn range_for_syntax_ancestor(&self, range: Range) -> Option> { if let Some(tree) = self.syntax_tree() { let root = tree.root_node(); @@ -1308,18 +1238,18 @@ impl Buffer { } pub fn text_for_range<'a, T: ToOffset>(&'a self, range: Range) -> Chunks<'a> { - let start = range.start.to_offset(self); - let end = range.end.to_offset(self); - self.visible_text.chunks_in_range(start..end) + self.content().text_for_range(range) } pub fn chars(&self) -> impl Iterator + '_ { self.chars_at(0) } - pub fn chars_at(&self, position: T) -> impl Iterator + '_ { - let offset = position.to_offset(self); - self.visible_text.chars_at(offset) + pub fn chars_at<'a, T: 'a + ToOffset>( + &'a self, + position: T, + ) -> impl Iterator + 'a { + self.content().chars_at(position) } pub fn chars_for_range(&self, range: Range) -> impl Iterator + '_ { @@ -1484,36 +1414,20 @@ impl Buffer { return; } - // When autoindent is enabled, compute the previous indent suggestion for all edited lines. - if autoindent { - if let Some((language, tree)) = self.language.as_ref().zip(self.syntax_tree()) { - let mut cursor = QueryCursorHandle::new(); - let starts_with_newline = new_text.starts_with('\n'); - - let mut autoindent_requests = mem::take(&mut self.autoindent_requests); - let edited_rows = ranges.iter().filter_map(|range| { - let start = range.start.to_point(&*self); - if !starts_with_newline || start.column < self.line_len(start.row) { - Some(start.row) - } else { - None - } - }); - for row_range in contiguous_ranges(edited_rows) { - let suggestions = self - .suggest_autoindents(row_range.clone(), &tree, language, &mut cursor) - .collect::>(); - for (row, suggestion) in row_range.zip(suggestions) { - autoindent_requests.push(AutoindentRequest { - position: self.anchor_before(Point::new(row, 0)), - end_position: None, - prev_suggestion: Some(suggestion), - }) - } + let autoindent_request = if autoindent && self.language.is_some() { + let before_edit = self.snapshot(); + let edited = self.content().anchor_set(ranges.iter().filter_map(|range| { + let start = range.start.to_point(&*self); + if new_text.starts_with('\n') && start.column == self.line_len(start.row) { + None + } else { + Some((range.start, Bias::Left)) } - self.autoindent_requests = autoindent_requests; - } - } + })); + Some((before_edit, edited)) + } else { + None + }; let new_text = if new_text.len() > 0 { Some(new_text) @@ -1534,25 +1448,18 @@ impl Buffer { self.last_edit = edit.timestamp.local(); self.version.observe(edit.timestamp.local()); - if autoindent && self.language.is_some() { - let ranges = edit.ranges.iter().map(|range| { - Anchor { - offset: range.start, - bias: Bias::Right, - version: edit.version.clone(), - }..Anchor { - offset: range.end, - bias: Bias::Right, - version: edit.version.clone(), - } - }); - for range in ranges { - self.autoindent_requests.push(AutoindentRequest { - position: range.start, - end_position: Some(range.end), - prev_suggestion: None, - }) - } + if let Some((before_edit, edited)) = autoindent_request { + let inserted = self.content().anchor_range_set( + edit.ranges + .iter() + .map(|range| (range.start, Bias::Left)..(range.end, Bias::Right)), + ); + + self.autoindent_requests.push_back(AutoindentRequest { + before_edit, + edited, + inserted, + }) } self.end_transaction_at(None, Instant::now(), cx).unwrap(); @@ -2134,20 +2041,20 @@ impl Buffer { let mut new_ropes = RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); - let mut old_fragments = self.fragments.cursor::<(usize, FragmentTextSummary)>(); + let mut old_fragments = self.fragments.cursor::(); let mut new_fragments = old_fragments.slice(&ranges[0].start, Bias::Right, &None); new_ropes.push_tree(new_fragments.summary().text); - let mut fragment_start = old_fragments.start().1.visible; + let mut fragment_start = old_fragments.start().visible; for range in ranges { - let fragment_end = old_fragments.end(&None).1.visible; + let fragment_end = old_fragments.end(&None).visible; // If the current fragment ends before this range, then jump ahead to the first fragment // that extends past the start of this range, reusing any intervening fragments. if fragment_end < range.start { // If the current fragment has been partially consumed, then consume the rest of it // and advance to the next fragment before slicing. - if fragment_start > old_fragments.start().1.visible { + if fragment_start > old_fragments.start().visible { if fragment_end > fragment_start { let mut suffix = old_fragments.item().unwrap().clone(); suffix.len = fragment_end - fragment_start; @@ -2160,10 +2067,10 @@ impl Buffer { let slice = old_fragments.slice(&range.start, Bias::Right, &None); new_ropes.push_tree(slice.summary().text); new_fragments.push_tree(slice, &None); - fragment_start = old_fragments.start().1.visible; + fragment_start = old_fragments.start().visible; } - let full_range_start = range.start + old_fragments.start().1.deleted; + let full_range_start = range.start + old_fragments.start().deleted; // Preserve any portion of the current fragment that precedes this range. if fragment_start < range.start { @@ -2193,7 +2100,7 @@ impl Buffer { // portions as deleted. while fragment_start < range.end { let fragment = old_fragments.item().unwrap(); - let fragment_end = old_fragments.end(&None).1.visible; + let fragment_end = old_fragments.end(&None).visible; let mut intersection = fragment.clone(); let intersection_end = cmp::min(range.end, fragment_end); if fragment.visible { @@ -2211,14 +2118,14 @@ impl Buffer { } } - let full_range_end = range.end + old_fragments.start().1.deleted; + let full_range_end = range.end + old_fragments.start().deleted; edit.ranges.push(full_range_start..full_range_end); } // If the current fragment has been partially consumed, then consume the rest of it // and advance to the next fragment before slicing. - if fragment_start > old_fragments.start().1.visible { - let fragment_end = old_fragments.end(&None).1.visible; + if fragment_start > old_fragments.start().visible { + let fragment_end = old_fragments.end(&None).visible; if fragment_end > fragment_start { let mut suffix = old_fragments.item().unwrap().clone(); suffix.len = fragment_end - fragment_start; @@ -2458,7 +2365,7 @@ impl Clone for Buffer { parsing_in_background: false, sync_parse_timeout: self.sync_parse_timeout, parse_count: self.parse_count, - autoindent_requests: self.autoindent_requests.clone(), + autoindent_requests: Default::default(), deferred_replicas: self.deferred_replicas.clone(), replica_id: self.replica_id, remote_id: self.remote_id.clone(), @@ -2504,6 +2411,10 @@ impl Snapshot { self.content().line_len(row) } + pub fn indent_column_for_line(&self, row: u32) -> u32 { + self.content().indent_column_for_line(row) + } + pub fn text(&self) -> Rope { self.visible_text.clone() } @@ -2644,6 +2555,17 @@ impl<'a> Content<'a> { self.fragments.extent::(&None) } + pub fn chars_at(&self, position: T) -> impl Iterator + 'a { + let offset = position.to_offset(self); + self.visible_text.chars_at(offset) + } + + pub fn text_for_range(&self, range: Range) -> Chunks<'a> { + let start = range.start.to_offset(self); + let end = range.end.to_offset(self); + self.visible_text.chunks_in_range(start..end) + } + fn line_len(&self, row: u32) -> u32 { let row_start_offset = Point::new(row, 0).to_offset(self); let row_end_offset = if row >= self.max_point().row { @@ -2654,6 +2576,33 @@ impl<'a> Content<'a> { (row_end_offset - row_start_offset) as u32 } + pub fn indent_column_for_line(&self, row: u32) -> u32 { + let mut result = 0; + for c in self.chars_at(Point::new(row, 0)) { + if c == ' ' { + result += 1; + } else { + break; + } + } + result + } + + fn prev_non_blank_row(&self, mut row: u32) -> Option { + while row > 0 { + row -= 1; + if !self.is_line_blank(row) { + return Some(row); + } + } + None + } + + fn is_line_blank(&self, row: u32) -> bool { + self.text_for_range(Point::new(row, 0)..Point::new(row, self.line_len(row))) + .all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none()) + } + fn summary_for_anchor(&self, anchor: &Anchor) -> TextSummary { let cx = Some(anchor.version.clone()); let mut cursor = self.fragments.cursor::<(VersionedOffset, usize)>(); @@ -2670,19 +2619,134 @@ impl<'a> Content<'a> { self.visible_text.cursor(range.start).summary(range.end) } + fn summaries_for_anchors( + &self, + map: &'a AnchorMap, + ) -> impl Iterator { + let cx = Some(map.version.clone()); + let mut summary = TextSummary::default(); + let mut rope_cursor = self.visible_text.cursor(0); + let mut cursor = self.fragments.cursor::<(VersionedOffset, usize)>(); + map.entries.iter().map(move |((offset, bias), value)| { + cursor.seek(&VersionedOffset::Offset(*offset), *bias, &cx); + let overshoot = if cursor.item().map_or(false, |fragment| fragment.visible) { + offset - cursor.start().0.offset() + } else { + 0 + }; + summary += rope_cursor.summary(cursor.start().1 + overshoot); + (summary.clone(), value) + }) + } + + fn summaries_for_anchor_ranges( + &self, + map: &'a AnchorRangeMap, + ) -> impl Iterator, &'a T)> { + let cx = Some(map.version.clone()); + let mut summary = TextSummary::default(); + let mut rope_cursor = self.visible_text.cursor(0); + let mut cursor = self.fragments.cursor::<(VersionedOffset, usize)>(); + map.entries.iter().map(move |(range, value)| { + let Range { + start: (start_offset, start_bias), + end: (end_offset, end_bias), + } = range; + + cursor.seek(&VersionedOffset::Offset(*start_offset), *start_bias, &cx); + let overshoot = if cursor.item().map_or(false, |fragment| fragment.visible) { + start_offset - cursor.start().0.offset() + } else { + 0 + }; + summary += rope_cursor.summary(cursor.start().1 + overshoot); + let start_summary = summary.clone(); + + cursor.seek(&VersionedOffset::Offset(*end_offset), *end_bias, &cx); + let overshoot = if cursor.item().map_or(false, |fragment| fragment.visible) { + end_offset - cursor.start().0.offset() + } else { + 0 + }; + summary += rope_cursor.summary(cursor.start().1 + overshoot); + let end_summary = summary.clone(); + + (start_summary..end_summary, value) + }) + } + fn anchor_at(&self, position: T, bias: Bias) -> Anchor { let offset = position.to_offset(self); let max_offset = self.len(); assert!(offset <= max_offset, "offset is out of range"); - let mut cursor = self.fragments.cursor::<(usize, FragmentTextSummary)>(); + let mut cursor = self.fragments.cursor::(); cursor.seek(&offset, bias, &None); Anchor { - offset: offset + cursor.start().1.deleted, + offset: offset + cursor.start().deleted, bias, version: self.version.clone(), } } + pub fn anchor_map(&self, entries: E) -> AnchorMap + where + E: IntoIterator, + { + let version = self.version.clone(); + let mut cursor = self.fragments.cursor::(); + let entries = entries + .into_iter() + .map(|((offset, bias), value)| { + cursor.seek_forward(&offset, bias, &None); + let full_offset = cursor.start().deleted + offset; + ((full_offset, bias), value) + }) + .collect(); + + AnchorMap { version, entries } + } + + pub fn anchor_range_map(&self, entries: E) -> AnchorRangeMap + where + E: IntoIterator, T)>, + { + let version = self.version.clone(); + let mut cursor = self.fragments.cursor::(); + let entries = entries + .into_iter() + .map(|(range, value)| { + let Range { + start: (start_offset, start_bias), + end: (end_offset, end_bias), + } = range; + cursor.seek_forward(&start_offset, start_bias, &None); + let full_start_offset = cursor.start().deleted + start_offset; + cursor.seek_forward(&end_offset, end_bias, &None); + let full_end_offset = cursor.start().deleted + end_offset; + ( + (full_start_offset, start_bias)..(full_end_offset, end_bias), + value, + ) + }) + .collect(); + + AnchorRangeMap { version, entries } + } + + pub fn anchor_set(&self, entries: E) -> AnchorSet + where + E: IntoIterator, + { + AnchorSet(self.anchor_map(entries.into_iter().map(|range| (range, ())))) + } + + pub fn anchor_range_set(&self, entries: E) -> AnchorRangeSet + where + E: IntoIterator>, + { + AnchorRangeSet(self.anchor_range_map(entries.into_iter().map(|range| (range, ())))) + } + fn full_offset_for_anchor(&self, anchor: &Anchor) -> usize { let cx = Some(anchor.version.clone()); let mut cursor = self @@ -2705,6 +2769,117 @@ impl<'a> Content<'a> { Err(anyhow!("offset out of bounds")) } } + + fn suggest_autoindents( + &'a self, + row_range: Range, + tree: &Tree, + language: &Language, + cursor: &mut QueryCursor, + ) -> impl Iterator + 'a { + let prev_non_blank_row = self.prev_non_blank_row(row_range.start); + + // Get the "indentation ranges" that intersect this row range. + let indent_capture_ix = language.indents_query.capture_index_for_name("indent"); + let end_capture_ix = language.indents_query.capture_index_for_name("end"); + cursor.set_point_range( + Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0).into() + ..Point::new(row_range.end, 0).into(), + ); + let mut indentation_ranges = Vec::<(Range, &'static str)>::new(); + for mat in cursor.matches( + &language.indents_query, + tree.root_node(), + TextProvider(&self.visible_text), + ) { + let mut node_kind = ""; + let mut start: Option = None; + let mut end: Option = None; + for capture in mat.captures { + if Some(capture.index) == indent_capture_ix { + node_kind = capture.node.kind(); + start.get_or_insert(capture.node.start_position().into()); + end.get_or_insert(capture.node.end_position().into()); + } else if Some(capture.index) == end_capture_ix { + end = Some(capture.node.start_position().into()); + } + } + + if let Some((start, end)) = start.zip(end) { + if start.row == end.row { + continue; + } + + let range = start..end; + match indentation_ranges.binary_search_by_key(&range.start, |r| r.0.start) { + Err(ix) => indentation_ranges.insert(ix, (range, node_kind)), + Ok(ix) => { + let prev_range = &mut indentation_ranges[ix]; + prev_range.0.end = prev_range.0.end.max(range.end); + } + } + } + } + + eprintln!( + "autoindent {:?}. ranges: {:?}", + row_range, indentation_ranges + ); + + let mut prev_row = prev_non_blank_row.unwrap_or(0); + row_range.map(move |row| { + let row_start = Point::new(row, self.indent_column_for_line(row)); + + eprintln!(" autoindent row: {:?}", row); + + let mut indent_from_prev_row = false; + let mut outdent_to_row = u32::MAX; + for (range, node_kind) in &indentation_ranges { + if range.start.row >= row { + break; + } + + if range.start.row == prev_row && range.end > row_start { + eprintln!(" indent because of {} {:?}", node_kind, range); + indent_from_prev_row = true; + } + if range.end.row >= prev_row && range.end <= row_start { + eprintln!(" outdent because of {} {:?}", node_kind, range); + outdent_to_row = outdent_to_row.min(range.start.row); + } + } + + let suggestion = if outdent_to_row == prev_row { + IndentSuggestion { + basis_row: prev_row, + indent: false, + } + } else if indent_from_prev_row { + IndentSuggestion { + basis_row: prev_row, + indent: true, + } + } else if outdent_to_row < prev_row { + IndentSuggestion { + basis_row: outdent_to_row, + indent: false, + } + } else { + IndentSuggestion { + basis_row: prev_row, + indent: false, + } + }; + + prev_row = row; + suggestion + }) + } +} + +struct IndentSuggestion { + basis_row: u32, + indent: bool, } struct RopeBuilder<'a> { @@ -3050,6 +3225,16 @@ impl<'a> sum_tree::Dimension<'a, FragmentSummary> for usize { } } +impl<'a> sum_tree::SeekTarget<'a, FragmentSummary, FragmentTextSummary> for usize { + fn cmp( + &self, + cursor_location: &FragmentTextSummary, + _: &Option, + ) -> cmp::Ordering { + Ord::cmp(self, &cursor_location.visible) + } +} + #[derive(Copy, Clone, Debug, Eq, PartialEq)] enum VersionedOffset { Offset(usize), diff --git a/crates/buffer/src/rope.rs b/crates/buffer/src/rope.rs index a5f4b905ba..ebe01258dc 100644 --- a/crates/buffer/src/rope.rs +++ b/crates/buffer/src/rope.rs @@ -268,6 +268,7 @@ impl<'a> Cursor<'a> { } } + self.offset = end_offset; summary } From 6531df23681b1bcf8195b118787268fb6461f35f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 8 Oct 2021 22:07:01 -0600 Subject: [PATCH 19/28] Improve auto-indenting of inserted lines Still not working totally correctly with our new approach, but getting closer. --- crates/buffer/src/lib.rs | 65 +++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index 4c40ed98d4..c4409531b1 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -196,13 +196,14 @@ struct SyntaxTree { struct AutoindentRequest { before_edit: Snapshot, + version_after_edit: clock::Global, edited: AnchorSet, - inserted: AnchorRangeSet, + inserted: Option, } impl AutoindentRequest { fn version_after_edit(&self) -> &clock::Global { - self.inserted.version() + &self.version_after_edit } } @@ -1040,8 +1041,8 @@ impl Buffer { self.start_transaction(None).unwrap(); let new_edited_row_ranges = contiguous_ranges(old_to_new_rows.values().copied()); for new_edited_row_range in new_edited_row_ranges { - let new_content = self.content(); - let suggestions = new_content + let suggestions = self + .content() .suggest_autoindents( new_edited_row_range.clone(), &new_tree, @@ -1050,6 +1051,7 @@ impl Buffer { ) .collect::>(); for (new_row, suggestion) in new_edited_row_range.zip(suggestions) { + dbg!(&suggestion); let delta = if suggestion.indent { INDENT_SIZE } else { 0 }; let new_indentation = self.indent_column_for_line(suggestion.basis_row) + delta; if old_suggestions @@ -1060,6 +1062,36 @@ impl Buffer { } } } + + if let Some(inserted) = request.inserted { + let inserted_row_ranges = contiguous_ranges( + inserted + .to_point_ranges(self.content()) + .flat_map(|range| dbg!(range.start.row..range.end.row + 1)), + ) + .collect::>(); + dbg!(&inserted_row_ranges); + for inserted_row_range in inserted_row_ranges { + let suggestions = self + .content() + .suggest_autoindents( + inserted_row_range.clone(), + &new_tree, + &language, + &mut cursor, + ) + .collect::>(); + + for (row, suggestion) in inserted_row_range.zip(suggestions) { + dbg!(&suggestion); + let delta = if suggestion.indent { INDENT_SIZE } else { 0 }; + let new_indentation = + self.indent_column_for_line(suggestion.basis_row) + delta; + self.set_indent_column_for_line(row, new_indentation, cx); + } + } + } + self.end_transaction(None, cx).unwrap(); } } @@ -1429,7 +1461,9 @@ impl Buffer { None }; - let new_text = if new_text.len() > 0 { + let first_newline_ix = new_text.find('\n'); + let new_text_len = new_text.len(); + let new_text = if new_text_len > 0 { Some(new_text) } else { None @@ -1449,17 +1483,25 @@ impl Buffer { self.version.observe(edit.timestamp.local()); if let Some((before_edit, edited)) = autoindent_request { - let inserted = self.content().anchor_range_set( - edit.ranges - .iter() - .map(|range| (range.start, Bias::Left)..(range.end, Bias::Right)), - ); + let mut inserted = None; + if let Some(first_newline_ix) = first_newline_ix { + inserted = Some( + self.content() + .anchor_range_set(edit.ranges.iter().map(|range| { + dbg!( + (range.start + first_newline_ix + 1, Bias::Left) + ..(range.start + new_text_len, Bias::Right) + ) + })), + ); + } self.autoindent_requests.push_back(AutoindentRequest { before_edit, + version_after_edit: self.version.clone(), edited, inserted, - }) + }); } self.end_transaction_at(None, Instant::now(), cx).unwrap(); @@ -2877,6 +2919,7 @@ impl<'a> Content<'a> { } } +#[derive(Debug)] struct IndentSuggestion { basis_row: u32, indent: bool, From b43c78053bf57f9ceaba1cf5e4e916c9a964293a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 11 Oct 2021 09:37:21 +0200 Subject: [PATCH 20/28] Always use `seek_forward` when resolving anchors --- crates/buffer/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index c4409531b1..182eabcc12 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -2670,7 +2670,7 @@ impl<'a> Content<'a> { let mut rope_cursor = self.visible_text.cursor(0); let mut cursor = self.fragments.cursor::<(VersionedOffset, usize)>(); map.entries.iter().map(move |((offset, bias), value)| { - cursor.seek(&VersionedOffset::Offset(*offset), *bias, &cx); + cursor.seek_forward(&VersionedOffset::Offset(*offset), *bias, &cx); let overshoot = if cursor.item().map_or(false, |fragment| fragment.visible) { offset - cursor.start().0.offset() } else { @@ -2695,7 +2695,7 @@ impl<'a> Content<'a> { end: (end_offset, end_bias), } = range; - cursor.seek(&VersionedOffset::Offset(*start_offset), *start_bias, &cx); + cursor.seek_forward(&VersionedOffset::Offset(*start_offset), *start_bias, &cx); let overshoot = if cursor.item().map_or(false, |fragment| fragment.visible) { start_offset - cursor.start().0.offset() } else { @@ -2704,7 +2704,7 @@ impl<'a> Content<'a> { summary += rope_cursor.summary(cursor.start().1 + overshoot); let start_summary = summary.clone(); - cursor.seek(&VersionedOffset::Offset(*end_offset), *end_bias, &cx); + cursor.seek_forward(&VersionedOffset::Offset(*end_offset), *end_bias, &cx); let overshoot = if cursor.item().map_or(false, |fragment| fragment.visible) { end_offset - cursor.start().0.offset() } else { From 7f5d454b2d3c9c864e90d4378bd4e546a8dc0c27 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 11 Oct 2021 13:08:50 +0200 Subject: [PATCH 21/28] Fix creation of anchors for the inserted portion of an edit --- crates/buffer/src/lib.rs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index 182eabcc12..64a1a944fd 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -1051,7 +1051,6 @@ impl Buffer { ) .collect::>(); for (new_row, suggestion) in new_edited_row_range.zip(suggestions) { - dbg!(&suggestion); let delta = if suggestion.indent { INDENT_SIZE } else { 0 }; let new_indentation = self.indent_column_for_line(suggestion.basis_row) + delta; if old_suggestions @@ -1067,10 +1066,9 @@ impl Buffer { let inserted_row_ranges = contiguous_ranges( inserted .to_point_ranges(self.content()) - .flat_map(|range| dbg!(range.start.row..range.end.row + 1)), + .flat_map(|range| range.start.row..range.end.row + 1), ) .collect::>(); - dbg!(&inserted_row_ranges); for inserted_row_range in inserted_row_ranges { let suggestions = self .content() @@ -1083,7 +1081,6 @@ impl Buffer { .collect::>(); for (row, suggestion) in inserted_row_range.zip(suggestions) { - dbg!(&suggestion); let delta = if suggestion.indent { INDENT_SIZE } else { 0 }; let new_indentation = self.indent_column_for_line(suggestion.basis_row) + delta; @@ -1485,15 +1482,10 @@ impl Buffer { if let Some((before_edit, edited)) = autoindent_request { let mut inserted = None; if let Some(first_newline_ix) = first_newline_ix { - inserted = Some( - self.content() - .anchor_range_set(edit.ranges.iter().map(|range| { - dbg!( - (range.start + first_newline_ix + 1, Bias::Left) - ..(range.start + new_text_len, Bias::Right) - ) - })), - ); + inserted = Some(self.content().anchor_range_set(ranges.iter().map(|range| { + (range.start + first_newline_ix + 1, Bias::Left) + ..(range.start + new_text_len, Bias::Right) + }))); } self.autoindent_requests.push_back(AutoindentRequest { From 28ffd750ce700a795ba6cd4dda6acca2fc462b64 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 11 Oct 2021 18:00:48 +0200 Subject: [PATCH 22/28] WIP: Determine autoindents asynchronously We still need to insert yield points in `compute_autoindents`. Co-Authored-By: Nathan Sobo --- crates/buffer/src/anchor.rs | 4 + crates/buffer/src/lib.rs | 476 +++++++++++++++++++----------------- 2 files changed, 252 insertions(+), 228 deletions(-) diff --git a/crates/buffer/src/anchor.rs b/crates/buffer/src/anchor.rs index 407446e7a2..1ac82727df 100644 --- a/crates/buffer/src/anchor.rs +++ b/crates/buffer/src/anchor.rs @@ -12,18 +12,22 @@ pub struct Anchor { pub version: clock::Global, } +#[derive(Clone)] pub struct AnchorMap { pub(crate) version: clock::Global, pub(crate) entries: Vec<((usize, Bias), T)>, } +#[derive(Clone)] pub struct AnchorSet(pub(crate) AnchorMap<()>); +#[derive(Clone)] pub struct AnchorRangeMap { pub(crate) version: clock::Global, pub(crate) entries: Vec<(Range<(usize, Bias)>, T)>, } +#[derive(Clone)] pub struct AnchorRangeSet(pub(crate) AnchorRangeMap<()>); impl Anchor { diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index 64a1a944fd..1bffc3ed82 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -30,9 +30,10 @@ use std::{ any::Any, cell::RefCell, cmp, - collections::{BTreeMap, VecDeque}, + collections::BTreeMap, convert::{TryFrom, TryInto}, ffi::OsString, + future::Future, hash::BuildHasher, iter::Iterator, ops::{Deref, DerefMut, Range}, @@ -166,7 +167,8 @@ pub struct Buffer { history: History, file: Option>, language: Option>, - autoindent_requests: VecDeque, + autoindent_requests: Vec>, + pending_autoindent: Option>, sync_parse_timeout: Duration, syntax_tree: Mutex>, parsing_in_background: bool, @@ -194,19 +196,13 @@ struct SyntaxTree { version: clock::Global, } +#[derive(Clone)] struct AutoindentRequest { before_edit: Snapshot, - version_after_edit: clock::Global, edited: AnchorSet, inserted: Option, } -impl AutoindentRequest { - fn version_after_edit(&self) -> &clock::Global { - &self.version_after_edit - } -} - #[derive(Clone, Debug)] struct Transaction { start: clock::Global, @@ -651,6 +647,7 @@ impl Buffer { parse_count: 0, sync_parse_timeout: Duration::from_millis(1), autoindent_requests: Default::default(), + pending_autoindent: Default::default(), language, saved_mtime, selections: HashMap::default(), @@ -896,7 +893,7 @@ impl Buffer { .block_with_timeout(self.sync_parse_timeout, parse_task) { Ok(new_tree) => { - self.did_finish_parsing(new_tree, parsed_version, language, cx); + self.did_finish_parsing(new_tree, parsed_version, cx); return true; } Err(parse_task) => { @@ -910,7 +907,7 @@ impl Buffer { }); let parse_again = this.version > parsed_version || language_changed; this.parsing_in_background = false; - this.did_finish_parsing(new_tree, parsed_version, language, cx); + this.did_finish_parsing(new_tree, parsed_version, cx); if parse_again && this.reparse(cx) { return; @@ -969,54 +966,77 @@ impl Buffer { &mut self, tree: Tree, version: clock::Global, - language: Arc, cx: &mut ModelContext, ) { - self.perform_autoindent(&tree, &version, language, cx); self.parse_count += 1; *self.syntax_tree.lock() = Some(SyntaxTree { tree, version }); + self.request_autoindent(cx); cx.emit(Event::Reparsed); cx.notify(); } - fn perform_autoindent( - &mut self, - new_tree: &Tree, - new_version: &clock::Global, - language: Arc, - cx: &mut ModelContext, - ) { - let mut cursor = QueryCursorHandle::new(); - while let Some(request) = self.autoindent_requests.front() { - if new_version < request.version_after_edit() { - break; + fn request_autoindent(&mut self, cx: &mut ModelContext) { + if let Some(indent_columns) = self.compute_autoindents() { + let indent_columns = cx.background().spawn(indent_columns); + match cx + .background() + .block_with_timeout(Duration::from_micros(500), indent_columns) + { + Ok(indent_columns) => { + log::info!("finished synchronously {:?}", indent_columns); + self.autoindent_requests.clear(); + self.start_transaction(None).unwrap(); + for (row, indent_column) in indent_columns { + self.set_indent_column_for_line(row, indent_column, cx); + } + self.end_transaction(None, cx).unwrap(); + } + Err(indent_columns) => { + self.pending_autoindent = Some(cx.spawn(|this, mut cx| async move { + let indent_columns = indent_columns.await; + log::info!("finished ASYNC, {:?}", indent_columns); + this.update(&mut cx, |this, cx| { + this.autoindent_requests.clear(); + this.start_transaction(None).unwrap(); + for (row, indent_column) in indent_columns { + this.set_indent_column_for_line(row, indent_column, cx); + } + this.end_transaction(None, cx).unwrap(); + }); + })); + } } + } + } - let request = self.autoindent_requests.pop_front().unwrap(); + fn compute_autoindents(&self) -> Option>> { + let snapshot = self.snapshot(); + if snapshot.language.is_none() + || snapshot.tree.is_none() + || self.autoindent_requests.is_empty() + { + return None; + } - let old_to_new_rows = request - .edited - .to_points(request.before_edit.content()) - .map(|point| point.row) - .zip( - request - .edited - .to_points(self.content()) - .map(|point| point.row), - ) - .collect::>(); + let autoindent_requests = self.autoindent_requests.clone(); + Some(async move { + let mut indent_columns = BTreeMap::new(); + for request in autoindent_requests { + let old_to_new_rows = request + .edited + .to_points(&request.before_edit) + .map(|point| point.row) + .zip(request.edited.to_points(&snapshot).map(|point| point.row)) + .collect::>(); - let mut old_suggestions = HashMap::default(); - if let Some(old_tree) = &request.before_edit.tree { + let mut old_suggestions = HashMap::default(); let old_edited_ranges = contiguous_ranges(old_to_new_rows.keys().copied()); for old_edited_range in old_edited_ranges { - let old_content = request.before_edit.content(); - let suggestions = old_content.suggest_autoindents( - old_edited_range.clone(), - old_tree, - &language, - &mut cursor, - ); + let suggestions = request + .before_edit + .suggest_autoindents(old_edited_range.clone()) + .into_iter() + .flatten(); for (old_row, suggestion) in old_edited_range.zip(suggestions) { let indentation_basis = old_to_new_rows .get(&suggestion.basis_row) @@ -1033,64 +1053,60 @@ impl Buffer { ); } } - } - // At this point, old_suggestions contains the suggested indentation for all edited lines with respect to the state of the - // buffer before the edit, but keyed by the row for these lines after the edits were applied. - - self.start_transaction(None).unwrap(); - let new_edited_row_ranges = contiguous_ranges(old_to_new_rows.values().copied()); - for new_edited_row_range in new_edited_row_ranges { - let suggestions = self - .content() - .suggest_autoindents( - new_edited_row_range.clone(), - &new_tree, - &language, - &mut cursor, - ) - .collect::>(); - for (new_row, suggestion) in new_edited_row_range.zip(suggestions) { - let delta = if suggestion.indent { INDENT_SIZE } else { 0 }; - let new_indentation = self.indent_column_for_line(suggestion.basis_row) + delta; - if old_suggestions - .get(&new_row) - .map_or(true, |old_indentation| new_indentation != *old_indentation) - { - self.set_indent_column_for_line(new_row, new_indentation, cx); - } - } - } - - if let Some(inserted) = request.inserted { - let inserted_row_ranges = contiguous_ranges( - inserted - .to_point_ranges(self.content()) - .flat_map(|range| range.start.row..range.end.row + 1), - ) - .collect::>(); - for inserted_row_range in inserted_row_ranges { - let suggestions = self - .content() - .suggest_autoindents( - inserted_row_range.clone(), - &new_tree, - &language, - &mut cursor, - ) - .collect::>(); - - for (row, suggestion) in inserted_row_range.zip(suggestions) { + // At this point, old_suggestions contains the suggested indentation for all edited lines with respect to the state of the + // buffer before the edit, but keyed by the row for these lines after the edits were applied. + let new_edited_row_ranges = contiguous_ranges(old_to_new_rows.values().copied()); + for new_edited_row_range in new_edited_row_ranges { + let suggestions = snapshot + .suggest_autoindents(new_edited_row_range.clone()) + .into_iter() + .flatten(); + for (new_row, suggestion) in new_edited_row_range.zip(suggestions) { let delta = if suggestion.indent { INDENT_SIZE } else { 0 }; - let new_indentation = - self.indent_column_for_line(suggestion.basis_row) + delta; - self.set_indent_column_for_line(row, new_indentation, cx); + let new_indentation = indent_columns + .get(&suggestion.basis_row) + .copied() + .unwrap_or_else(|| { + snapshot.indent_column_for_line(suggestion.basis_row) + }) + + delta; + if old_suggestions + .get(&new_row) + .map_or(true, |old_indentation| new_indentation != *old_indentation) + { + indent_columns.insert(new_row, new_indentation); + } + } + } + + if let Some(inserted) = request.inserted.as_ref() { + let inserted_row_ranges = contiguous_ranges( + inserted + .to_point_ranges(&snapshot) + .flat_map(|range| range.start.row..range.end.row + 1), + ); + for inserted_row_range in inserted_row_ranges { + let suggestions = snapshot + .suggest_autoindents(inserted_row_range.clone()) + .into_iter() + .flatten(); + for (row, suggestion) in inserted_row_range.zip(suggestions) { + let delta = if suggestion.indent { INDENT_SIZE } else { 0 }; + let new_indentation = indent_columns + .get(&suggestion.basis_row) + .copied() + .unwrap_or_else(|| { + snapshot.indent_column_for_line(suggestion.basis_row) + }) + + delta; + indent_columns.insert(row, new_indentation); + } } } } - - self.end_transaction(None, cx).unwrap(); - } + indent_columns + }) } pub fn indent_column_for_line(&self, row: u32) -> u32 { @@ -1443,6 +1459,7 @@ impl Buffer { return; } + self.pending_autoindent.take(); let autoindent_request = if autoindent && self.language.is_some() { let before_edit = self.snapshot(); let edited = self.content().anchor_set(ranges.iter().filter_map(|range| { @@ -1466,7 +1483,7 @@ impl Buffer { None }; - self.start_transaction_at(None, Instant::now()).unwrap(); + self.start_transaction(None).unwrap(); let timestamp = InsertionTimestamp { replica_id: self.replica_id, local: self.local_clock.tick().value, @@ -1482,21 +1499,23 @@ impl Buffer { if let Some((before_edit, edited)) = autoindent_request { let mut inserted = None; if let Some(first_newline_ix) = first_newline_ix { + let mut delta = 0isize; inserted = Some(self.content().anchor_range_set(ranges.iter().map(|range| { - (range.start + first_newline_ix + 1, Bias::Left) - ..(range.start + new_text_len, Bias::Right) + let start = (delta + range.start as isize) as usize + first_newline_ix + 1; + let end = (delta + range.start as isize) as usize + new_text_len; + delta += (range.end as isize - range.start as isize) + new_text_len as isize; + (start, Bias::Left)..(end, Bias::Right) }))); } - self.autoindent_requests.push_back(AutoindentRequest { + self.autoindent_requests.push(Arc::new(AutoindentRequest { before_edit, - version_after_edit: self.version.clone(), edited, inserted, - }); + })); } - self.end_transaction_at(None, Instant::now(), cx).unwrap(); + self.end_transaction(None, cx).unwrap(); self.send_operation(Operation::Edit(edit), cx); } @@ -1626,6 +1645,8 @@ impl Buffer { ops: I, cx: &mut ModelContext, ) -> Result<()> { + self.pending_autoindent.take(); + let was_dirty = self.is_dirty(); let old_version = self.version.clone(); @@ -2400,6 +2421,7 @@ impl Clone for Buffer { sync_parse_timeout: self.sync_parse_timeout, parse_count: self.parse_count, autoindent_requests: Default::default(), + pending_autoindent: Default::default(), deferred_replicas: self.deferred_replicas.clone(), replica_id: self.replica_id, remote_id: self.remote_id.clone(), @@ -2449,6 +2471,120 @@ impl Snapshot { self.content().indent_column_for_line(row) } + fn suggest_autoindents<'a>( + &'a self, + row_range: Range, + ) -> Option + 'a> { + let mut query_cursor = QueryCursorHandle::new(); + if let Some((language, tree)) = self.language.as_ref().zip(self.tree.as_ref()) { + let prev_non_blank_row = self.prev_non_blank_row(row_range.start); + + // Get the "indentation ranges" that intersect this row range. + let indent_capture_ix = language.indents_query.capture_index_for_name("indent"); + let end_capture_ix = language.indents_query.capture_index_for_name("end"); + query_cursor.set_point_range( + Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0).into() + ..Point::new(row_range.end, 0).into(), + ); + let mut indentation_ranges = Vec::<(Range, &'static str)>::new(); + for mat in query_cursor.matches( + &language.indents_query, + tree.root_node(), + TextProvider(&self.visible_text), + ) { + let mut node_kind = ""; + let mut start: Option = None; + let mut end: Option = None; + for capture in mat.captures { + if Some(capture.index) == indent_capture_ix { + node_kind = capture.node.kind(); + start.get_or_insert(capture.node.start_position().into()); + end.get_or_insert(capture.node.end_position().into()); + } else if Some(capture.index) == end_capture_ix { + end = Some(capture.node.start_position().into()); + } + } + + if let Some((start, end)) = start.zip(end) { + if start.row == end.row { + continue; + } + + let range = start..end; + match indentation_ranges.binary_search_by_key(&range.start, |r| r.0.start) { + Err(ix) => indentation_ranges.insert(ix, (range, node_kind)), + Ok(ix) => { + let prev_range = &mut indentation_ranges[ix]; + prev_range.0.end = prev_range.0.end.max(range.end); + } + } + } + } + + let mut prev_row = prev_non_blank_row.unwrap_or(0); + Some(row_range.map(move |row| { + let row_start = Point::new(row, self.indent_column_for_line(row)); + + let mut indent_from_prev_row = false; + let mut outdent_to_row = u32::MAX; + for (range, _node_kind) in &indentation_ranges { + if range.start.row >= row { + break; + } + + if range.start.row == prev_row && range.end > row_start { + indent_from_prev_row = true; + } + if range.end.row >= prev_row && range.end <= row_start { + outdent_to_row = outdent_to_row.min(range.start.row); + } + } + + let suggestion = if outdent_to_row == prev_row { + IndentSuggestion { + basis_row: prev_row, + indent: false, + } + } else if indent_from_prev_row { + IndentSuggestion { + basis_row: prev_row, + indent: true, + } + } else if outdent_to_row < prev_row { + IndentSuggestion { + basis_row: outdent_to_row, + indent: false, + } + } else { + IndentSuggestion { + basis_row: prev_row, + indent: false, + } + }; + + prev_row = row; + suggestion + })) + } else { + None + } + } + + fn prev_non_blank_row(&self, mut row: u32) -> Option { + while row > 0 { + row -= 1; + if !self.is_line_blank(row) { + return Some(row); + } + } + None + } + + fn is_line_blank(&self, row: u32) -> bool { + self.text_for_range(Point::new(row, 0)..Point::new(row, self.line_len(row))) + .all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none()) + } + pub fn text(&self) -> Rope { self.visible_text.clone() } @@ -2461,11 +2597,16 @@ impl Snapshot { self.visible_text.max_point() } - pub fn text_for_range(&self, range: Range) -> Chunks { + pub fn text_for_range(&self, range: Range) -> Chunks { + let range = range.start.to_offset(self)..range.end.to_offset(self); self.visible_text.chunks_in_range(range) } - pub fn highlighted_text_for_range(&mut self, range: Range) -> HighlightedChunks { + pub fn highlighted_text_for_range( + &mut self, + range: Range, + ) -> HighlightedChunks { + let range = range.start.to_offset(&*self)..range.end.to_offset(&*self); let chunks = self.visible_text.chunks_in_range(range.clone()); if let Some((language, tree)) = self.language.as_ref().zip(self.tree.as_ref()) { let captures = self.query_cursor.set_byte_range(range.clone()).captures( @@ -2622,21 +2763,6 @@ impl<'a> Content<'a> { result } - fn prev_non_blank_row(&self, mut row: u32) -> Option { - while row > 0 { - row -= 1; - if !self.is_line_blank(row) { - return Some(row); - } - } - None - } - - fn is_line_blank(&self, row: u32) -> bool { - self.text_for_range(Point::new(row, 0)..Point::new(row, self.line_len(row))) - .all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none()) - } - fn summary_for_anchor(&self, anchor: &Anchor) -> TextSummary { let cx = Some(anchor.version.clone()); let mut cursor = self.fragments.cursor::<(VersionedOffset, usize)>(); @@ -2803,112 +2929,6 @@ impl<'a> Content<'a> { Err(anyhow!("offset out of bounds")) } } - - fn suggest_autoindents( - &'a self, - row_range: Range, - tree: &Tree, - language: &Language, - cursor: &mut QueryCursor, - ) -> impl Iterator + 'a { - let prev_non_blank_row = self.prev_non_blank_row(row_range.start); - - // Get the "indentation ranges" that intersect this row range. - let indent_capture_ix = language.indents_query.capture_index_for_name("indent"); - let end_capture_ix = language.indents_query.capture_index_for_name("end"); - cursor.set_point_range( - Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0).into() - ..Point::new(row_range.end, 0).into(), - ); - let mut indentation_ranges = Vec::<(Range, &'static str)>::new(); - for mat in cursor.matches( - &language.indents_query, - tree.root_node(), - TextProvider(&self.visible_text), - ) { - let mut node_kind = ""; - let mut start: Option = None; - let mut end: Option = None; - for capture in mat.captures { - if Some(capture.index) == indent_capture_ix { - node_kind = capture.node.kind(); - start.get_or_insert(capture.node.start_position().into()); - end.get_or_insert(capture.node.end_position().into()); - } else if Some(capture.index) == end_capture_ix { - end = Some(capture.node.start_position().into()); - } - } - - if let Some((start, end)) = start.zip(end) { - if start.row == end.row { - continue; - } - - let range = start..end; - match indentation_ranges.binary_search_by_key(&range.start, |r| r.0.start) { - Err(ix) => indentation_ranges.insert(ix, (range, node_kind)), - Ok(ix) => { - let prev_range = &mut indentation_ranges[ix]; - prev_range.0.end = prev_range.0.end.max(range.end); - } - } - } - } - - eprintln!( - "autoindent {:?}. ranges: {:?}", - row_range, indentation_ranges - ); - - let mut prev_row = prev_non_blank_row.unwrap_or(0); - row_range.map(move |row| { - let row_start = Point::new(row, self.indent_column_for_line(row)); - - eprintln!(" autoindent row: {:?}", row); - - let mut indent_from_prev_row = false; - let mut outdent_to_row = u32::MAX; - for (range, node_kind) in &indentation_ranges { - if range.start.row >= row { - break; - } - - if range.start.row == prev_row && range.end > row_start { - eprintln!(" indent because of {} {:?}", node_kind, range); - indent_from_prev_row = true; - } - if range.end.row >= prev_row && range.end <= row_start { - eprintln!(" outdent because of {} {:?}", node_kind, range); - outdent_to_row = outdent_to_row.min(range.start.row); - } - } - - let suggestion = if outdent_to_row == prev_row { - IndentSuggestion { - basis_row: prev_row, - indent: false, - } - } else if indent_from_prev_row { - IndentSuggestion { - basis_row: prev_row, - indent: true, - } - } else if outdent_to_row < prev_row { - IndentSuggestion { - basis_row: outdent_to_row, - indent: false, - } - } else { - IndentSuggestion { - basis_row: prev_row, - indent: false, - } - }; - - prev_row = row; - suggestion - }) - } } #[derive(Debug)] From 53f107811919c3471170282332b339968846b5e0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 11 Oct 2021 11:25:15 -0700 Subject: [PATCH 23/28] Ensure that autoindent task yields frequently to respect cancellation Co-Authored-By: Nathan Sobo --- Cargo.lock | 1 + crates/buffer/Cargo.toml | 1 + crates/buffer/src/lib.rs | 29 ++++++++++++++++++++++++----- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d751e25284..0cee1f91d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -759,6 +759,7 @@ dependencies = [ "serde 1.0.125", "similar", "smallvec", + "smol", "sum_tree", "theme", "tree-sitter", diff --git a/crates/buffer/Cargo.toml b/crates/buffer/Cargo.toml index 541c449d46..0cb283aa46 100644 --- a/crates/buffer/Cargo.toml +++ b/crates/buffer/Cargo.toml @@ -22,6 +22,7 @@ seahash = "4.1" serde = { version = "1", features = ["derive"] } similar = "1.3" smallvec = { version = "1.6", features = ["union"] } +smol = "1.2" tree-sitter = "0.19.5" [dev-dependencies] diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index 1bffc3ed82..b565bcaac8 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -26,6 +26,7 @@ use rpc::proto; use seahash::SeaHasher; pub use selection::*; use similar::{ChangeTag, TextDiff}; +use smol::future::yield_now; use std::{ any::Any, cell::RefCell, @@ -1010,6 +1011,7 @@ impl Buffer { } fn compute_autoindents(&self) -> Option>> { + let max_rows_between_yields = 100; let snapshot = self.snapshot(); if snapshot.language.is_none() || snapshot.tree.is_none() @@ -1030,7 +1032,8 @@ impl Buffer { .collect::>(); let mut old_suggestions = HashMap::default(); - let old_edited_ranges = contiguous_ranges(old_to_new_rows.keys().copied()); + let old_edited_ranges = + contiguous_ranges(old_to_new_rows.keys().copied(), max_rows_between_yields); for old_edited_range in old_edited_ranges { let suggestions = request .before_edit @@ -1052,11 +1055,13 @@ impl Buffer { indentation_basis + delta, ); } + yield_now().await; } // At this point, old_suggestions contains the suggested indentation for all edited lines with respect to the state of the // buffer before the edit, but keyed by the row for these lines after the edits were applied. - let new_edited_row_ranges = contiguous_ranges(old_to_new_rows.values().copied()); + let new_edited_row_ranges = + contiguous_ranges(old_to_new_rows.values().copied(), max_rows_between_yields); for new_edited_row_range in new_edited_row_ranges { let suggestions = snapshot .suggest_autoindents(new_edited_row_range.clone()) @@ -1078,6 +1083,7 @@ impl Buffer { indent_columns.insert(new_row, new_indentation); } } + yield_now().await; } if let Some(inserted) = request.inserted.as_ref() { @@ -1085,6 +1091,7 @@ impl Buffer { inserted .to_point_ranges(&snapshot) .flat_map(|range| range.start.row..range.end.row + 1), + max_rows_between_yields, ); for inserted_row_range in inserted_row_ranges { let suggestions = snapshot @@ -1102,6 +1109,7 @@ impl Buffer { + delta; indent_columns.insert(row, new_indentation); } + yield_now().await; } } } @@ -3667,12 +3675,16 @@ impl ToPoint for usize { } } -fn contiguous_ranges(mut values: impl Iterator) -> impl Iterator> { +fn contiguous_ranges( + values: impl IntoIterator, + max_len: usize, +) -> impl Iterator> { + let mut values = values.into_iter(); let mut current_range: Option> = None; std::iter::from_fn(move || loop { if let Some(value) = values.next() { if let Some(range) = &mut current_range { - if value == range.end { + if value == range.end && range.len() < max_len { range.end += 1; continue; } @@ -4607,9 +4619,16 @@ mod tests { #[test] fn test_contiguous_ranges() { assert_eq!( - contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12].iter().copied()).collect::>(), + contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12], 100).collect::>(), &[1..4, 5..7, 9..13] ); + + // Respects the `max_len` parameter + assert_eq!( + contiguous_ranges([2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31], 3) + .collect::>(), + &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32], + ); } #[derive(Clone)] From 6dc9d3ac19096099e760ae27601857069e295899 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 11 Oct 2021 12:18:31 -0700 Subject: [PATCH 24/28] Move buffer tests into their own file --- crates/buffer/src/lib.rs | 1034 +---------------------------- crates/buffer/src/tests.rs | 2 + crates/buffer/src/tests/buffer.rs | 790 ++++++++++++++++++++++ crates/buffer/src/tests/syntax.rs | 237 +++++++ 4 files changed, 1031 insertions(+), 1032 deletions(-) create mode 100644 crates/buffer/src/tests.rs create mode 100644 crates/buffer/src/tests/buffer.rs create mode 100644 crates/buffer/src/tests/syntax.rs diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index b565bcaac8..3937f39920 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -7,6 +7,8 @@ mod point; pub mod random_char_iter; pub mod rope; mod selection; +#[cfg(test)] +mod tests; pub use anchor::*; use anyhow::{anyhow, Result}; @@ -3700,1035 +3702,3 @@ fn contiguous_ranges( } }) } - -#[cfg(test)] -mod tests { - use crate::random_char_iter::RandomCharIter; - - use super::*; - use gpui::ModelHandle; - use rand::prelude::*; - use std::{cell::RefCell, cmp::Ordering, env, mem, rc::Rc}; - - #[gpui::test] - fn test_edit(cx: &mut gpui::MutableAppContext) { - cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "abc", cx); - assert_eq!(buffer.text(), "abc"); - buffer.edit(vec![3..3], "def", cx); - assert_eq!(buffer.text(), "abcdef"); - buffer.edit(vec![0..0], "ghi", cx); - assert_eq!(buffer.text(), "ghiabcdef"); - buffer.edit(vec![5..5], "jkl", cx); - assert_eq!(buffer.text(), "ghiabjklcdef"); - buffer.edit(vec![6..7], "", cx); - assert_eq!(buffer.text(), "ghiabjlcdef"); - buffer.edit(vec![4..9], "mno", cx); - assert_eq!(buffer.text(), "ghiamnoef"); - buffer - }); - } - - #[gpui::test] - fn test_edit_events(cx: &mut gpui::MutableAppContext) { - let mut now = Instant::now(); - let buffer_1_events = Rc::new(RefCell::new(Vec::new())); - let buffer_2_events = Rc::new(RefCell::new(Vec::new())); - - let buffer1 = cx.add_model(|cx| Buffer::new(0, "abcdef", cx)); - let buffer2 = cx.add_model(|cx| Buffer::new(1, "abcdef", cx)); - let buffer_ops = buffer1.update(cx, |buffer, cx| { - let buffer_1_events = buffer_1_events.clone(); - cx.subscribe(&buffer1, move |_, _, event, _| { - buffer_1_events.borrow_mut().push(event.clone()) - }) - .detach(); - let buffer_2_events = buffer_2_events.clone(); - cx.subscribe(&buffer2, move |_, _, event, _| { - buffer_2_events.borrow_mut().push(event.clone()) - }) - .detach(); - - // An edit emits an edited event, followed by a dirtied event, - // since the buffer was previously in a clean state. - buffer.edit(Some(2..4), "XYZ", cx); - - // An empty transaction does not emit any events. - buffer.start_transaction(None).unwrap(); - buffer.end_transaction(None, cx).unwrap(); - - // A transaction containing two edits emits one edited event. - now += Duration::from_secs(1); - buffer.start_transaction_at(None, now).unwrap(); - buffer.edit(Some(5..5), "u", cx); - buffer.edit(Some(6..6), "w", cx); - buffer.end_transaction_at(None, now, cx).unwrap(); - - // Undoing a transaction emits one edited event. - buffer.undo(cx); - - buffer.operations.clone() - }); - - // Incorporating a set of remote ops emits a single edited event, - // followed by a dirtied event. - buffer2.update(cx, |buffer, cx| { - buffer.apply_ops(buffer_ops, cx).unwrap(); - }); - - let buffer_1_events = buffer_1_events.borrow(); - assert_eq!( - *buffer_1_events, - vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited] - ); - - let buffer_2_events = buffer_2_events.borrow(); - assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]); - } - - #[gpui::test(iterations = 100)] - fn test_random_edits(cx: &mut gpui::MutableAppContext, mut rng: StdRng) { - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - let reference_string_len = rng.gen_range(0..3); - let mut reference_string = RandomCharIter::new(&mut rng) - .take(reference_string_len) - .collect::(); - cx.add_model(|cx| { - let mut buffer = Buffer::new(0, reference_string.as_str(), cx); - buffer.history.group_interval = Duration::from_millis(rng.gen_range(0..=200)); - let mut buffer_versions = Vec::new(); - log::info!( - "buffer text {:?}, version: {:?}", - buffer.text(), - buffer.version() - ); - - for _i in 0..operations { - let (old_ranges, new_text) = buffer.randomly_mutate(&mut rng, cx); - for old_range in old_ranges.iter().rev() { - reference_string.replace_range(old_range.clone(), &new_text); - } - assert_eq!(buffer.text(), reference_string); - log::info!( - "buffer text {:?}, version: {:?}", - buffer.text(), - buffer.version() - ); - - if rng.gen_bool(0.25) { - buffer.randomly_undo_redo(&mut rng, cx); - reference_string = buffer.text(); - log::info!( - "buffer text {:?}, version: {:?}", - buffer.text(), - buffer.version() - ); - } - - let range = buffer.random_byte_range(0, &mut rng); - assert_eq!( - buffer.text_summary_for_range(range.clone()), - TextSummary::from(&reference_string[range]) - ); - - if rng.gen_bool(0.3) { - buffer_versions.push(buffer.clone()); - } - } - - for mut old_buffer in buffer_versions { - let edits = buffer - .edits_since(old_buffer.version.clone()) - .collect::>(); - - log::info!( - "mutating old buffer version {:?}, text: {:?}, edits since: {:?}", - old_buffer.version(), - old_buffer.text(), - edits, - ); - - let mut delta = 0_isize; - for edit in edits { - let old_start = (edit.old_bytes.start as isize + delta) as usize; - let new_text: String = buffer.text_for_range(edit.new_bytes.clone()).collect(); - old_buffer.edit( - Some(old_start..old_start + edit.deleted_bytes()), - new_text, - cx, - ); - delta += edit.delta(); - } - assert_eq!(old_buffer.text(), buffer.text()); - } - - buffer - }); - } - - #[gpui::test] - fn test_line_len(cx: &mut gpui::MutableAppContext) { - cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx); - buffer.edit(vec![0..0], "abcd\nefg\nhij", cx); - buffer.edit(vec![12..12], "kl\nmno", cx); - buffer.edit(vec![18..18], "\npqrs\n", cx); - buffer.edit(vec![18..21], "\nPQ", cx); - - assert_eq!(buffer.line_len(0), 4); - assert_eq!(buffer.line_len(1), 3); - assert_eq!(buffer.line_len(2), 5); - assert_eq!(buffer.line_len(3), 3); - assert_eq!(buffer.line_len(4), 4); - assert_eq!(buffer.line_len(5), 0); - buffer - }); - } - - #[gpui::test] - fn test_text_summary_for_range(cx: &mut gpui::MutableAppContext) { - cx.add_model(|cx| { - let buffer = Buffer::new(0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz", cx); - assert_eq!( - buffer.text_summary_for_range(1..3), - TextSummary { - bytes: 2, - lines: Point::new(1, 0), - first_line_chars: 1, - last_line_chars: 0, - longest_row: 0, - longest_row_chars: 1, - } - ); - assert_eq!( - buffer.text_summary_for_range(1..12), - TextSummary { - bytes: 11, - lines: Point::new(3, 0), - first_line_chars: 1, - last_line_chars: 0, - longest_row: 2, - longest_row_chars: 4, - } - ); - assert_eq!( - buffer.text_summary_for_range(0..20), - TextSummary { - bytes: 20, - lines: Point::new(4, 1), - first_line_chars: 2, - last_line_chars: 1, - longest_row: 3, - longest_row_chars: 6, - } - ); - assert_eq!( - buffer.text_summary_for_range(0..22), - TextSummary { - bytes: 22, - lines: Point::new(4, 3), - first_line_chars: 2, - last_line_chars: 3, - longest_row: 3, - longest_row_chars: 6, - } - ); - assert_eq!( - buffer.text_summary_for_range(7..22), - TextSummary { - bytes: 15, - lines: Point::new(2, 3), - first_line_chars: 4, - last_line_chars: 3, - longest_row: 1, - longest_row_chars: 6, - } - ); - buffer - }); - } - - #[gpui::test] - fn test_chars_at(cx: &mut gpui::MutableAppContext) { - cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx); - buffer.edit(vec![0..0], "abcd\nefgh\nij", cx); - buffer.edit(vec![12..12], "kl\nmno", cx); - buffer.edit(vec![18..18], "\npqrs", cx); - buffer.edit(vec![18..21], "\nPQ", cx); - - let chars = buffer.chars_at(Point::new(0, 0)); - assert_eq!(chars.collect::(), "abcd\nefgh\nijkl\nmno\nPQrs"); - - let chars = buffer.chars_at(Point::new(1, 0)); - assert_eq!(chars.collect::(), "efgh\nijkl\nmno\nPQrs"); - - let chars = buffer.chars_at(Point::new(2, 0)); - assert_eq!(chars.collect::(), "ijkl\nmno\nPQrs"); - - let chars = buffer.chars_at(Point::new(3, 0)); - assert_eq!(chars.collect::(), "mno\nPQrs"); - - let chars = buffer.chars_at(Point::new(4, 0)); - assert_eq!(chars.collect::(), "PQrs"); - - // Regression test: - let mut buffer = Buffer::new(0, "", cx); - buffer.edit(vec![0..0], "[workspace]\nmembers = [\n \"xray_core\",\n \"xray_server\",\n \"xray_cli\",\n \"xray_wasm\",\n]\n", cx); - buffer.edit(vec![60..60], "\n", cx); - - let chars = buffer.chars_at(Point::new(6, 0)); - assert_eq!(chars.collect::(), " \"xray_wasm\",\n]\n"); - - buffer - }); - } - - #[gpui::test] - fn test_anchors(cx: &mut gpui::MutableAppContext) { - cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx); - buffer.edit(vec![0..0], "abc", cx); - let left_anchor = buffer.anchor_before(2); - let right_anchor = buffer.anchor_after(2); - - buffer.edit(vec![1..1], "def\n", cx); - assert_eq!(buffer.text(), "adef\nbc"); - assert_eq!(left_anchor.to_offset(&buffer), 6); - assert_eq!(right_anchor.to_offset(&buffer), 6); - assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - - buffer.edit(vec![2..3], "", cx); - assert_eq!(buffer.text(), "adf\nbc"); - assert_eq!(left_anchor.to_offset(&buffer), 5); - assert_eq!(right_anchor.to_offset(&buffer), 5); - assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - - buffer.edit(vec![5..5], "ghi\n", cx); - assert_eq!(buffer.text(), "adf\nbghi\nc"); - assert_eq!(left_anchor.to_offset(&buffer), 5); - assert_eq!(right_anchor.to_offset(&buffer), 9); - assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - assert_eq!(right_anchor.to_point(&buffer), Point { row: 2, column: 0 }); - - buffer.edit(vec![7..9], "", cx); - assert_eq!(buffer.text(), "adf\nbghc"); - assert_eq!(left_anchor.to_offset(&buffer), 5); - assert_eq!(right_anchor.to_offset(&buffer), 7); - assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 },); - assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 3 }); - - // Ensure anchoring to a point is equivalent to anchoring to an offset. - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 0 }), - buffer.anchor_before(0) - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 1 }), - buffer.anchor_before(1) - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 2 }), - buffer.anchor_before(2) - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 3 }), - buffer.anchor_before(3) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 0 }), - buffer.anchor_before(4) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 1 }), - buffer.anchor_before(5) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 2 }), - buffer.anchor_before(6) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 3 }), - buffer.anchor_before(7) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 4 }), - buffer.anchor_before(8) - ); - - // Comparison between anchors. - let anchor_at_offset_0 = buffer.anchor_before(0); - let anchor_at_offset_1 = buffer.anchor_before(1); - let anchor_at_offset_2 = buffer.anchor_before(2); - - assert_eq!( - anchor_at_offset_0 - .cmp(&anchor_at_offset_0, &buffer) - .unwrap(), - Ordering::Equal - ); - assert_eq!( - anchor_at_offset_1 - .cmp(&anchor_at_offset_1, &buffer) - .unwrap(), - Ordering::Equal - ); - assert_eq!( - anchor_at_offset_2 - .cmp(&anchor_at_offset_2, &buffer) - .unwrap(), - Ordering::Equal - ); - - assert_eq!( - anchor_at_offset_0 - .cmp(&anchor_at_offset_1, &buffer) - .unwrap(), - Ordering::Less - ); - assert_eq!( - anchor_at_offset_1 - .cmp(&anchor_at_offset_2, &buffer) - .unwrap(), - Ordering::Less - ); - assert_eq!( - anchor_at_offset_0 - .cmp(&anchor_at_offset_2, &buffer) - .unwrap(), - Ordering::Less - ); - - assert_eq!( - anchor_at_offset_1 - .cmp(&anchor_at_offset_0, &buffer) - .unwrap(), - Ordering::Greater - ); - assert_eq!( - anchor_at_offset_2 - .cmp(&anchor_at_offset_1, &buffer) - .unwrap(), - Ordering::Greater - ); - assert_eq!( - anchor_at_offset_2 - .cmp(&anchor_at_offset_0, &buffer) - .unwrap(), - Ordering::Greater - ); - buffer - }); - } - - #[gpui::test] - fn test_anchors_at_start_and_end(cx: &mut gpui::MutableAppContext) { - cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx); - let before_start_anchor = buffer.anchor_before(0); - let after_end_anchor = buffer.anchor_after(0); - - buffer.edit(vec![0..0], "abc", cx); - assert_eq!(buffer.text(), "abc"); - assert_eq!(before_start_anchor.to_offset(&buffer), 0); - assert_eq!(after_end_anchor.to_offset(&buffer), 3); - - let after_start_anchor = buffer.anchor_after(0); - let before_end_anchor = buffer.anchor_before(3); - - buffer.edit(vec![3..3], "def", cx); - buffer.edit(vec![0..0], "ghi", cx); - assert_eq!(buffer.text(), "ghiabcdef"); - assert_eq!(before_start_anchor.to_offset(&buffer), 0); - assert_eq!(after_start_anchor.to_offset(&buffer), 3); - assert_eq!(before_end_anchor.to_offset(&buffer), 6); - assert_eq!(after_end_anchor.to_offset(&buffer), 9); - buffer - }); - } - - #[gpui::test] - async fn test_apply_diff(mut cx: gpui::TestAppContext) { - let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n"; - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); - - let text = "a\nccc\ndddd\nffffff\n"; - let diff = buffer.read_with(&cx, |b, cx| b.diff(text.into(), cx)).await; - buffer.update(&mut cx, |b, cx| b.apply_diff(diff, cx)); - cx.read(|cx| assert_eq!(buffer.read(cx).text(), text)); - - let text = "a\n1\n\nccc\ndd2dd\nffffff\n"; - let diff = buffer.read_with(&cx, |b, cx| b.diff(text.into(), cx)).await; - buffer.update(&mut cx, |b, cx| b.apply_diff(diff, cx)); - cx.read(|cx| assert_eq!(buffer.read(cx).text(), text)); - } - - #[gpui::test] - fn test_undo_redo(cx: &mut gpui::MutableAppContext) { - cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "1234", cx); - // Set group interval to zero so as to not group edits in the undo stack. - buffer.history.group_interval = Duration::from_secs(0); - - buffer.edit(vec![1..1], "abx", cx); - buffer.edit(vec![3..4], "yzef", cx); - buffer.edit(vec![3..5], "cd", cx); - assert_eq!(buffer.text(), "1abcdef234"); - - let transactions = buffer.history.undo_stack.clone(); - assert_eq!(transactions.len(), 3); - - buffer.undo_or_redo(transactions[0].clone(), cx).unwrap(); - assert_eq!(buffer.text(), "1cdef234"); - buffer.undo_or_redo(transactions[0].clone(), cx).unwrap(); - assert_eq!(buffer.text(), "1abcdef234"); - - buffer.undo_or_redo(transactions[1].clone(), cx).unwrap(); - assert_eq!(buffer.text(), "1abcdx234"); - buffer.undo_or_redo(transactions[2].clone(), cx).unwrap(); - assert_eq!(buffer.text(), "1abx234"); - buffer.undo_or_redo(transactions[1].clone(), cx).unwrap(); - assert_eq!(buffer.text(), "1abyzef234"); - buffer.undo_or_redo(transactions[2].clone(), cx).unwrap(); - assert_eq!(buffer.text(), "1abcdef234"); - - buffer.undo_or_redo(transactions[2].clone(), cx).unwrap(); - assert_eq!(buffer.text(), "1abyzef234"); - buffer.undo_or_redo(transactions[0].clone(), cx).unwrap(); - assert_eq!(buffer.text(), "1yzef234"); - buffer.undo_or_redo(transactions[1].clone(), cx).unwrap(); - assert_eq!(buffer.text(), "1234"); - - buffer - }); - } - - #[gpui::test] - fn test_history(cx: &mut gpui::MutableAppContext) { - cx.add_model(|cx| { - let mut now = Instant::now(); - let mut buffer = Buffer::new(0, "123456", cx); - - let set_id = - buffer.add_selection_set(buffer.selections_from_ranges(vec![4..4]).unwrap(), cx); - buffer.start_transaction_at(Some(set_id), now).unwrap(); - buffer.edit(vec![2..4], "cd", cx); - buffer.end_transaction_at(Some(set_id), now, cx).unwrap(); - assert_eq!(buffer.text(), "12cd56"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]); - - buffer.start_transaction_at(Some(set_id), now).unwrap(); - buffer - .update_selection_set( - set_id, - buffer.selections_from_ranges(vec![1..3]).unwrap(), - cx, - ) - .unwrap(); - buffer.edit(vec![4..5], "e", cx); - buffer.end_transaction_at(Some(set_id), now, cx).unwrap(); - assert_eq!(buffer.text(), "12cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); - - now += buffer.history.group_interval + Duration::from_millis(1); - buffer.start_transaction_at(Some(set_id), now).unwrap(); - buffer - .update_selection_set( - set_id, - buffer.selections_from_ranges(vec![2..2]).unwrap(), - cx, - ) - .unwrap(); - buffer.edit(vec![0..1], "a", cx); - buffer.edit(vec![1..1], "b", cx); - buffer.end_transaction_at(Some(set_id), now, cx).unwrap(); - assert_eq!(buffer.text(), "ab2cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]); - - // Last transaction happened past the group interval, undo it on its - // own. - buffer.undo(cx); - assert_eq!(buffer.text(), "12cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); - - // First two transactions happened within the group interval, undo them - // together. - buffer.undo(cx); - assert_eq!(buffer.text(), "123456"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]); - - // Redo the first two transactions together. - buffer.redo(cx); - assert_eq!(buffer.text(), "12cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); - - // Redo the last transaction on its own. - buffer.redo(cx); - assert_eq!(buffer.text(), "ab2cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]); - - buffer.start_transaction_at(None, now).unwrap(); - buffer.end_transaction_at(None, now, cx).unwrap(); - buffer.undo(cx); - assert_eq!(buffer.text(), "12cde6"); - - buffer - }); - } - - #[gpui::test] - fn test_concurrent_edits(cx: &mut gpui::MutableAppContext) { - let text = "abcdef"; - - let buffer1 = cx.add_model(|cx| Buffer::new(1, text, cx)); - let buffer2 = cx.add_model(|cx| Buffer::new(2, text, cx)); - let buffer3 = cx.add_model(|cx| Buffer::new(3, text, cx)); - - let buf1_op = buffer1.update(cx, |buffer, cx| { - buffer.edit(vec![1..2], "12", cx); - assert_eq!(buffer.text(), "a12cdef"); - buffer.operations.last().unwrap().clone() - }); - let buf2_op = buffer2.update(cx, |buffer, cx| { - buffer.edit(vec![3..4], "34", cx); - assert_eq!(buffer.text(), "abc34ef"); - buffer.operations.last().unwrap().clone() - }); - let buf3_op = buffer3.update(cx, |buffer, cx| { - buffer.edit(vec![5..6], "56", cx); - assert_eq!(buffer.text(), "abcde56"); - buffer.operations.last().unwrap().clone() - }); - - buffer1.update(cx, |buffer, _| { - buffer.apply_op(buf2_op.clone()).unwrap(); - buffer.apply_op(buf3_op.clone()).unwrap(); - }); - buffer2.update(cx, |buffer, _| { - buffer.apply_op(buf1_op.clone()).unwrap(); - buffer.apply_op(buf3_op.clone()).unwrap(); - }); - buffer3.update(cx, |buffer, _| { - buffer.apply_op(buf1_op.clone()).unwrap(); - buffer.apply_op(buf2_op.clone()).unwrap(); - }); - - assert_eq!(buffer1.read(cx).text(), "a12c34e56"); - assert_eq!(buffer2.read(cx).text(), "a12c34e56"); - assert_eq!(buffer3.read(cx).text(), "a12c34e56"); - } - - #[gpui::test(iterations = 100)] - fn test_random_concurrent_edits(cx: &mut gpui::MutableAppContext, mut rng: StdRng) { - let peers = env::var("PEERS") - .map(|i| i.parse().expect("invalid `PEERS` variable")) - .unwrap_or(5); - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - let base_text_len = rng.gen_range(0..10); - let base_text = RandomCharIter::new(&mut rng) - .take(base_text_len) - .collect::(); - let mut replica_ids = Vec::new(); - let mut buffers = Vec::new(); - let mut network = Network::new(rng.clone()); - - for i in 0..peers { - let buffer = cx.add_model(|cx| { - let mut buf = Buffer::new(i as ReplicaId, base_text.as_str(), cx); - buf.history.group_interval = Duration::from_millis(rng.gen_range(0..=200)); - buf - }); - buffers.push(buffer); - replica_ids.push(i as u16); - network.add_peer(i as u16); - } - - log::info!("initial text: {:?}", base_text); - - let mut mutation_count = operations; - loop { - let replica_index = rng.gen_range(0..peers); - let replica_id = replica_ids[replica_index]; - buffers[replica_index].update(cx, |buffer, cx| match rng.gen_range(0..=100) { - 0..=50 if mutation_count != 0 => { - buffer.randomly_mutate(&mut rng, cx); - network.broadcast(buffer.replica_id, mem::take(&mut buffer.operations)); - log::info!("buffer {} text: {:?}", buffer.replica_id, buffer.text()); - mutation_count -= 1; - } - 51..=70 if mutation_count != 0 => { - buffer.randomly_undo_redo(&mut rng, cx); - network.broadcast(buffer.replica_id, mem::take(&mut buffer.operations)); - mutation_count -= 1; - } - 71..=100 if network.has_unreceived(replica_id) => { - let ops = network.receive(replica_id); - if !ops.is_empty() { - log::info!( - "peer {} applying {} ops from the network.", - replica_id, - ops.len() - ); - buffer.apply_ops(ops, cx).unwrap(); - } - } - _ => {} - }); - - if mutation_count == 0 && network.is_idle() { - break; - } - } - - let first_buffer = buffers[0].read(cx); - for buffer in &buffers[1..] { - let buffer = buffer.read(cx); - assert_eq!( - buffer.text(), - first_buffer.text(), - "Replica {} text != Replica 0 text", - buffer.replica_id - ); - assert_eq!( - buffer.selection_sets().collect::>(), - first_buffer.selection_sets().collect::>() - ); - assert_eq!( - buffer.all_selection_ranges().collect::>(), - first_buffer - .all_selection_ranges() - .collect::>() - ); - } - } - - #[gpui::test] - async fn test_reparse(mut cx: gpui::TestAppContext) { - let rust_lang = rust_lang(); - let buffer = cx.add_model(|cx| { - let text = "fn a() {}".into(); - Buffer::from_history(0, History::new(text), None, Some(rust_lang.clone()), cx) - }); - - // Wait for the initial text to parse - buffer - .condition(&cx, |buffer, _| !buffer.is_parsing()) - .await; - assert_eq!( - get_tree_sexp(&buffer, &cx), - concat!( - "(source_file (function_item name: (identifier) ", - "parameters: (parameters) ", - "body: (block)))" - ) - ); - - buffer.update(&mut cx, |buffer, _| { - buffer.set_sync_parse_timeout(Duration::ZERO) - }); - - // Perform some edits (add parameter and variable reference) - // Parsing doesn't begin until the transaction is complete - buffer.update(&mut cx, |buf, cx| { - buf.start_transaction(None).unwrap(); - - let offset = buf.text().find(")").unwrap(); - buf.edit(vec![offset..offset], "b: C", cx); - assert!(!buf.is_parsing()); - - let offset = buf.text().find("}").unwrap(); - buf.edit(vec![offset..offset], " d; ", cx); - assert!(!buf.is_parsing()); - - buf.end_transaction(None, cx).unwrap(); - assert_eq!(buf.text(), "fn a(b: C) { d; }"); - assert!(buf.is_parsing()); - }); - buffer - .condition(&cx, |buffer, _| !buffer.is_parsing()) - .await; - assert_eq!( - get_tree_sexp(&buffer, &cx), - concat!( - "(source_file (function_item name: (identifier) ", - "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", - "body: (block (identifier))))" - ) - ); - - // Perform a series of edits without waiting for the current parse to complete: - // * turn identifier into a field expression - // * turn field expression into a method call - // * add a turbofish to the method call - buffer.update(&mut cx, |buf, cx| { - let offset = buf.text().find(";").unwrap(); - buf.edit(vec![offset..offset], ".e", cx); - assert_eq!(buf.text(), "fn a(b: C) { d.e; }"); - assert!(buf.is_parsing()); - }); - buffer.update(&mut cx, |buf, cx| { - let offset = buf.text().find(";").unwrap(); - buf.edit(vec![offset..offset], "(f)", cx); - assert_eq!(buf.text(), "fn a(b: C) { d.e(f); }"); - assert!(buf.is_parsing()); - }); - buffer.update(&mut cx, |buf, cx| { - let offset = buf.text().find("(f)").unwrap(); - buf.edit(vec![offset..offset], "::", cx); - assert_eq!(buf.text(), "fn a(b: C) { d.e::(f); }"); - assert!(buf.is_parsing()); - }); - buffer - .condition(&cx, |buffer, _| !buffer.is_parsing()) - .await; - assert_eq!( - get_tree_sexp(&buffer, &cx), - concat!( - "(source_file (function_item name: (identifier) ", - "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", - "body: (block (call_expression ", - "function: (generic_function ", - "function: (field_expression value: (identifier) field: (field_identifier)) ", - "type_arguments: (type_arguments (type_identifier))) ", - "arguments: (arguments (identifier))))))", - ) - ); - - buffer.update(&mut cx, |buf, cx| { - buf.undo(cx); - assert_eq!(buf.text(), "fn a() {}"); - assert!(buf.is_parsing()); - }); - buffer - .condition(&cx, |buffer, _| !buffer.is_parsing()) - .await; - assert_eq!( - get_tree_sexp(&buffer, &cx), - concat!( - "(source_file (function_item name: (identifier) ", - "parameters: (parameters) ", - "body: (block)))" - ) - ); - - buffer.update(&mut cx, |buf, cx| { - buf.redo(cx); - assert_eq!(buf.text(), "fn a(b: C) { d.e::(f); }"); - assert!(buf.is_parsing()); - }); - buffer - .condition(&cx, |buffer, _| !buffer.is_parsing()) - .await; - assert_eq!( - get_tree_sexp(&buffer, &cx), - concat!( - "(source_file (function_item name: (identifier) ", - "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", - "body: (block (call_expression ", - "function: (generic_function ", - "function: (field_expression value: (identifier) field: (field_identifier)) ", - "type_arguments: (type_arguments (type_identifier))) ", - "arguments: (arguments (identifier))))))", - ) - ); - - fn get_tree_sexp(buffer: &ModelHandle, cx: &gpui::TestAppContext) -> String { - buffer.read_with(cx, |buffer, _| { - buffer.syntax_tree().unwrap().root_node().to_sexp() - }) - } - } - - #[gpui::test] - async fn test_enclosing_bracket_ranges(mut cx: gpui::TestAppContext) { - use unindent::Unindent as _; - - let rust_lang = rust_lang(); - let buffer = cx.add_model(|cx| { - let text = " - mod x { - mod y { - - } - } - " - .unindent() - .into(); - Buffer::from_history(0, History::new(text), None, Some(rust_lang.clone()), cx) - }); - buffer - .condition(&cx, |buffer, _| !buffer.is_parsing()) - .await; - buffer.read_with(&cx, |buf, _| { - assert_eq!( - buf.enclosing_bracket_point_ranges(Point::new(1, 6)..Point::new(1, 6)), - Some(( - Point::new(0, 6)..Point::new(0, 7), - Point::new(4, 0)..Point::new(4, 1) - )) - ); - assert_eq!( - buf.enclosing_bracket_point_ranges(Point::new(1, 10)..Point::new(1, 10)), - Some(( - Point::new(1, 10)..Point::new(1, 11), - Point::new(3, 4)..Point::new(3, 5) - )) - ); - assert_eq!( - buf.enclosing_bracket_point_ranges(Point::new(3, 5)..Point::new(3, 5)), - Some(( - Point::new(1, 10)..Point::new(1, 11), - Point::new(3, 4)..Point::new(3, 5) - )) - ); - }); - } - - #[gpui::test] - async fn test_edit_with_autoindent(mut cx: gpui::TestAppContext) { - let rust_lang = rust_lang(); - let buffer = cx.add_model(|cx| { - let text = "fn a() {}".into(); - Buffer::from_history(0, History::new(text), None, Some(rust_lang.clone()), cx) - }); - - buffer - .condition(&cx, |buffer, _| !buffer.is_parsing()) - .await; - - buffer.update(&mut cx, |buffer, cx| { - buffer.edit_with_autoindent([8..8], "\n\n", cx); - assert_eq!(buffer.text(), "fn a() {\n \n}"); - - buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 4)], "b()\n", cx); - assert_eq!(buffer.text(), "fn a() {\n b()\n \n}"); - - buffer.edit_with_autoindent([Point::new(2, 4)..Point::new(2, 4)], ".c", cx); - assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}"); - }); - } - - #[test] - fn test_contiguous_ranges() { - assert_eq!( - contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12], 100).collect::>(), - &[1..4, 5..7, 9..13] - ); - - // Respects the `max_len` parameter - assert_eq!( - contiguous_ranges([2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31], 3) - .collect::>(), - &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32], - ); - } - - #[derive(Clone)] - struct Envelope { - message: T, - sender: ReplicaId, - } - - struct Network { - inboxes: std::collections::BTreeMap>>, - all_messages: Vec, - rng: R, - } - - impl Network { - fn new(rng: R) -> Self { - Network { - inboxes: Default::default(), - all_messages: Vec::new(), - rng, - } - } - - fn add_peer(&mut self, id: ReplicaId) { - self.inboxes.insert(id, Vec::new()); - } - - fn is_idle(&self) -> bool { - self.inboxes.values().all(|i| i.is_empty()) - } - - fn broadcast(&mut self, sender: ReplicaId, messages: Vec) { - for (replica, inbox) in self.inboxes.iter_mut() { - if *replica != sender { - for message in &messages { - let min_index = inbox - .iter() - .enumerate() - .rev() - .find_map(|(index, envelope)| { - if sender == envelope.sender { - Some(index + 1) - } else { - None - } - }) - .unwrap_or(0); - - // Insert one or more duplicates of this message *after* the previous - // message delivered by this replica. - for _ in 0..self.rng.gen_range(1..4) { - let insertion_index = self.rng.gen_range(min_index..inbox.len() + 1); - inbox.insert( - insertion_index, - Envelope { - message: message.clone(), - sender, - }, - ); - } - } - } - } - self.all_messages.extend(messages); - } - - fn has_unreceived(&self, receiver: ReplicaId) -> bool { - !self.inboxes[&receiver].is_empty() - } - - fn receive(&mut self, receiver: ReplicaId) -> Vec { - let inbox = self.inboxes.get_mut(&receiver).unwrap(); - let count = self.rng.gen_range(0..inbox.len() + 1); - inbox - .drain(0..count) - .map(|envelope| envelope.message) - .collect() - } - } - - fn rust_lang() -> Arc { - Arc::new( - Language::new( - LanguageConfig { - name: "Rust".to_string(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - tree_sitter_rust::language(), - ) - .with_indents_query( - r#" - (call_expression) @indent - (field_expression) @indent - (_ "{" "}" @end) @indent - "#, - ) - .unwrap() - .with_brackets_query(r#" ("{" @open "}" @close) "#) - .unwrap(), - ) - } -} diff --git a/crates/buffer/src/tests.rs b/crates/buffer/src/tests.rs new file mode 100644 index 0000000000..c1b6050de3 --- /dev/null +++ b/crates/buffer/src/tests.rs @@ -0,0 +1,2 @@ +mod buffer; +mod syntax; diff --git a/crates/buffer/src/tests/buffer.rs b/crates/buffer/src/tests/buffer.rs new file mode 100644 index 0000000000..7c627a45ed --- /dev/null +++ b/crates/buffer/src/tests/buffer.rs @@ -0,0 +1,790 @@ +use crate::*; +use clock::ReplicaId; +use rand::prelude::*; +use std::{ + cell::RefCell, + cmp::Ordering, + env, + iter::Iterator, + mem, + rc::Rc, + time::{Duration, Instant}, +}; + +#[gpui::test] +fn test_edit(cx: &mut gpui::MutableAppContext) { + cx.add_model(|cx| { + let mut buffer = Buffer::new(0, "abc", cx); + assert_eq!(buffer.text(), "abc"); + buffer.edit(vec![3..3], "def", cx); + assert_eq!(buffer.text(), "abcdef"); + buffer.edit(vec![0..0], "ghi", cx); + assert_eq!(buffer.text(), "ghiabcdef"); + buffer.edit(vec![5..5], "jkl", cx); + assert_eq!(buffer.text(), "ghiabjklcdef"); + buffer.edit(vec![6..7], "", cx); + assert_eq!(buffer.text(), "ghiabjlcdef"); + buffer.edit(vec![4..9], "mno", cx); + assert_eq!(buffer.text(), "ghiamnoef"); + buffer + }); +} + +#[gpui::test] +fn test_edit_events(cx: &mut gpui::MutableAppContext) { + let mut now = Instant::now(); + let buffer_1_events = Rc::new(RefCell::new(Vec::new())); + let buffer_2_events = Rc::new(RefCell::new(Vec::new())); + + let buffer1 = cx.add_model(|cx| Buffer::new(0, "abcdef", cx)); + let buffer2 = cx.add_model(|cx| Buffer::new(1, "abcdef", cx)); + let buffer_ops = buffer1.update(cx, |buffer, cx| { + let buffer_1_events = buffer_1_events.clone(); + cx.subscribe(&buffer1, move |_, _, event, _| { + buffer_1_events.borrow_mut().push(event.clone()) + }) + .detach(); + let buffer_2_events = buffer_2_events.clone(); + cx.subscribe(&buffer2, move |_, _, event, _| { + buffer_2_events.borrow_mut().push(event.clone()) + }) + .detach(); + + // An edit emits an edited event, followed by a dirtied event, + // since the buffer was previously in a clean state. + buffer.edit(Some(2..4), "XYZ", cx); + + // An empty transaction does not emit any events. + buffer.start_transaction(None).unwrap(); + buffer.end_transaction(None, cx).unwrap(); + + // A transaction containing two edits emits one edited event. + now += Duration::from_secs(1); + buffer.start_transaction_at(None, now).unwrap(); + buffer.edit(Some(5..5), "u", cx); + buffer.edit(Some(6..6), "w", cx); + buffer.end_transaction_at(None, now, cx).unwrap(); + + // Undoing a transaction emits one edited event. + buffer.undo(cx); + + buffer.operations.clone() + }); + + // Incorporating a set of remote ops emits a single edited event, + // followed by a dirtied event. + buffer2.update(cx, |buffer, cx| { + buffer.apply_ops(buffer_ops, cx).unwrap(); + }); + + let buffer_1_events = buffer_1_events.borrow(); + assert_eq!( + *buffer_1_events, + vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited] + ); + + let buffer_2_events = buffer_2_events.borrow(); + assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]); +} + +#[gpui::test(iterations = 100)] +fn test_random_edits(cx: &mut gpui::MutableAppContext, mut rng: StdRng) { + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let reference_string_len = rng.gen_range(0..3); + let mut reference_string = RandomCharIter::new(&mut rng) + .take(reference_string_len) + .collect::(); + cx.add_model(|cx| { + let mut buffer = Buffer::new(0, reference_string.as_str(), cx); + buffer.history.group_interval = Duration::from_millis(rng.gen_range(0..=200)); + let mut buffer_versions = Vec::new(); + log::info!( + "buffer text {:?}, version: {:?}", + buffer.text(), + buffer.version() + ); + + for _i in 0..operations { + let (old_ranges, new_text) = buffer.randomly_mutate(&mut rng, cx); + for old_range in old_ranges.iter().rev() { + reference_string.replace_range(old_range.clone(), &new_text); + } + assert_eq!(buffer.text(), reference_string); + log::info!( + "buffer text {:?}, version: {:?}", + buffer.text(), + buffer.version() + ); + + if rng.gen_bool(0.25) { + buffer.randomly_undo_redo(&mut rng, cx); + reference_string = buffer.text(); + log::info!( + "buffer text {:?}, version: {:?}", + buffer.text(), + buffer.version() + ); + } + + let range = buffer.random_byte_range(0, &mut rng); + assert_eq!( + buffer.text_summary_for_range(range.clone()), + TextSummary::from(&reference_string[range]) + ); + + if rng.gen_bool(0.3) { + buffer_versions.push(buffer.clone()); + } + } + + for mut old_buffer in buffer_versions { + let edits = buffer + .edits_since(old_buffer.version.clone()) + .collect::>(); + + log::info!( + "mutating old buffer version {:?}, text: {:?}, edits since: {:?}", + old_buffer.version(), + old_buffer.text(), + edits, + ); + + let mut delta = 0_isize; + for edit in edits { + let old_start = (edit.old_bytes.start as isize + delta) as usize; + let new_text: String = buffer.text_for_range(edit.new_bytes.clone()).collect(); + old_buffer.edit( + Some(old_start..old_start + edit.deleted_bytes()), + new_text, + cx, + ); + delta += edit.delta(); + } + assert_eq!(old_buffer.text(), buffer.text()); + } + + buffer + }); +} + +#[gpui::test] +fn test_line_len(cx: &mut gpui::MutableAppContext) { + cx.add_model(|cx| { + let mut buffer = Buffer::new(0, "", cx); + buffer.edit(vec![0..0], "abcd\nefg\nhij", cx); + buffer.edit(vec![12..12], "kl\nmno", cx); + buffer.edit(vec![18..18], "\npqrs\n", cx); + buffer.edit(vec![18..21], "\nPQ", cx); + + assert_eq!(buffer.line_len(0), 4); + assert_eq!(buffer.line_len(1), 3); + assert_eq!(buffer.line_len(2), 5); + assert_eq!(buffer.line_len(3), 3); + assert_eq!(buffer.line_len(4), 4); + assert_eq!(buffer.line_len(5), 0); + buffer + }); +} + +#[gpui::test] +fn test_text_summary_for_range(cx: &mut gpui::MutableAppContext) { + cx.add_model(|cx| { + let buffer = Buffer::new(0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz", cx); + assert_eq!( + buffer.text_summary_for_range(1..3), + TextSummary { + bytes: 2, + lines: Point::new(1, 0), + first_line_chars: 1, + last_line_chars: 0, + longest_row: 0, + longest_row_chars: 1, + } + ); + assert_eq!( + buffer.text_summary_for_range(1..12), + TextSummary { + bytes: 11, + lines: Point::new(3, 0), + first_line_chars: 1, + last_line_chars: 0, + longest_row: 2, + longest_row_chars: 4, + } + ); + assert_eq!( + buffer.text_summary_for_range(0..20), + TextSummary { + bytes: 20, + lines: Point::new(4, 1), + first_line_chars: 2, + last_line_chars: 1, + longest_row: 3, + longest_row_chars: 6, + } + ); + assert_eq!( + buffer.text_summary_for_range(0..22), + TextSummary { + bytes: 22, + lines: Point::new(4, 3), + first_line_chars: 2, + last_line_chars: 3, + longest_row: 3, + longest_row_chars: 6, + } + ); + assert_eq!( + buffer.text_summary_for_range(7..22), + TextSummary { + bytes: 15, + lines: Point::new(2, 3), + first_line_chars: 4, + last_line_chars: 3, + longest_row: 1, + longest_row_chars: 6, + } + ); + buffer + }); +} + +#[gpui::test] +fn test_chars_at(cx: &mut gpui::MutableAppContext) { + cx.add_model(|cx| { + let mut buffer = Buffer::new(0, "", cx); + buffer.edit(vec![0..0], "abcd\nefgh\nij", cx); + buffer.edit(vec![12..12], "kl\nmno", cx); + buffer.edit(vec![18..18], "\npqrs", cx); + buffer.edit(vec![18..21], "\nPQ", cx); + + let chars = buffer.chars_at(Point::new(0, 0)); + assert_eq!(chars.collect::(), "abcd\nefgh\nijkl\nmno\nPQrs"); + + let chars = buffer.chars_at(Point::new(1, 0)); + assert_eq!(chars.collect::(), "efgh\nijkl\nmno\nPQrs"); + + let chars = buffer.chars_at(Point::new(2, 0)); + assert_eq!(chars.collect::(), "ijkl\nmno\nPQrs"); + + let chars = buffer.chars_at(Point::new(3, 0)); + assert_eq!(chars.collect::(), "mno\nPQrs"); + + let chars = buffer.chars_at(Point::new(4, 0)); + assert_eq!(chars.collect::(), "PQrs"); + + // Regression test: + let mut buffer = Buffer::new(0, "", cx); + buffer.edit(vec![0..0], "[workspace]\nmembers = [\n \"xray_core\",\n \"xray_server\",\n \"xray_cli\",\n \"xray_wasm\",\n]\n", cx); + buffer.edit(vec![60..60], "\n", cx); + + let chars = buffer.chars_at(Point::new(6, 0)); + assert_eq!(chars.collect::(), " \"xray_wasm\",\n]\n"); + + buffer + }); +} + +#[gpui::test] +fn test_anchors(cx: &mut gpui::MutableAppContext) { + cx.add_model(|cx| { + let mut buffer = Buffer::new(0, "", cx); + buffer.edit(vec![0..0], "abc", cx); + let left_anchor = buffer.anchor_before(2); + let right_anchor = buffer.anchor_after(2); + + buffer.edit(vec![1..1], "def\n", cx); + assert_eq!(buffer.text(), "adef\nbc"); + assert_eq!(left_anchor.to_offset(&buffer), 6); + assert_eq!(right_anchor.to_offset(&buffer), 6); + assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); + assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 1 }); + + buffer.edit(vec![2..3], "", cx); + assert_eq!(buffer.text(), "adf\nbc"); + assert_eq!(left_anchor.to_offset(&buffer), 5); + assert_eq!(right_anchor.to_offset(&buffer), 5); + assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); + assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 1 }); + + buffer.edit(vec![5..5], "ghi\n", cx); + assert_eq!(buffer.text(), "adf\nbghi\nc"); + assert_eq!(left_anchor.to_offset(&buffer), 5); + assert_eq!(right_anchor.to_offset(&buffer), 9); + assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); + assert_eq!(right_anchor.to_point(&buffer), Point { row: 2, column: 0 }); + + buffer.edit(vec![7..9], "", cx); + assert_eq!(buffer.text(), "adf\nbghc"); + assert_eq!(left_anchor.to_offset(&buffer), 5); + assert_eq!(right_anchor.to_offset(&buffer), 7); + assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 },); + assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 3 }); + + // Ensure anchoring to a point is equivalent to anchoring to an offset. + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 0 }), + buffer.anchor_before(0) + ); + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 1 }), + buffer.anchor_before(1) + ); + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 2 }), + buffer.anchor_before(2) + ); + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 3 }), + buffer.anchor_before(3) + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 0 }), + buffer.anchor_before(4) + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 1 }), + buffer.anchor_before(5) + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 2 }), + buffer.anchor_before(6) + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 3 }), + buffer.anchor_before(7) + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 4 }), + buffer.anchor_before(8) + ); + + // Comparison between anchors. + let anchor_at_offset_0 = buffer.anchor_before(0); + let anchor_at_offset_1 = buffer.anchor_before(1); + let anchor_at_offset_2 = buffer.anchor_before(2); + + assert_eq!( + anchor_at_offset_0 + .cmp(&anchor_at_offset_0, &buffer) + .unwrap(), + Ordering::Equal + ); + assert_eq!( + anchor_at_offset_1 + .cmp(&anchor_at_offset_1, &buffer) + .unwrap(), + Ordering::Equal + ); + assert_eq!( + anchor_at_offset_2 + .cmp(&anchor_at_offset_2, &buffer) + .unwrap(), + Ordering::Equal + ); + + assert_eq!( + anchor_at_offset_0 + .cmp(&anchor_at_offset_1, &buffer) + .unwrap(), + Ordering::Less + ); + assert_eq!( + anchor_at_offset_1 + .cmp(&anchor_at_offset_2, &buffer) + .unwrap(), + Ordering::Less + ); + assert_eq!( + anchor_at_offset_0 + .cmp(&anchor_at_offset_2, &buffer) + .unwrap(), + Ordering::Less + ); + + assert_eq!( + anchor_at_offset_1 + .cmp(&anchor_at_offset_0, &buffer) + .unwrap(), + Ordering::Greater + ); + assert_eq!( + anchor_at_offset_2 + .cmp(&anchor_at_offset_1, &buffer) + .unwrap(), + Ordering::Greater + ); + assert_eq!( + anchor_at_offset_2 + .cmp(&anchor_at_offset_0, &buffer) + .unwrap(), + Ordering::Greater + ); + buffer + }); +} + +#[gpui::test] +fn test_anchors_at_start_and_end(cx: &mut gpui::MutableAppContext) { + cx.add_model(|cx| { + let mut buffer = Buffer::new(0, "", cx); + let before_start_anchor = buffer.anchor_before(0); + let after_end_anchor = buffer.anchor_after(0); + + buffer.edit(vec![0..0], "abc", cx); + assert_eq!(buffer.text(), "abc"); + assert_eq!(before_start_anchor.to_offset(&buffer), 0); + assert_eq!(after_end_anchor.to_offset(&buffer), 3); + + let after_start_anchor = buffer.anchor_after(0); + let before_end_anchor = buffer.anchor_before(3); + + buffer.edit(vec![3..3], "def", cx); + buffer.edit(vec![0..0], "ghi", cx); + assert_eq!(buffer.text(), "ghiabcdef"); + assert_eq!(before_start_anchor.to_offset(&buffer), 0); + assert_eq!(after_start_anchor.to_offset(&buffer), 3); + assert_eq!(before_end_anchor.to_offset(&buffer), 6); + assert_eq!(after_end_anchor.to_offset(&buffer), 9); + buffer + }); +} + +#[gpui::test] +async fn test_apply_diff(mut cx: gpui::TestAppContext) { + let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n"; + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + + let text = "a\nccc\ndddd\nffffff\n"; + let diff = buffer.read_with(&cx, |b, cx| b.diff(text.into(), cx)).await; + buffer.update(&mut cx, |b, cx| b.apply_diff(diff, cx)); + cx.read(|cx| assert_eq!(buffer.read(cx).text(), text)); + + let text = "a\n1\n\nccc\ndd2dd\nffffff\n"; + let diff = buffer.read_with(&cx, |b, cx| b.diff(text.into(), cx)).await; + buffer.update(&mut cx, |b, cx| b.apply_diff(diff, cx)); + cx.read(|cx| assert_eq!(buffer.read(cx).text(), text)); +} + +#[gpui::test] +fn test_undo_redo(cx: &mut gpui::MutableAppContext) { + cx.add_model(|cx| { + let mut buffer = Buffer::new(0, "1234", cx); + // Set group interval to zero so as to not group edits in the undo stack. + buffer.history.group_interval = Duration::from_secs(0); + + buffer.edit(vec![1..1], "abx", cx); + buffer.edit(vec![3..4], "yzef", cx); + buffer.edit(vec![3..5], "cd", cx); + assert_eq!(buffer.text(), "1abcdef234"); + + let transactions = buffer.history.undo_stack.clone(); + assert_eq!(transactions.len(), 3); + + buffer.undo_or_redo(transactions[0].clone(), cx).unwrap(); + assert_eq!(buffer.text(), "1cdef234"); + buffer.undo_or_redo(transactions[0].clone(), cx).unwrap(); + assert_eq!(buffer.text(), "1abcdef234"); + + buffer.undo_or_redo(transactions[1].clone(), cx).unwrap(); + assert_eq!(buffer.text(), "1abcdx234"); + buffer.undo_or_redo(transactions[2].clone(), cx).unwrap(); + assert_eq!(buffer.text(), "1abx234"); + buffer.undo_or_redo(transactions[1].clone(), cx).unwrap(); + assert_eq!(buffer.text(), "1abyzef234"); + buffer.undo_or_redo(transactions[2].clone(), cx).unwrap(); + assert_eq!(buffer.text(), "1abcdef234"); + + buffer.undo_or_redo(transactions[2].clone(), cx).unwrap(); + assert_eq!(buffer.text(), "1abyzef234"); + buffer.undo_or_redo(transactions[0].clone(), cx).unwrap(); + assert_eq!(buffer.text(), "1yzef234"); + buffer.undo_or_redo(transactions[1].clone(), cx).unwrap(); + assert_eq!(buffer.text(), "1234"); + + buffer + }); +} + +#[gpui::test] +fn test_history(cx: &mut gpui::MutableAppContext) { + cx.add_model(|cx| { + let mut now = Instant::now(); + let mut buffer = Buffer::new(0, "123456", cx); + + let set_id = + buffer.add_selection_set(buffer.selections_from_ranges(vec![4..4]).unwrap(), cx); + buffer.start_transaction_at(Some(set_id), now).unwrap(); + buffer.edit(vec![2..4], "cd", cx); + buffer.end_transaction_at(Some(set_id), now, cx).unwrap(); + assert_eq!(buffer.text(), "12cd56"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]); + + buffer.start_transaction_at(Some(set_id), now).unwrap(); + buffer + .update_selection_set( + set_id, + buffer.selections_from_ranges(vec![1..3]).unwrap(), + cx, + ) + .unwrap(); + buffer.edit(vec![4..5], "e", cx); + buffer.end_transaction_at(Some(set_id), now, cx).unwrap(); + assert_eq!(buffer.text(), "12cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); + + now += buffer.history.group_interval + Duration::from_millis(1); + buffer.start_transaction_at(Some(set_id), now).unwrap(); + buffer + .update_selection_set( + set_id, + buffer.selections_from_ranges(vec![2..2]).unwrap(), + cx, + ) + .unwrap(); + buffer.edit(vec![0..1], "a", cx); + buffer.edit(vec![1..1], "b", cx); + buffer.end_transaction_at(Some(set_id), now, cx).unwrap(); + assert_eq!(buffer.text(), "ab2cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]); + + // Last transaction happened past the group interval, undo it on its + // own. + buffer.undo(cx); + assert_eq!(buffer.text(), "12cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); + + // First two transactions happened within the group interval, undo them + // together. + buffer.undo(cx); + assert_eq!(buffer.text(), "123456"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]); + + // Redo the first two transactions together. + buffer.redo(cx); + assert_eq!(buffer.text(), "12cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); + + // Redo the last transaction on its own. + buffer.redo(cx); + assert_eq!(buffer.text(), "ab2cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]); + + buffer.start_transaction_at(None, now).unwrap(); + buffer.end_transaction_at(None, now, cx).unwrap(); + buffer.undo(cx); + assert_eq!(buffer.text(), "12cde6"); + + buffer + }); +} + +#[gpui::test] +fn test_concurrent_edits(cx: &mut gpui::MutableAppContext) { + let text = "abcdef"; + + let buffer1 = cx.add_model(|cx| Buffer::new(1, text, cx)); + let buffer2 = cx.add_model(|cx| Buffer::new(2, text, cx)); + let buffer3 = cx.add_model(|cx| Buffer::new(3, text, cx)); + + let buf1_op = buffer1.update(cx, |buffer, cx| { + buffer.edit(vec![1..2], "12", cx); + assert_eq!(buffer.text(), "a12cdef"); + buffer.operations.last().unwrap().clone() + }); + let buf2_op = buffer2.update(cx, |buffer, cx| { + buffer.edit(vec![3..4], "34", cx); + assert_eq!(buffer.text(), "abc34ef"); + buffer.operations.last().unwrap().clone() + }); + let buf3_op = buffer3.update(cx, |buffer, cx| { + buffer.edit(vec![5..6], "56", cx); + assert_eq!(buffer.text(), "abcde56"); + buffer.operations.last().unwrap().clone() + }); + + buffer1.update(cx, |buffer, _| { + buffer.apply_op(buf2_op.clone()).unwrap(); + buffer.apply_op(buf3_op.clone()).unwrap(); + }); + buffer2.update(cx, |buffer, _| { + buffer.apply_op(buf1_op.clone()).unwrap(); + buffer.apply_op(buf3_op.clone()).unwrap(); + }); + buffer3.update(cx, |buffer, _| { + buffer.apply_op(buf1_op.clone()).unwrap(); + buffer.apply_op(buf2_op.clone()).unwrap(); + }); + + assert_eq!(buffer1.read(cx).text(), "a12c34e56"); + assert_eq!(buffer2.read(cx).text(), "a12c34e56"); + assert_eq!(buffer3.read(cx).text(), "a12c34e56"); +} + +#[gpui::test(iterations = 100)] +fn test_random_concurrent_edits(cx: &mut gpui::MutableAppContext, mut rng: StdRng) { + let peers = env::var("PEERS") + .map(|i| i.parse().expect("invalid `PEERS` variable")) + .unwrap_or(5); + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let base_text_len = rng.gen_range(0..10); + let base_text = RandomCharIter::new(&mut rng) + .take(base_text_len) + .collect::(); + let mut replica_ids = Vec::new(); + let mut buffers = Vec::new(); + let mut network = Network::new(rng.clone()); + + for i in 0..peers { + let buffer = cx.add_model(|cx| { + let mut buf = Buffer::new(i as ReplicaId, base_text.as_str(), cx); + buf.history.group_interval = Duration::from_millis(rng.gen_range(0..=200)); + buf + }); + buffers.push(buffer); + replica_ids.push(i as u16); + network.add_peer(i as u16); + } + + log::info!("initial text: {:?}", base_text); + + let mut mutation_count = operations; + loop { + let replica_index = rng.gen_range(0..peers); + let replica_id = replica_ids[replica_index]; + buffers[replica_index].update(cx, |buffer, cx| match rng.gen_range(0..=100) { + 0..=50 if mutation_count != 0 => { + buffer.randomly_mutate(&mut rng, cx); + network.broadcast(buffer.replica_id, mem::take(&mut buffer.operations)); + log::info!("buffer {} text: {:?}", buffer.replica_id, buffer.text()); + mutation_count -= 1; + } + 51..=70 if mutation_count != 0 => { + buffer.randomly_undo_redo(&mut rng, cx); + network.broadcast(buffer.replica_id, mem::take(&mut buffer.operations)); + mutation_count -= 1; + } + 71..=100 if network.has_unreceived(replica_id) => { + let ops = network.receive(replica_id); + if !ops.is_empty() { + log::info!( + "peer {} applying {} ops from the network.", + replica_id, + ops.len() + ); + buffer.apply_ops(ops, cx).unwrap(); + } + } + _ => {} + }); + + if mutation_count == 0 && network.is_idle() { + break; + } + } + + let first_buffer = buffers[0].read(cx); + for buffer in &buffers[1..] { + let buffer = buffer.read(cx); + assert_eq!( + buffer.text(), + first_buffer.text(), + "Replica {} text != Replica 0 text", + buffer.replica_id + ); + assert_eq!( + buffer.selection_sets().collect::>(), + first_buffer.selection_sets().collect::>() + ); + assert_eq!( + buffer.all_selection_ranges().collect::>(), + first_buffer + .all_selection_ranges() + .collect::>() + ); + } +} + +#[derive(Clone)] +struct Envelope { + message: T, + sender: ReplicaId, +} + +struct Network { + inboxes: std::collections::BTreeMap>>, + all_messages: Vec, + rng: R, +} + +impl Network { + fn new(rng: R) -> Self { + Network { + inboxes: Default::default(), + all_messages: Vec::new(), + rng, + } + } + + fn add_peer(&mut self, id: ReplicaId) { + self.inboxes.insert(id, Vec::new()); + } + + fn is_idle(&self) -> bool { + self.inboxes.values().all(|i| i.is_empty()) + } + + fn broadcast(&mut self, sender: ReplicaId, messages: Vec) { + for (replica, inbox) in self.inboxes.iter_mut() { + if *replica != sender { + for message in &messages { + let min_index = inbox + .iter() + .enumerate() + .rev() + .find_map(|(index, envelope)| { + if sender == envelope.sender { + Some(index + 1) + } else { + None + } + }) + .unwrap_or(0); + + // Insert one or more duplicates of this message *after* the previous + // message delivered by this replica. + for _ in 0..self.rng.gen_range(1..4) { + let insertion_index = self.rng.gen_range(min_index..inbox.len() + 1); + inbox.insert( + insertion_index, + Envelope { + message: message.clone(), + sender, + }, + ); + } + } + } + } + self.all_messages.extend(messages); + } + + fn has_unreceived(&self, receiver: ReplicaId) -> bool { + !self.inboxes[&receiver].is_empty() + } + + fn receive(&mut self, receiver: ReplicaId) -> Vec { + let inbox = self.inboxes.get_mut(&receiver).unwrap(); + let count = self.rng.gen_range(0..inbox.len() + 1); + inbox + .drain(0..count) + .map(|envelope| envelope.message) + .collect() + } +} diff --git a/crates/buffer/src/tests/syntax.rs b/crates/buffer/src/tests/syntax.rs new file mode 100644 index 0000000000..8c05e10316 --- /dev/null +++ b/crates/buffer/src/tests/syntax.rs @@ -0,0 +1,237 @@ +use crate::*; +use gpui::ModelHandle; +use unindent::Unindent as _; + +#[gpui::test] +async fn test_reparse(mut cx: gpui::TestAppContext) { + let buffer = cx.add_model(|cx| { + let text = "fn a() {}".into(); + Buffer::from_history(0, History::new(text), None, Some(rust_lang()), cx) + }); + + // Wait for the initial text to parse + buffer + .condition(&cx, |buffer, _| !buffer.is_parsing()) + .await; + assert_eq!( + get_tree_sexp(&buffer, &cx), + concat!( + "(source_file (function_item name: (identifier) ", + "parameters: (parameters) ", + "body: (block)))" + ) + ); + + buffer.update(&mut cx, |buffer, _| { + buffer.set_sync_parse_timeout(Duration::ZERO) + }); + + // Perform some edits (add parameter and variable reference) + // Parsing doesn't begin until the transaction is complete + buffer.update(&mut cx, |buf, cx| { + buf.start_transaction(None).unwrap(); + + let offset = buf.text().find(")").unwrap(); + buf.edit(vec![offset..offset], "b: C", cx); + assert!(!buf.is_parsing()); + + let offset = buf.text().find("}").unwrap(); + buf.edit(vec![offset..offset], " d; ", cx); + assert!(!buf.is_parsing()); + + buf.end_transaction(None, cx).unwrap(); + assert_eq!(buf.text(), "fn a(b: C) { d; }"); + assert!(buf.is_parsing()); + }); + buffer + .condition(&cx, |buffer, _| !buffer.is_parsing()) + .await; + assert_eq!( + get_tree_sexp(&buffer, &cx), + concat!( + "(source_file (function_item name: (identifier) ", + "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", + "body: (block (identifier))))" + ) + ); + + // Perform a series of edits without waiting for the current parse to complete: + // * turn identifier into a field expression + // * turn field expression into a method call + // * add a turbofish to the method call + buffer.update(&mut cx, |buf, cx| { + let offset = buf.text().find(";").unwrap(); + buf.edit(vec![offset..offset], ".e", cx); + assert_eq!(buf.text(), "fn a(b: C) { d.e; }"); + assert!(buf.is_parsing()); + }); + buffer.update(&mut cx, |buf, cx| { + let offset = buf.text().find(";").unwrap(); + buf.edit(vec![offset..offset], "(f)", cx); + assert_eq!(buf.text(), "fn a(b: C) { d.e(f); }"); + assert!(buf.is_parsing()); + }); + buffer.update(&mut cx, |buf, cx| { + let offset = buf.text().find("(f)").unwrap(); + buf.edit(vec![offset..offset], "::", cx); + assert_eq!(buf.text(), "fn a(b: C) { d.e::(f); }"); + assert!(buf.is_parsing()); + }); + buffer + .condition(&cx, |buffer, _| !buffer.is_parsing()) + .await; + assert_eq!( + get_tree_sexp(&buffer, &cx), + concat!( + "(source_file (function_item name: (identifier) ", + "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", + "body: (block (call_expression ", + "function: (generic_function ", + "function: (field_expression value: (identifier) field: (field_identifier)) ", + "type_arguments: (type_arguments (type_identifier))) ", + "arguments: (arguments (identifier))))))", + ) + ); + + buffer.update(&mut cx, |buf, cx| { + buf.undo(cx); + assert_eq!(buf.text(), "fn a() {}"); + assert!(buf.is_parsing()); + }); + buffer + .condition(&cx, |buffer, _| !buffer.is_parsing()) + .await; + assert_eq!( + get_tree_sexp(&buffer, &cx), + concat!( + "(source_file (function_item name: (identifier) ", + "parameters: (parameters) ", + "body: (block)))" + ) + ); + + buffer.update(&mut cx, |buf, cx| { + buf.redo(cx); + assert_eq!(buf.text(), "fn a(b: C) { d.e::(f); }"); + assert!(buf.is_parsing()); + }); + buffer + .condition(&cx, |buffer, _| !buffer.is_parsing()) + .await; + assert_eq!( + get_tree_sexp(&buffer, &cx), + concat!( + "(source_file (function_item name: (identifier) ", + "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", + "body: (block (call_expression ", + "function: (generic_function ", + "function: (field_expression value: (identifier) field: (field_identifier)) ", + "type_arguments: (type_arguments (type_identifier))) ", + "arguments: (arguments (identifier))))))", + ) + ); + + fn get_tree_sexp(buffer: &ModelHandle, cx: &gpui::TestAppContext) -> String { + buffer.read_with(cx, |buffer, _| { + buffer.syntax_tree().unwrap().root_node().to_sexp() + }) + } +} + +#[gpui::test] +async fn test_enclosing_bracket_ranges(mut cx: gpui::TestAppContext) { + let buffer = cx.add_model(|cx| { + let text = " + mod x { + mod y { + + } + } + " + .unindent() + .into(); + Buffer::from_history(0, History::new(text), None, Some(rust_lang()), cx) + }); + buffer + .condition(&cx, |buffer, _| !buffer.is_parsing()) + .await; + buffer.read_with(&cx, |buf, _| { + assert_eq!( + buf.enclosing_bracket_point_ranges(Point::new(1, 6)..Point::new(1, 6)), + Some(( + Point::new(0, 6)..Point::new(0, 7), + Point::new(4, 0)..Point::new(4, 1) + )) + ); + assert_eq!( + buf.enclosing_bracket_point_ranges(Point::new(1, 10)..Point::new(1, 10)), + Some(( + Point::new(1, 10)..Point::new(1, 11), + Point::new(3, 4)..Point::new(3, 5) + )) + ); + assert_eq!( + buf.enclosing_bracket_point_ranges(Point::new(3, 5)..Point::new(3, 5)), + Some(( + Point::new(1, 10)..Point::new(1, 11), + Point::new(3, 4)..Point::new(3, 5) + )) + ); + }); +} + +#[gpui::test] +async fn test_edit_with_autoindent(mut cx: gpui::TestAppContext) { + let buffer = cx.add_model(|cx| { + let text = "fn a() {}".into(); + Buffer::from_history(0, History::new(text), None, Some(rust_lang()), cx) + }); + + buffer.update(&mut cx, |buffer, cx| { + buffer.edit_with_autoindent([8..8], "\n\n", cx); + assert_eq!(buffer.text(), "fn a() {\n \n}"); + + buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 4)], "b()\n", cx); + assert_eq!(buffer.text(), "fn a() {\n b()\n \n}"); + + buffer.edit_with_autoindent([Point::new(2, 4)..Point::new(2, 4)], ".c", cx); + assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}"); + }); +} + +#[test] +fn test_contiguous_ranges() { + assert_eq!( + contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12], 100).collect::>(), + &[1..4, 5..7, 9..13] + ); + + // Respects the `max_len` parameter + assert_eq!( + contiguous_ranges([2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31], 3).collect::>(), + &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32], + ); +} + +fn rust_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "Rust".to_string(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + tree_sitter_rust::language(), + ) + .with_indents_query( + r#" + (call_expression) @indent + (field_expression) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap() + .with_brackets_query(r#" ("{" @open "}" @close) "#) + .unwrap(), + ) +} From 63e775eb4c212e02e7d3dc693c28457209372d8c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 11 Oct 2021 13:02:01 -0700 Subject: [PATCH 25/28] Add unit tests for selective indentation adjustment --- crates/buffer/src/tests/syntax.rs | 157 +++++++++++++++++++++++------- 1 file changed, 124 insertions(+), 33 deletions(-) diff --git a/crates/buffer/src/tests/syntax.rs b/crates/buffer/src/tests/syntax.rs index 8c05e10316..af7b68c289 100644 --- a/crates/buffer/src/tests/syntax.rs +++ b/crates/buffer/src/tests/syntax.rs @@ -1,5 +1,5 @@ use crate::*; -use gpui::ModelHandle; +use gpui::{ModelHandle, MutableAppContext}; use unindent::Unindent as _; #[gpui::test] @@ -139,7 +139,7 @@ async fn test_reparse(mut cx: gpui::TestAppContext) { } #[gpui::test] -async fn test_enclosing_bracket_ranges(mut cx: gpui::TestAppContext) { +fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { let buffer = cx.add_model(|cx| { let text = " mod x { @@ -152,42 +152,36 @@ async fn test_enclosing_bracket_ranges(mut cx: gpui::TestAppContext) { .into(); Buffer::from_history(0, History::new(text), None, Some(rust_lang()), cx) }); - buffer - .condition(&cx, |buffer, _| !buffer.is_parsing()) - .await; - buffer.read_with(&cx, |buf, _| { - assert_eq!( - buf.enclosing_bracket_point_ranges(Point::new(1, 6)..Point::new(1, 6)), - Some(( - Point::new(0, 6)..Point::new(0, 7), - Point::new(4, 0)..Point::new(4, 1) - )) - ); - assert_eq!( - buf.enclosing_bracket_point_ranges(Point::new(1, 10)..Point::new(1, 10)), - Some(( - Point::new(1, 10)..Point::new(1, 11), - Point::new(3, 4)..Point::new(3, 5) - )) - ); - assert_eq!( - buf.enclosing_bracket_point_ranges(Point::new(3, 5)..Point::new(3, 5)), - Some(( - Point::new(1, 10)..Point::new(1, 11), - Point::new(3, 4)..Point::new(3, 5) - )) - ); - }); + let buffer = buffer.read(cx); + assert_eq!( + buffer.enclosing_bracket_point_ranges(Point::new(1, 6)..Point::new(1, 6)), + Some(( + Point::new(0, 6)..Point::new(0, 7), + Point::new(4, 0)..Point::new(4, 1) + )) + ); + assert_eq!( + buffer.enclosing_bracket_point_ranges(Point::new(1, 10)..Point::new(1, 10)), + Some(( + Point::new(1, 10)..Point::new(1, 11), + Point::new(3, 4)..Point::new(3, 5) + )) + ); + assert_eq!( + buffer.enclosing_bracket_point_ranges(Point::new(3, 5)..Point::new(3, 5)), + Some(( + Point::new(1, 10)..Point::new(1, 11), + Point::new(3, 4)..Point::new(3, 5) + )) + ); } #[gpui::test] -async fn test_edit_with_autoindent(mut cx: gpui::TestAppContext) { - let buffer = cx.add_model(|cx| { +fn test_edit_with_autoindent(cx: &mut MutableAppContext) { + cx.add_model(|cx| { let text = "fn a() {}".into(); - Buffer::from_history(0, History::new(text), None, Some(rust_lang()), cx) - }); + let mut buffer = Buffer::from_history(0, History::new(text), None, Some(rust_lang()), cx); - buffer.update(&mut cx, |buffer, cx| { buffer.edit_with_autoindent([8..8], "\n\n", cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); @@ -196,9 +190,105 @@ async fn test_edit_with_autoindent(mut cx: gpui::TestAppContext) { buffer.edit_with_autoindent([Point::new(2, 4)..Point::new(2, 4)], ".c", cx); assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}"); + + buffer }); } +#[gpui::test] +fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut MutableAppContext) { + cx.add_model(|cx| { + let text = " + fn a() { + c; + d; + } + " + .unindent() + .into(); + let mut buffer = Buffer::from_history(0, History::new(text), None, Some(rust_lang()), cx); + + // Lines 2 and 3 don't match the indentation suggestion. When editing these lines, + // their indentation is not adjusted. + buffer.edit_with_autoindent([empty(Point::new(1, 1)), empty(Point::new(2, 1))], "()", cx); + assert_eq!( + buffer.text(), + " + fn a() { + c(); + d(); + } + " + .unindent() + ); + + // When appending new content after these lines, the indentation is based on the + // preceding lines' actual indentation. + buffer.edit_with_autoindent( + [empty(Point::new(1, 1)), empty(Point::new(2, 1))], + "\n.f\n.g", + cx, + ); + assert_eq!( + buffer.text(), + " + fn a() { + c + .f + .g(); + d + .f + .g(); + } + " + .unindent() + ); + buffer + }); +} + +#[gpui::test] +fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppContext) { + cx.add_model(|cx| { + let text = History::new( + " + fn a() {} + " + .unindent() + .into(), + ); + let mut buffer = Buffer::from_history(0, text, None, Some(rust_lang()), cx); + + buffer.edit_with_autoindent([5..5], "\nb", cx); + assert_eq!( + buffer.text(), + " + fn a( + b) {} + " + .unindent() + ); + + // The indentation suggestion changed because `@end` node (a close paren) + // is now at the beginning of the line. + buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 5)], "", cx); + assert_eq!( + buffer.text(), + " + fn a( + ) {} + " + .unindent() + ); + + buffer + }); +} + +fn empty(point: Point) -> Range { + point..point +} + #[test] fn test_contiguous_ranges() { assert_eq!( @@ -227,6 +317,7 @@ fn rust_lang() -> Arc { r#" (call_expression) @indent (field_expression) @indent + (_ "(" ")" @end) @indent (_ "{" "}" @end) @indent "#, ) From 47372e734292f814631c0d6f0c3bf0c2d57f0886 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 11 Oct 2021 15:27:38 -0700 Subject: [PATCH 26/28] Move selections explicitly when applying autoindents --- crates/buffer/src/lib.rs | 101 ++++++++++++++++++++---------- crates/buffer/src/tests/syntax.rs | 60 ++++++++++++++++-- 2 files changed, 124 insertions(+), 37 deletions(-) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index 3937f39920..51f8a04603 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -201,6 +201,7 @@ struct SyntaxTree { #[derive(Clone)] struct AutoindentRequest { + selection_set_id: Option, before_edit: Snapshot, edited: AnchorSet, inserted: Option, @@ -985,26 +986,12 @@ impl Buffer { .background() .block_with_timeout(Duration::from_micros(500), indent_columns) { - Ok(indent_columns) => { - log::info!("finished synchronously {:?}", indent_columns); - self.autoindent_requests.clear(); - self.start_transaction(None).unwrap(); - for (row, indent_column) in indent_columns { - self.set_indent_column_for_line(row, indent_column, cx); - } - self.end_transaction(None, cx).unwrap(); - } + Ok(indent_columns) => self.apply_autoindents(indent_columns, cx), Err(indent_columns) => { self.pending_autoindent = Some(cx.spawn(|this, mut cx| async move { let indent_columns = indent_columns.await; - log::info!("finished ASYNC, {:?}", indent_columns); this.update(&mut cx, |this, cx| { - this.autoindent_requests.clear(); - this.start_transaction(None).unwrap(); - for (row, indent_column) in indent_columns { - this.set_indent_column_for_line(row, indent_column, cx); - } - this.end_transaction(None, cx).unwrap(); + this.apply_autoindents(indent_columns, cx); }); })); } @@ -1119,6 +1106,57 @@ impl Buffer { }) } + fn apply_autoindents( + &mut self, + indent_columns: BTreeMap, + cx: &mut ModelContext, + ) { + let selection_set_ids = self + .autoindent_requests + .drain(..) + .filter_map(|req| req.selection_set_id) + .collect::>(); + + self.start_transaction(None).unwrap(); + for (row, indent_column) in &indent_columns { + self.set_indent_column_for_line(*row, *indent_column, cx); + } + + for selection_set_id in selection_set_ids { + if let Some(set) = self.selections.get(&selection_set_id) { + let new_selections = set + .selections + .iter() + .map(|selection| { + let start_point = selection.start.to_point(&*self); + if start_point.column == 0 { + let end_point = selection.end.to_point(&*self); + let delta = Point::new( + 0, + indent_columns.get(&start_point.row).copied().unwrap_or(0), + ); + if delta.column > 0 { + return Selection { + id: selection.id, + goal: selection.goal, + reversed: selection.reversed, + start: self + .anchor_at(start_point + delta, selection.start.bias), + end: self.anchor_at(end_point + delta, selection.end.bias), + }; + } + } + selection.clone() + }) + .collect::>(); + self.update_selection_set(selection_set_id, new_selections, cx) + .unwrap(); + } + } + + self.end_transaction(None, cx).unwrap(); + } + pub fn indent_column_for_line(&self, row: u32) -> u32 { self.content().indent_column_for_line(row) } @@ -1127,23 +1165,11 @@ impl Buffer { let current_column = self.indent_column_for_line(row); if column > current_column { let offset = self.visible_text.to_offset(Point::new(row, 0)); - - // TODO: do this differently. By replacing the preceding newline, - // we force the new indentation to come before any left-biased anchors - // on the line. - let delta = (column - current_column) as usize; - if offset > 0 { - let mut prefix = String::with_capacity(1 + delta); - prefix.push('\n'); - prefix.extend(std::iter::repeat(' ').take(delta)); - self.edit([(offset - 1)..offset], prefix, cx); - } else { - self.edit( - [offset..offset], - std::iter::repeat(' ').take(delta).collect::(), - cx, - ); - } + self.edit( + [offset..offset], + " ".repeat((column - current_column) as usize), + cx, + ); } else if column < current_column { self.edit( [Point::new(row, 0)..Point::new(row, current_column - column)], @@ -1518,7 +1544,16 @@ impl Buffer { }))); } + let selection_set_id = self + .history + .undo_stack + .last() + .unwrap() + .selections_before + .as_ref() + .map(|(set_id, _)| *set_id); self.autoindent_requests.push(Arc::new(AutoindentRequest { + selection_set_id, before_edit, edited, inserted, diff --git a/crates/buffer/src/tests/syntax.rs b/crates/buffer/src/tests/syntax.rs index af7b68c289..4b897dd942 100644 --- a/crates/buffer/src/tests/syntax.rs +++ b/crates/buffer/src/tests/syntax.rs @@ -195,6 +195,58 @@ fn test_edit_with_autoindent(cx: &mut MutableAppContext) { }); } +#[gpui::test] +fn test_autoindent_moves_selections(cx: &mut MutableAppContext) { + cx.add_model(|cx| { + let text = History::new("fn a() {}".into()); + let mut buffer = Buffer::from_history(0, text, None, Some(rust_lang()), cx); + + let selection_set_id = buffer.add_selection_set(Vec::new(), cx); + buffer.start_transaction(Some(selection_set_id)).unwrap(); + buffer.edit_with_autoindent([5..5, 9..9], "\n\n", cx); + buffer + .update_selection_set( + selection_set_id, + vec![ + Selection { + id: 0, + start: buffer.anchor_before(Point::new(1, 0)), + end: buffer.anchor_before(Point::new(1, 0)), + reversed: false, + goal: SelectionGoal::None, + }, + Selection { + id: 1, + start: buffer.anchor_before(Point::new(4, 0)), + end: buffer.anchor_before(Point::new(4, 0)), + reversed: false, + goal: SelectionGoal::None, + }, + ], + cx, + ) + .unwrap(); + assert_eq!(buffer.text(), "fn a(\n\n) {}\n\n"); + + // Ending the transaction runs the auto-indent. The selection + // at the start of the auto-indented row is pushed to the right. + buffer.end_transaction(Some(selection_set_id), cx).unwrap(); + assert_eq!(buffer.text(), "fn a(\n \n) {}\n\n"); + let selection_ranges = buffer + .selection_set(selection_set_id) + .unwrap() + .selections + .iter() + .map(|selection| selection.point_range(&buffer)) + .collect::>(); + + assert_eq!(selection_ranges[0], empty(Point::new(1, 4))); + assert_eq!(selection_ranges[1], empty(Point::new(4, 0))); + + buffer + }); +} + #[gpui::test] fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut MutableAppContext) { cx.add_model(|cx| { @@ -285,10 +337,6 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte }); } -fn empty(point: Point) -> Range { - point..point -} - #[test] fn test_contiguous_ranges() { assert_eq!( @@ -326,3 +374,7 @@ fn rust_lang() -> Arc { .unwrap(), ) } + +fn empty(point: Point) -> Range { + point..point +} From 561857fdf232c466b0035ce6bca47725a3816db6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 11 Oct 2021 17:22:18 -0600 Subject: [PATCH 27/28] Restore all active selections when undoing/redoing autoindent In the unlikely event that we're handling autoindent requests from multiple editors, we undo/redo selections from both editors. This is somewhat imperfect but probably good enough and easier than performing auto-indents on a per-editor basis. --- crates/buffer/src/lib.rs | 104 ++++++++++++++++++++++----------------- 1 file changed, 59 insertions(+), 45 deletions(-) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index 51f8a04603..19c74da9f1 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -201,7 +201,7 @@ struct SyntaxTree { #[derive(Clone)] struct AutoindentRequest { - selection_set_id: Option, + selection_set_ids: HashSet, before_edit: Snapshot, edited: AnchorSet, inserted: Option, @@ -214,8 +214,8 @@ struct Transaction { buffer_was_dirty: bool, edits: Vec, ranges: Vec>, - selections_before: Option<(SelectionSetId, Arc<[Selection]>)>, - selections_after: Option<(SelectionSetId, Arc<[Selection]>)>, + selections_before: HashMap>, + selections_after: HashMap>, first_edit_at: Instant, last_edit_at: Instant, } @@ -299,7 +299,7 @@ impl History { &mut self, start: clock::Global, buffer_was_dirty: bool, - selections: Option<(SelectionSetId, Arc<[Selection]>)>, + selections_before: HashMap>, now: Instant, ) { self.transaction_depth += 1; @@ -310,8 +310,8 @@ impl History { buffer_was_dirty, edits: Vec::new(), ranges: Vec::new(), - selections_before: selections, - selections_after: None, + selections_before, + selections_after: Default::default(), first_edit_at: now, last_edit_at: now, }); @@ -320,7 +320,7 @@ impl History { fn end_transaction( &mut self, - selections: Option<(SelectionSetId, Arc<[Selection]>)>, + selections_after: HashMap>, now: Instant, ) -> Option<&Transaction> { assert_ne!(self.transaction_depth, 0); @@ -331,7 +331,7 @@ impl History { None } else { let transaction = self.undo_stack.last_mut().unwrap(); - transaction.selections_after = selections; + transaction.selections_after = selections_after; transaction.last_edit_at = now; Some(transaction) } @@ -367,7 +367,9 @@ impl History { if let Some(transaction) = transactions_to_merge.last_mut() { last_transaction.last_edit_at = transaction.last_edit_at; - last_transaction.selections_after = transaction.selections_after.take(); + last_transaction + .selections_after + .extend(transaction.selections_after.drain()); last_transaction.end = transaction.end.clone(); } } @@ -1114,16 +1116,17 @@ impl Buffer { let selection_set_ids = self .autoindent_requests .drain(..) - .filter_map(|req| req.selection_set_id) + .flat_map(|req| req.selection_set_ids.clone()) .collect::>(); - self.start_transaction(None).unwrap(); + self.start_transaction(selection_set_ids.iter().copied()) + .unwrap(); for (row, indent_column) in &indent_columns { self.set_indent_column_for_line(*row, *indent_column, cx); } - for selection_set_id in selection_set_ids { - if let Some(set) = self.selections.get(&selection_set_id) { + for selection_set_id in &selection_set_ids { + if let Some(set) = self.selections.get(selection_set_id) { let new_selections = set .selections .iter() @@ -1149,12 +1152,13 @@ impl Buffer { selection.clone() }) .collect::>(); - self.update_selection_set(selection_set_id, new_selections, cx) + self.update_selection_set(*selection_set_id, new_selections, cx) .unwrap(); } } - self.end_transaction(None, cx).unwrap(); + self.end_transaction(selection_set_ids.iter().copied(), cx) + .unwrap(); } pub fn indent_column_for_line(&self, row: u32) -> u32 { @@ -1382,20 +1386,28 @@ impl Buffer { self.deferred_ops.len() } - pub fn start_transaction(&mut self, set_id: Option) -> Result<()> { - self.start_transaction_at(set_id, Instant::now()) + pub fn start_transaction( + &mut self, + selection_set_ids: impl IntoIterator, + ) -> Result<()> { + self.start_transaction_at(selection_set_ids, Instant::now()) } - fn start_transaction_at(&mut self, set_id: Option, now: Instant) -> Result<()> { - let selections = if let Some(set_id) = set_id { - let set = self - .selections - .get(&set_id) - .ok_or_else(|| anyhow!("invalid selection set {:?}", set_id))?; - Some((set_id, set.selections.clone())) - } else { - None - }; + fn start_transaction_at( + &mut self, + selection_set_ids: impl IntoIterator, + now: Instant, + ) -> Result<()> { + let selections = selection_set_ids + .into_iter() + .map(|set_id| { + let set = self + .selections + .get(&set_id) + .expect("invalid selection set id"); + (set_id, set.selections.clone()) + }) + .collect(); self.history .start_transaction(self.version.clone(), self.is_dirty(), selections, now); Ok(()) @@ -1403,27 +1415,28 @@ impl Buffer { pub fn end_transaction( &mut self, - set_id: Option, + selection_set_ids: impl IntoIterator, cx: &mut ModelContext, ) -> Result<()> { - self.end_transaction_at(set_id, Instant::now(), cx) + self.end_transaction_at(selection_set_ids, Instant::now(), cx) } fn end_transaction_at( &mut self, - set_id: Option, + selection_set_ids: impl IntoIterator, now: Instant, cx: &mut ModelContext, ) -> Result<()> { - let selections = if let Some(set_id) = set_id { - let set = self - .selections - .get(&set_id) - .ok_or_else(|| anyhow!("invalid selection set {:?}", set_id))?; - Some((set_id, set.selections.clone())) - } else { - None - }; + let selections = selection_set_ids + .into_iter() + .map(|set_id| { + let set = self + .selections + .get(&set_id) + .expect("invalid selection set id"); + (set_id, set.selections.clone()) + }) + .collect(); if let Some(transaction) = self.history.end_transaction(selections, now) { let since = transaction.start.clone(); @@ -1544,16 +1557,17 @@ impl Buffer { }))); } - let selection_set_id = self + let selection_set_ids = self .history .undo_stack .last() .unwrap() .selections_before - .as_ref() - .map(|(set_id, _)| *set_id); + .keys() + .copied() + .collect(); self.autoindent_requests.push(Arc::new(AutoindentRequest { - selection_set_id, + selection_set_ids, before_edit, edited, inserted, @@ -1953,7 +1967,7 @@ impl Buffer { if let Some(transaction) = self.history.pop_undo().cloned() { let selections = transaction.selections_before.clone(); self.undo_or_redo(transaction, cx).unwrap(); - if let Some((set_id, selections)) = selections { + for (set_id, selections) in selections { let _ = self.update_selection_set(set_id, selections, cx); } } @@ -1972,7 +1986,7 @@ impl Buffer { if let Some(transaction) = self.history.pop_redo().cloned() { let selections = transaction.selections_after.clone(); self.undo_or_redo(transaction, cx).unwrap(); - if let Some((set_id, selections)) = selections { + for (set_id, selections) in selections { let _ = self.update_selection_set(set_id, selections, cx); } } From 5558d553bb6982e8ecc7f41d17d2872398ba24b6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 19 Oct 2021 13:17:16 +0200 Subject: [PATCH 28/28] Insert an extra newline between brackets Co-Authored-By: Antonio Scandurra Co-Authored-By: Max Brunsfeld --- crates/buffer/src/language.rs | 12 +- crates/buffer/src/lib.rs | 14 +- crates/buffer/src/rope.rs | 88 ++++++++++--- crates/editor/src/lib.rs | 178 +++++++++++++++++++++----- crates/zed/languages/rust/config.toml | 13 +- 5 files changed, 245 insertions(+), 60 deletions(-) diff --git a/crates/buffer/src/language.rs b/crates/buffer/src/language.rs index e7755070cf..2260990566 100644 --- a/crates/buffer/src/language.rs +++ b/crates/buffer/src/language.rs @@ -11,13 +11,15 @@ pub use tree_sitter::{Parser, Tree}; pub struct LanguageConfig { pub name: String, pub path_suffixes: Vec, - pub autoclose_pairs: Vec, + pub brackets: Vec, } -#[derive(Clone, Deserialize)] -pub struct AutoclosePair { +#[derive(Clone, Debug, Deserialize)] +pub struct BracketPair { pub start: String, pub end: String, + pub close: bool, + pub newline: bool, } pub struct Language { @@ -95,8 +97,8 @@ impl Language { self.config.name.as_str() } - pub fn autoclose_pairs(&self) -> &[AutoclosePair] { - &self.config.autoclose_pairs + pub fn brackets(&self) -> &[BracketPair] { + &self.config.brackets } pub fn highlight_map(&self) -> HighlightMap { diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index 19c74da9f1..3817b7131a 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -16,7 +16,7 @@ use clock::ReplicaId; use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task}; pub use highlight_map::{HighlightId, HighlightMap}; use language::Tree; -pub use language::{AutoclosePair, Language, LanguageConfig, LanguageRegistry}; +pub use language::{BracketPair, Language, LanguageConfig, LanguageRegistry}; use lazy_static::lazy_static; use operation_queue::OperationQueue; use parking_lot::Mutex; @@ -1337,6 +1337,13 @@ impl Buffer { self.content().chars_at(position) } + pub fn reversed_chars_at<'a, T: 'a + ToOffset>( + &'a self, + position: T, + ) -> impl Iterator + 'a { + self.content().reversed_chars_at(position) + } + pub fn chars_for_range(&self, range: Range) -> impl Iterator + '_ { self.text_for_range(range).flat_map(str::chars) } @@ -2794,6 +2801,11 @@ impl<'a> Content<'a> { self.visible_text.chars_at(offset) } + pub fn reversed_chars_at(&self, position: T) -> impl Iterator + 'a { + let offset = position.to_offset(self); + self.visible_text.reversed_chars_at(offset) + } + pub fn text_for_range(&self, range: Range) -> Chunks<'a> { let start = range.start.to_offset(self); let end = range.end.to_offset(self); diff --git a/crates/buffer/src/rope.rs b/crates/buffer/src/rope.rs index ebe01258dc..a1c5714002 100644 --- a/crates/buffer/src/rope.rs +++ b/crates/buffer/src/rope.rs @@ -115,6 +115,11 @@ impl Rope { self.chunks_in_range(start..self.len()).flat_map(str::chars) } + pub fn reversed_chars_at(&self, start: usize) -> impl Iterator + '_ { + self.reversed_chunks_in_range(0..start) + .flat_map(|chunk| chunk.chars().rev()) + } + pub fn bytes_at(&self, start: usize) -> impl Iterator + '_ { self.chunks_in_range(start..self.len()).flat_map(str::bytes) } @@ -123,8 +128,12 @@ impl Rope { self.chunks_in_range(0..self.len()) } - pub fn chunks_in_range<'a>(&'a self, range: Range) -> Chunks<'a> { - Chunks::new(self, range) + pub fn chunks_in_range(&self, range: Range) -> Chunks { + Chunks::new(self, range, false) + } + + pub fn reversed_chunks_in_range(&self, range: Range) -> Chunks { + Chunks::new(self, range, true) } pub fn to_point(&self, offset: usize) -> Point { @@ -284,38 +293,65 @@ impl<'a> Cursor<'a> { pub struct Chunks<'a> { chunks: sum_tree::Cursor<'a, Chunk, usize>, range: Range, + reversed: bool, } impl<'a> Chunks<'a> { - pub fn new(rope: &'a Rope, range: Range) -> Self { + pub fn new(rope: &'a Rope, range: Range, reversed: bool) -> Self { let mut chunks = rope.chunks.cursor(); - chunks.seek(&range.start, Bias::Right, &()); - Self { chunks, range } + if reversed { + chunks.seek(&range.end, Bias::Left, &()); + } else { + chunks.seek(&range.start, Bias::Right, &()); + } + Self { + chunks, + range, + reversed, + } } pub fn offset(&self) -> usize { - self.range.start.max(*self.chunks.start()) + if self.reversed { + self.range.end.min(self.chunks.end(&())) + } else { + self.range.start.max(*self.chunks.start()) + } } pub fn seek(&mut self, offset: usize) { - if offset >= self.chunks.end(&()) { - self.chunks.seek_forward(&offset, Bias::Right, &()); + let bias = if self.reversed { + Bias::Left } else { - self.chunks.seek(&offset, Bias::Right, &()); + Bias::Right + }; + + if offset >= self.chunks.end(&()) { + self.chunks.seek_forward(&offset, bias, &()); + } else { + self.chunks.seek(&offset, bias, &()); + } + + if self.reversed { + self.range.end = offset; + } else { + self.range.start = offset; } - self.range.start = offset; } pub fn peek(&self) -> Option<&'a str> { - if let Some(chunk) = self.chunks.item() { - let offset = *self.chunks.start(); - if self.range.end > offset { - let start = self.range.start.saturating_sub(*self.chunks.start()); - let end = self.range.end - self.chunks.start(); - return Some(&chunk.0[start..chunk.0.len().min(end)]); - } + let chunk = self.chunks.item()?; + if self.reversed && self.range.start >= self.chunks.end(&()) { + return None; } - None + let chunk_start = *self.chunks.start(); + if self.range.end <= chunk_start { + return None; + } + + let start = self.range.start.saturating_sub(chunk_start); + let end = self.range.end - chunk_start; + Some(&chunk.0[start..chunk.0.len().min(end)]) } } @@ -325,7 +361,11 @@ impl<'a> Iterator for Chunks<'a> { fn next(&mut self) -> Option { let result = self.peek(); if result.is_some() { - self.chunks.next(&()); + if self.reversed { + self.chunks.prev(&()); + } else { + self.chunks.next(&()); + } } result } @@ -571,6 +611,16 @@ mod tests { actual.chunks_in_range(start_ix..end_ix).collect::(), &expected[start_ix..end_ix] ); + + assert_eq!( + actual + .reversed_chunks_in_range(start_ix..end_ix) + .collect::>() + .into_iter() + .rev() + .collect::(), + &expected[start_ix..end_ix] + ); } let mut point = Point::new(0, 0); diff --git a/crates/editor/src/lib.rs b/crates/editor/src/lib.rs index 05a7569df7..a835e5e50c 100644 --- a/crates/editor/src/lib.rs +++ b/crates/editor/src/lib.rs @@ -296,7 +296,7 @@ pub struct Editor { pending_selection: Option, next_selection_id: usize, add_selections_state: Option, - autoclose_stack: Vec, + autoclose_stack: Vec, select_larger_syntax_node_stack: Vec>, scroll_position: Vector2F, scroll_top_anchor: Anchor, @@ -324,9 +324,9 @@ struct AddSelectionsState { stack: Vec, } -struct AutoclosePairState { +struct BracketPairState { ranges: SmallVec<[Range; 32]>, - pair: AutoclosePair, + pair: BracketPair, } #[derive(Serialize, Deserialize)] @@ -767,7 +767,35 @@ impl Editor { .min(start_point.column); let start = selection.start.to_offset(buffer); let end = selection.end.to_offset(buffer); - old_selections.push((selection.id, start..end, indent)); + + let mut insert_extra_newline = false; + if let Some(language) = buffer.language() { + let leading_whitespace_len = buffer + .reversed_chars_at(start) + .take_while(|c| c.is_whitespace() && *c != '\n') + .map(|c| c.len_utf8()) + .sum::(); + + let trailing_whitespace_len = buffer + .chars_at(end) + .take_while(|c| c.is_whitespace() && *c != '\n') + .map(|c| c.len_utf8()) + .sum::(); + + insert_extra_newline = language.brackets().iter().any(|pair| { + let pair_start = pair.start.trim_end(); + let pair_end = pair.end.trim_start(); + + pair.newline + && buffer.contains_str_at(end + trailing_whitespace_len, pair_end) + && buffer.contains_str_at( + (start - leading_whitespace_len).saturating_sub(pair_start.len()), + pair_start, + ) + }); + } + + old_selections.push((selection.id, start..end, indent, insert_extra_newline)); } } @@ -775,26 +803,33 @@ impl Editor { self.buffer.update(cx, |buffer, cx| { let mut delta = 0_isize; let mut pending_edit: Option = None; - for (_, range, indent) in &old_selections { - if pending_edit - .as_ref() - .map_or(false, |pending| pending.indent != *indent) - { + for (_, range, indent, insert_extra_newline) in &old_selections { + if pending_edit.as_ref().map_or(false, |pending| { + pending.indent != *indent + || pending.insert_extra_newline != *insert_extra_newline + }) { let pending = pending_edit.take().unwrap(); let mut new_text = String::with_capacity(1 + pending.indent as usize); new_text.push('\n'); new_text.extend(iter::repeat(' ').take(pending.indent as usize)); + if pending.insert_extra_newline { + new_text = new_text.repeat(2); + } buffer.edit_with_autoindent(pending.ranges, new_text, cx); delta += pending.delta; } let start = (range.start as isize + delta) as usize; let end = (range.end as isize + delta) as usize; - let text_len = *indent as usize + 1; + let mut text_len = *indent as usize + 1; + if *insert_extra_newline { + text_len *= 2; + } let pending = pending_edit.get_or_insert_with(Default::default); pending.delta += text_len as isize - (end - start) as isize; pending.indent = *indent; + pending.insert_extra_newline = *insert_extra_newline; pending.ranges.push(start..end); } @@ -802,23 +837,33 @@ impl Editor { let mut new_text = String::with_capacity(1 + pending.indent as usize); new_text.push('\n'); new_text.extend(iter::repeat(' ').take(pending.indent as usize)); + if pending.insert_extra_newline { + new_text = new_text.repeat(2); + } buffer.edit_with_autoindent(pending.ranges, new_text, cx); let mut delta = 0_isize; - new_selections.extend(old_selections.into_iter().map(|(id, range, indent)| { - let start = (range.start as isize + delta) as usize; - let end = (range.end as isize + delta) as usize; - let text_len = indent as usize + 1; - let anchor = buffer.anchor_before(start + text_len); - delta += text_len as isize - (end - start) as isize; - Selection { - id, - start: anchor.clone(), - end: anchor, - reversed: false, - goal: SelectionGoal::None, - } - })) + new_selections.extend(old_selections.into_iter().map( + |(id, range, indent, insert_extra_newline)| { + let start = (range.start as isize + delta) as usize; + let end = (range.end as isize + delta) as usize; + let text_before_cursor_len = indent as usize + 1; + let anchor = buffer.anchor_before(start + text_before_cursor_len); + let text_len = if insert_extra_newline { + text_before_cursor_len * 2 + } else { + text_before_cursor_len + }; + delta += text_len as isize - (end - start) as isize; + Selection { + id, + start: anchor.clone(), + end: anchor, + reversed: false, + goal: SelectionGoal::None, + } + }, + )) }); self.update_selections(new_selections, true, cx); @@ -827,6 +872,7 @@ impl Editor { #[derive(Default)] struct PendingEdit { indent: u32, + insert_extra_newline: bool, delta: isize, ranges: SmallVec<[Range; 32]>, } @@ -879,7 +925,7 @@ impl Editor { let new_autoclose_pair_state = self.buffer.update(cx, |buffer, cx| { let autoclose_pair = buffer.language().and_then(|language| { let first_selection_start = selections.first().unwrap().start.to_offset(&*buffer); - let pair = language.autoclose_pairs().iter().find(|pair| { + let pair = language.brackets().iter().find(|pair| { buffer.contains_str_at( first_selection_start.saturating_sub(pair.start.len()), &pair.start, @@ -914,7 +960,7 @@ impl Editor { buffer.edit(selection_ranges, &pair.end, cx); if pair.end.len() == 1 { - Some(AutoclosePairState { + Some(BracketPairState { ranges: selections .iter() .map(|selection| { @@ -4506,14 +4552,18 @@ mod tests { let settings = cx.read(EditorSettings::test); let language = Arc::new(Language::new( LanguageConfig { - autoclose_pairs: vec![ - AutoclosePair { + brackets: vec![ + BracketPair { start: "{".to_string(), end: "}".to_string(), + close: true, + newline: true, }, - AutoclosePair { + BracketPair { start: "/*".to_string(), end: " */".to_string(), + close: true, + newline: true, }, ], ..Default::default() @@ -4612,6 +4662,76 @@ mod tests { }); } + #[gpui::test] + async fn test_extra_newline_insertion(mut cx: gpui::TestAppContext) { + let settings = cx.read(EditorSettings::test); + let language = Arc::new(Language::new( + LanguageConfig { + brackets: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "/* ".to_string(), + end: " */".to_string(), + close: true, + newline: true, + }, + ], + ..Default::default() + }, + tree_sitter_rust::language(), + )); + + let text = concat!( + "{ }\n", // Suppress rustfmt + " x\n", // + " /* */\n", // + "x\n", // + "{{} }\n", // + ); + + let buffer = cx.add_model(|cx| { + let history = History::new(text.into()); + Buffer::from_history(0, history, None, Some(language), cx) + }); + let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx)); + view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing()) + .await; + + view.update(&mut cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), + DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), + ], + cx, + ) + .unwrap(); + view.newline(&Newline, cx); + + assert_eq!( + view.buffer().read(cx).text(), + concat!( + "{ \n", // Suppress rustfmt + "\n", // + "}\n", // + " x\n", // + " /* \n", // + " \n", // + " */\n", // + "x\n", // + "{{} \n", // + "}\n", // + ) + ); + }); + } + impl Editor { fn selection_ranges(&self, cx: &mut MutableAppContext) -> Vec> { self.selections_in_range( diff --git a/crates/zed/languages/rust/config.toml b/crates/zed/languages/rust/config.toml index ece9b57ca2..11b273d137 100644 --- a/crates/zed/languages/rust/config.toml +++ b/crates/zed/languages/rust/config.toml @@ -1,9 +1,10 @@ name = "Rust" path_suffixes = ["rs"] -autoclose_pairs = [ - { start = "{", end = "}" }, - { start = "[", end = "]" }, - { start = "(", end = ")" }, - { start = "\"", end = "\"" }, - { start = "/*", end = " */" }, +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = false, newline = true }, + { start = "\"", end = "\"", close = true, newline = false }, + { start = "/*", end = " */", close = true, newline = false }, ]