WIP: Determine autoindents asynchronously

Antonio Scandurra and Nathan Sobo created

We still need to insert yield points in `compute_autoindents`.

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/buffer/src/anchor.rs |   4 
crates/buffer/src/lib.rs    | 474 ++++++++++++++++++++------------------
2 files changed, 251 insertions(+), 227 deletions(-)

Detailed changes

crates/buffer/src/anchor.rs 🔗

@@ -12,18 +12,22 @@ pub struct Anchor {
     pub version: clock::Global,
 }
 
+#[derive(Clone)]
 pub struct AnchorMap<T> {
     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<T> {
     pub(crate) version: clock::Global,
     pub(crate) entries: Vec<(Range<(usize, Bias)>, T)>,
 }
 
+#[derive(Clone)]
 pub struct AnchorRangeSet(pub(crate) AnchorRangeMap<()>);
 
 impl Anchor {

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<Box<dyn File>>,
     language: Option<Arc<Language>>,
-    autoindent_requests: VecDeque<AutoindentRequest>,
+    autoindent_requests: Vec<Arc<AutoindentRequest>>,
+    pending_autoindent: Option<Task<()>>,
     sync_parse_timeout: Duration,
     syntax_tree: Mutex<Option<SyntaxTree>>,
     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<AnchorRangeSet>,
 }
 
-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<Language>,
         cx: &mut ModelContext<Self>,
     ) {
-        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<Language>,
-        cx: &mut ModelContext<Self>,
-    ) {
-        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<Self>) {
+        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();
-
-            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::<BTreeMap<u32, u32>>();
+    fn compute_autoindents(&self) -> Option<impl Future<Output = BTreeMap<u32, u32>>> {
+        let snapshot = self.snapshot();
+        if snapshot.language.is_none()
+            || snapshot.tree.is_none()
+            || self.autoindent_requests.is_empty()
+        {
+            return None;
+        }
 
-            let mut old_suggestions = HashMap::default();
-            if let Some(old_tree) = &request.before_edit.tree {
+        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::<BTreeMap<u32, u32>>();
+
+                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::<Vec<_>>();
-                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);
+                // 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 = 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 {
-                let inserted_row_ranges = contiguous_ranges(
-                    inserted
-                        .to_point_ranges(self.content())
-                        .flat_map(|range| range.start.row..range.end.row + 1),
-                )
-                .collect::<Vec<_>>();
-                for inserted_row_range in inserted_row_ranges {
-                    let suggestions = self
-                        .content()
-                        .suggest_autoindents(
-                            inserted_row_range.clone(),
-                            &new_tree,
-                            &language,
-                            &mut cursor,
-                        )
-                        .collect::<Vec<_>>();
-
-                    for (row, suggestion) in inserted_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);
+                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<Self>,
     ) -> 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<u32>,
+    ) -> Option<impl Iterator<Item = IndentSuggestion> + '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<Point>, &'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<Point> = None;
+                let mut end: Option<Point> = 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<u32> {
+        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<usize>) -> Chunks {
+    pub fn text_for_range<T: ToOffset>(&self, range: Range<T>) -> 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<usize>) -> HighlightedChunks {
+    pub fn highlighted_text_for_range<T: ToOffset>(
+        &mut self,
+        range: Range<T>,
+    ) -> 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<u32> {
-        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<u32>,
-        tree: &Tree,
-        language: &Language,
-        cursor: &mut QueryCursor,
-    ) -> impl Iterator<Item = IndentSuggestion> + '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<Point>, &'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<Point> = None;
-            let mut end: Option<Point> = 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)]