Start on new way of comparing old and new indent suggestions

Max Brunsfeld and Nathan Sobo created

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

Change summary

crates/buffer/src/anchor.rs |  70 ++++
crates/buffer/src/lib.rs    | 619 +++++++++++++++++++++++++-------------
crates/buffer/src/rope.rs   |   1 
3 files changed, 473 insertions(+), 217 deletions(-)

Detailed changes

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<T> {
+    pub(crate) version: clock::Global,
+    pub(crate) entries: Vec<((usize, Bias), T)>,
+}
+
+pub struct AnchorSet(pub(crate) AnchorMap<()>);
+
+pub struct AnchorRangeMap<T> {
+    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<T> AnchorMap<T> {
+    pub fn to_points<'a>(
+        &'a self,
+        content: impl Into<Content<'a>> + 'a,
+    ) -> impl Iterator<Item = (Point, &'a T)> + '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<Content<'a>> + 'a,
+    ) -> impl Iterator<Item = Point> + 'a {
+        self.0.to_points(content).map(move |(point, _)| point)
+    }
+}
+
+impl<T> AnchorRangeMap<T> {
+    pub fn to_point_ranges<'a>(
+        &'a self,
+        content: impl Into<Content<'a>> + 'a,
+    ) -> impl Iterator<Item = (Range<Point>, &'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<Content<'a>> + 'a,
+    ) -> impl Iterator<Item = Range<Point>> + '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<Anchor>, buffer: impl Into<Content<'a>>) -> Result<Ordering>;
 }

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<Box<dyn File>>,
     language: Option<Arc<Language>>,
-    autoindent_requests: Vec<AutoindentRequest>,
+    autoindent_requests: VecDeque<AutoindentRequest>,
     sync_parse_timeout: Duration,
     syntax_tree: Mutex<Option<SyntaxTree>>,
     parsing_in_background: bool,
@@ -195,11 +194,16 @@ struct SyntaxTree {
     version: clock::Global,
 }
 
-#[derive(Clone, Debug)]
 struct AutoindentRequest {
-    position: Anchor,
-    end_position: Option<Anchor>,
-    prev_suggestion: Option<u32>,
+    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<Language>,
         cx: &mut ModelContext<Self>,
     ) {
-        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<Language>,
         cx: &mut ModelContext<Self>,
     ) {
-        let mut autoindent_requests = mem::take(&mut self.autoindent_requests);
-        let mut prev_suggestions_by_row = BTreeMap::<u32, Option<u32>>::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::<Vec<_>>();
-            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<u32>,
-        tree: &Tree,
-        language: &Language,
-        cursor: &mut QueryCursor,
-    ) -> impl Iterator<Item = u32> + '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());
-                }
+        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 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 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>>();
+
+            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));
 
-            eprintln!(
-                "autoindent row: {:?}, prev_indent_column: {:?}",
-                row, prev_indent_column
-            );
+            // 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 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::<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);
+                    }
                 }
             }
-
-            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<u32> {
-        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<Self>) {
@@ -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<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
         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<T>) -> 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<Item = char> + '_ {
         self.chars_at(0)
     }
 
-    pub fn chars_at<T: ToOffset>(&self, position: T) -> impl Iterator<Item = char> + '_ {
-        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<Item = char> + 'a {
+        self.content().chars_at(position)
     }
 
     pub fn chars_for_range<T: ToOffset>(&self, range: Range<T>) -> impl Iterator<Item = char> + '_ {
@@ -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::<Vec<_>>();
-                    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::<FragmentTextSummary>();
         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::<usize>(&None)
     }
 
+    pub fn chars_at<T: ToOffset>(&self, position: T) -> impl Iterator<Item = char> + 'a {
+        let offset = position.to_offset(self);
+        self.visible_text.chars_at(offset)
+    }
+
+    pub fn text_for_range<T: ToOffset>(&self, range: Range<T>) -> 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<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)>();
@@ -2670,19 +2619,134 @@ impl<'a> Content<'a> {
         self.visible_text.cursor(range.start).summary(range.end)
     }
 
+    fn summaries_for_anchors<T>(
+        &self,
+        map: &'a AnchorMap<T>,
+    ) -> impl Iterator<Item = (TextSummary, &'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 |((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<T>(
+        &self,
+        map: &'a AnchorRangeMap<T>,
+    ) -> impl Iterator<Item = (Range<TextSummary>, &'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<T: ToOffset>(&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::<FragmentTextSummary>();
         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<T, E>(&self, entries: E) -> AnchorMap<T>
+    where
+        E: IntoIterator<Item = ((usize, Bias), T)>,
+    {
+        let version = self.version.clone();
+        let mut cursor = self.fragments.cursor::<FragmentTextSummary>();
+        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<T, E>(&self, entries: E) -> AnchorRangeMap<T>
+    where
+        E: IntoIterator<Item = (Range<(usize, Bias)>, T)>,
+    {
+        let version = self.version.clone();
+        let mut cursor = self.fragments.cursor::<FragmentTextSummary>();
+        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<E>(&self, entries: E) -> AnchorSet
+    where
+        E: IntoIterator<Item = (usize, Bias)>,
+    {
+        AnchorSet(self.anchor_map(entries.into_iter().map(|range| (range, ()))))
+    }
+
+    pub fn anchor_range_set<E>(&self, entries: E) -> AnchorRangeSet
+    where
+        E: IntoIterator<Item = Range<(usize, Bias)>>,
+    {
+        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<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
+        })
+    }
+}
+
+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<clock::Global>,
+    ) -> cmp::Ordering {
+        Ord::cmp(self, &cursor_location.visible)
+    }
+}
+
 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
 enum VersionedOffset {
     Offset(usize),