Restructure autoclosing to account for multi-language documents

Max Brunsfeld created

Change summary

crates/editor/src/editor.rs               | 570 ++++++++++++------------
crates/zed/src/languages/html/config.toml |   2 
2 files changed, 280 insertions(+), 292 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -410,7 +410,7 @@ pub struct Editor {
     add_selections_state: Option<AddSelectionsState>,
     select_next_state: Option<SelectNextState>,
     selection_history: SelectionHistory,
-    autoclose_stack: InvalidationStack<BracketPairState>,
+    autoclose_regions: Vec<AutocloseRegion>,
     snippet_stack: InvalidationStack<SnippetState>,
     select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
     ime_transaction: Option<TransactionId>,
@@ -569,8 +569,9 @@ struct SelectNextState {
     done: bool,
 }
 
-struct BracketPairState {
-    ranges: Vec<Range<Anchor>>,
+struct AutocloseRegion {
+    selection_id: usize,
+    range: Range<Anchor>,
     pair: BracketPair,
 }
 
@@ -1010,7 +1011,7 @@ impl Editor {
             add_selections_state: None,
             select_next_state: None,
             selection_history: Default::default(),
-            autoclose_stack: Default::default(),
+            autoclose_regions: Default::default(),
             snippet_stack: Default::default(),
             select_larger_syntax_node_stack: Vec::new(),
             ime_transaction: Default::default(),
@@ -1401,8 +1402,7 @@ impl Editor {
         self.add_selections_state = None;
         self.select_next_state = None;
         self.select_larger_syntax_node_stack.clear();
-        self.autoclose_stack
-            .invalidate(&self.selections.disjoint_anchors(), buffer);
+        self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer);
         self.snippet_stack
             .invalidate(&self.selections.disjoint_anchors(), buffer);
         self.take_rename(false, cx);
@@ -1849,15 +1849,158 @@ impl Editor {
             return;
         }
 
-        if !self.skip_autoclose_end(text, cx) {
-            self.transact(cx, |this, cx| {
-                if !this.surround_with_bracket_pair(text, cx) {
-                    this.insert(text, cx);
-                    this.autoclose_bracket_pairs(cx);
+        let text: Arc<str> = text.into();
+        let selections = self.selections.all_adjusted(cx);
+        let mut edits = Vec::new();
+        let mut new_selections = Vec::with_capacity(selections.len());
+        let mut new_autoclose_regions = Vec::new();
+        let snapshot = self.buffer.read(cx).read(cx);
+
+        for (selection, autoclose_region) in
+            self.selections_with_autoclose_regions(selections, &snapshot)
+        {
+            if let Some(language) = snapshot.language_at(selection.head()) {
+                // Determine if the inserted text matches the opening or closing
+                // bracket of any of this language's bracket pairs.
+                let mut bracket_pair = None;
+                let mut is_bracket_pair_start = false;
+                for pair in language.brackets() {
+                    if pair.start.ends_with(text.as_ref()) {
+                        bracket_pair = Some(pair.clone());
+                        is_bracket_pair_start = true;
+                        break;
+                    } else if pair.end.as_str() == text.as_ref() {
+                        bracket_pair = Some(pair.clone());
+                        break;
+                    }
                 }
-            });
-            self.trigger_completion_on_input(text, cx);
+
+                if let Some(bracket_pair) = bracket_pair {
+                    if selection.is_empty() {
+                        if is_bracket_pair_start {
+                            let prefix_len = bracket_pair.start.len() - text.len();
+
+                            // If the inserted text is a suffix of an opening bracket and the
+                            // selection is preceded by the rest of the opening bracket, then
+                            // insert the closing bracket.
+                            let should_autoclose = selection.start.column > (prefix_len as u32)
+                                && snapshot.contains_str_at(
+                                    Point::new(
+                                        selection.start.row,
+                                        selection.start.column - (prefix_len as u32),
+                                    ),
+                                    &bracket_pair.start[..prefix_len],
+                                )
+                                && snapshot
+                                    .chars_at(selection.start)
+                                    .next()
+                                    .map_or(true, |c| language.should_autoclose_before(c));
+                            if should_autoclose {
+                                let anchor = snapshot.anchor_before(selection.end);
+                                new_selections
+                                    .push((selection.map(|_| anchor.clone()), text.len()));
+                                new_autoclose_regions.push((
+                                    anchor.clone(),
+                                    text.len(),
+                                    selection.id,
+                                    bracket_pair.clone(),
+                                ));
+                                edits.push((
+                                    selection.range(),
+                                    format!("{}{}", text, bracket_pair.end).into(),
+                                ));
+                                continue;
+                            }
+                        } else if let Some(region) = autoclose_region {
+                            // If the selection is followed by an auto-inserted closing bracket,
+                            // then don't insert anything else; just move the selection past the
+                            // closing bracket.
+                            let should_skip = selection.end == region.range.end.to_point(&snapshot);
+                            if should_skip {
+                                let anchor = snapshot.anchor_after(selection.end);
+                                new_selections.push((
+                                    selection.map(|_| anchor.clone()),
+                                    region.pair.end.len(),
+                                ));
+                                continue;
+                            }
+                        }
+                    }
+                    // If an opening bracket is typed while text is selected, then
+                    // surround that text with the bracket pair.
+                    else if is_bracket_pair_start {
+                        edits.push((selection.start..selection.start, text.clone()));
+                        edits.push((
+                            selection.end..selection.end,
+                            bracket_pair.end.as_str().into(),
+                        ));
+                        new_selections.push((
+                            Selection {
+                                id: selection.id,
+                                start: snapshot.anchor_after(selection.start),
+                                end: snapshot.anchor_before(selection.end),
+                                reversed: selection.reversed,
+                                goal: selection.goal,
+                            },
+                            0,
+                        ));
+                        continue;
+                    }
+                }
+            }
+
+            // If not handling any auto-close operation, then just replace the selected
+            // text with the given input and move the selection to the end of the
+            // newly inserted text.
+            let anchor = snapshot.anchor_after(selection.end);
+            new_selections.push((selection.map(|_| anchor.clone()), 0));
+            edits.push((selection.start..selection.end, text.clone()));
         }
+
+        drop(snapshot);
+        self.transact(cx, |this, cx| {
+            this.buffer.update(cx, |buffer, cx| {
+                buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
+            });
+
+            let new_anchor_selections = new_selections.iter().map(|e| &e.0);
+            let new_selection_deltas = new_selections.iter().map(|e| e.1);
+            let snapshot = this.buffer.read(cx).read(cx);
+            let new_selections = resolve_multiple::<usize, _>(new_anchor_selections, &snapshot)
+                .zip(new_selection_deltas)
+                .map(|(selection, delta)| selection.map(|e| e + delta))
+                .collect::<Vec<_>>();
+
+            let mut i = 0;
+            for (position, delta, selection_id, pair) in new_autoclose_regions {
+                let position = position.to_offset(&snapshot) + delta;
+                let start = snapshot.anchor_before(position);
+                let end = snapshot.anchor_after(position);
+                while let Some(existing_state) = this.autoclose_regions.get(i) {
+                    match existing_state.range.start.cmp(&start, &snapshot) {
+                        Ordering::Less => i += 1,
+                        Ordering::Greater => break,
+                        Ordering::Equal => match end.cmp(&existing_state.range.end, &snapshot) {
+                            Ordering::Less => i += 1,
+                            Ordering::Equal => break,
+                            Ordering::Greater => break,
+                        },
+                    }
+                }
+                this.autoclose_regions.insert(
+                    i,
+                    AutocloseRegion {
+                        selection_id,
+                        range: start..end,
+                        pair,
+                    },
+                );
+            }
+
+            drop(snapshot);
+            this.change_selections(None, cx, |s| s.select(new_selections));
+            this.trigger_completion_on_input(&text, cx);
+        });
     }
 
     pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext<Self>) {
@@ -2029,232 +2172,89 @@ impl Editor {
         }
     }
 
-    fn surround_with_bracket_pair(&mut self, text: &str, cx: &mut ViewContext<Self>) -> bool {
-        let snapshot = self.buffer.read(cx).snapshot(cx);
-        if let Some(pair) = snapshot
-            .language()
-            .and_then(|language| language.brackets().iter().find(|b| b.start == text))
-            .cloned()
-        {
-            if self
-                .selections
-                .all::<usize>(cx)
-                .iter()
-                .any(|selection| selection.is_empty())
-            {
-                return false;
-            }
-
-            let mut selections = self.selections.disjoint_anchors().to_vec();
-            for selection in &mut selections {
-                selection.end = selection.end.bias_left(&snapshot);
-            }
-            drop(snapshot);
-
-            self.buffer.update(cx, |buffer, cx| {
-                let pair_start: Arc<str> = pair.start.clone().into();
-                let pair_end: Arc<str> = pair.end.clone().into();
-                buffer.edit(
-                    selections.iter().flat_map(|s| {
-                        [
-                            (s.start.clone()..s.start.clone(), pair_start.clone()),
-                            (s.end.clone()..s.end.clone(), pair_end.clone()),
-                        ]
-                    }),
-                    None,
-                    cx,
-                );
-            });
-
-            let snapshot = self.buffer.read(cx).read(cx);
-            for selection in &mut selections {
-                selection.end = selection.end.bias_right(&snapshot);
-            }
-            drop(snapshot);
-
-            self.change_selections(None, cx, |s| s.select_anchors(selections));
-            true
-        } else {
-            false
-        }
-    }
-
-    fn autoclose_bracket_pairs(&mut self, cx: &mut ViewContext<Self>) {
+    /// If any empty selections is touching the start of its innermost containing autoclose
+    /// region, expand it to select the brackets.
+    fn select_autoclose_pair(&mut self, cx: &mut ViewContext<Self>) {
         let selections = self.selections.all::<usize>(cx);
-        let mut bracket_pair_state = None;
-        let mut new_selections = None;
-        self.buffer.update(cx, |buffer, cx| {
-            let mut snapshot = buffer.snapshot(cx);
-            let left_biased_selections = selections
-                .iter()
-                .map(|selection| selection.map(|p| snapshot.anchor_before(p)))
-                .collect::<Vec<_>>();
-
-            let autoclose_pair = snapshot.language().and_then(|language| {
-                let first_selection_start = selections.first().unwrap().start;
-                let pair = language.brackets().iter().find(|pair| {
-                    pair.close
-                        && snapshot.contains_str_at(
-                            first_selection_start.saturating_sub(pair.start.len()),
-                            &pair.start,
-                        )
-                });
-                pair.and_then(|pair| {
-                    let should_autoclose = selections.iter().all(|selection| {
-                        // Ensure all selections are parked at the end of a pair start.
-                        if snapshot.contains_str_at(
-                            selection.start.saturating_sub(pair.start.len()),
-                            &pair.start,
-                        ) {
-                            snapshot
-                                .chars_at(selection.start)
-                                .next()
-                                .map_or(true, |c| language.should_autoclose_before(c))
-                        } else {
-                            false
+        let buffer = self.buffer.read(cx).read(cx);
+        let mut new_selections = Vec::new();
+        for (mut selection, region) in self.selections_with_autoclose_regions(selections, &buffer) {
+            if let (Some(region), true) = (region, selection.is_empty()) {
+                let mut range = region.range.to_offset(&buffer);
+                if selection.start == range.start {
+                    if range.start >= region.pair.start.len() {
+                        range.start -= region.pair.start.len();
+                        if buffer.contains_str_at(range.start, &region.pair.start) {
+                            if buffer.contains_str_at(range.end, &region.pair.end) {
+                                range.end += region.pair.end.len();
+                                selection.start = range.start;
+                                selection.end = range.end;
+                            }
                         }
-                    });
-
-                    if should_autoclose {
-                        Some(pair.clone())
-                    } else {
-                        None
                     }
-                })
-            });
-
-            if let Some(pair) = autoclose_pair {
-                let selection_ranges = selections
-                    .iter()
-                    .map(|selection| {
-                        let start = selection.start.to_offset(&snapshot);
-                        start..start
-                    })
-                    .collect::<SmallVec<[_; 32]>>();
-
-                let pair_end: Arc<str> = pair.end.clone().into();
-                buffer.edit(
-                    selection_ranges
-                        .iter()
-                        .map(|range| (range.clone(), pair_end.clone())),
-                    None,
-                    cx,
-                );
-                snapshot = buffer.snapshot(cx);
-
-                new_selections = Some(
-                    resolve_multiple::<usize, _>(left_biased_selections.iter(), &snapshot)
-                        .collect::<Vec<_>>(),
-                );
-
-                if pair.end.len() == 1 {
-                    let mut delta = 0;
-                    bracket_pair_state = Some(BracketPairState {
-                        ranges: selections
-                            .iter()
-                            .map(move |selection| {
-                                let offset = selection.start + delta;
-                                delta += 1;
-                                snapshot.anchor_before(offset)..snapshot.anchor_after(offset)
-                            })
-                            .collect(),
-                        pair,
-                    });
                 }
             }
-        });
-
-        if let Some(new_selections) = new_selections {
-            self.change_selections(None, cx, |s| {
-                s.select(new_selections);
-            });
-        }
-        if let Some(bracket_pair_state) = bracket_pair_state {
-            self.autoclose_stack.push(bracket_pair_state);
-        }
-    }
-
-    fn skip_autoclose_end(&mut self, text: &str, cx: &mut ViewContext<Self>) -> bool {
-        let buffer = self.buffer.read(cx).snapshot(cx);
-        let old_selections = self.selections.all::<usize>(cx);
-        let autoclose_pair = if let Some(autoclose_pair) = self.autoclose_stack.last() {
-            autoclose_pair
-        } else {
-            return false;
-        };
-        if text != autoclose_pair.pair.end {
-            return false;
+            new_selections.push(selection);
         }
 
-        debug_assert_eq!(old_selections.len(), autoclose_pair.ranges.len());
-
-        if old_selections
-            .iter()
-            .zip(autoclose_pair.ranges.iter().map(|r| r.to_offset(&buffer)))
-            .all(|(selection, autoclose_range)| {
-                let autoclose_range_end = autoclose_range.end.to_offset(&buffer);
-                selection.is_empty() && selection.start == autoclose_range_end
-            })
-        {
-            let new_selections = old_selections
-                .into_iter()
-                .map(|selection| {
-                    let cursor = selection.start + 1;
-                    Selection {
-                        id: selection.id,
-                        start: cursor,
-                        end: cursor,
-                        reversed: false,
-                        goal: SelectionGoal::None,
-                    }
-                })
-                .collect();
-            self.autoclose_stack.pop();
-            self.change_selections(Some(Autoscroll::Fit), cx, |s| {
-                s.select(new_selections);
-            });
-            true
-        } else {
-            false
-        }
+        drop(buffer);
+        self.change_selections(None, cx, |selections| selections.select(new_selections));
     }
 
-    fn select_autoclose_pair(&mut self, cx: &mut ViewContext<Self>) -> bool {
-        let buffer = self.buffer.read(cx).snapshot(cx);
-        let old_selections = self.selections.all::<usize>(cx);
-        let autoclose_pair = if let Some(autoclose_pair) = self.autoclose_stack.last() {
-            autoclose_pair
-        } else {
-            return false;
-        };
+    /// Iterate the given selections, and for each one, find the smallest surrounding
+    /// autoclose region. This uses the ordering of the selections and the autoclose
+    /// regions to avoid repeated comparisons.
+    fn selections_with_autoclose_regions<'a, D: ToOffset + Clone>(
+        &'a self,
+        selections: impl IntoIterator<Item = Selection<D>>,
+        buffer: &'a MultiBufferSnapshot,
+    ) -> impl Iterator<Item = (Selection<D>, Option<&'a AutocloseRegion>)> {
+        let mut i = 0;
+        let mut pair_states = self.autoclose_regions.as_slice();
+        selections.into_iter().map(move |selection| {
+            let range = selection.start.to_offset(buffer)..selection.end.to_offset(buffer);
+
+            let mut enclosing = None;
+            while let Some(pair_state) = pair_states.get(i) {
+                if pair_state.range.end.to_offset(buffer) < range.start {
+                    pair_states = &pair_states[i + 1..];
+                    i = 0;
+                } else if pair_state.range.start.to_offset(buffer) > range.end {
+                    break;
+                } else if pair_state.selection_id == selection.id {
+                    enclosing = Some(pair_state);
+                    i += 1;
+                }
+            }
 
-        debug_assert_eq!(old_selections.len(), autoclose_pair.ranges.len());
+            (selection.clone(), enclosing)
+        })
+    }
 
-        let mut new_selections = Vec::new();
-        for (selection, autoclose_range) in old_selections
-            .iter()
-            .zip(autoclose_pair.ranges.iter().map(|r| r.to_offset(&buffer)))
-        {
-            if selection.is_empty()
-                && autoclose_range.is_empty()
-                && selection.start == autoclose_range.start
-            {
-                new_selections.push(Selection {
-                    id: selection.id,
-                    start: selection.start - autoclose_pair.pair.start.len(),
-                    end: selection.end + autoclose_pair.pair.end.len(),
-                    reversed: true,
-                    goal: selection.goal,
-                });
-            } else {
-                return false;
+    /// Remove any autoclose regions that no longer contain their selection.
+    fn invalidate_autoclose_regions(
+        &mut self,
+        mut selections: &[Selection<Anchor>],
+        buffer: &MultiBufferSnapshot,
+    ) {
+        self.autoclose_regions.retain(|state| {
+            let mut i = 0;
+            while let Some(selection) = selections.get(i) {
+                if selection.end.cmp(&state.range.start, buffer).is_lt() {
+                    selections = &selections[1..];
+                    continue;
+                }
+                if selection.start.cmp(&state.range.end, buffer).is_gt() {
+                    break;
+                }
+                if selection.id == state.selection_id {
+                    return true;
+                } else {
+                    i += 1;
+                }
             }
-        }
-
-        self.change_selections(Some(Autoscroll::Fit), cx, |selections| {
-            selections.select(new_selections)
+            false
         });
-        true
     }
 
     fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
@@ -2909,51 +2909,47 @@ impl Editor {
 
     pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
         self.transact(cx, |this, cx| {
-            if !this.select_autoclose_pair(cx) {
-                let mut selections = this.selections.all::<Point>(cx);
-                if !this.selections.line_mode {
-                    let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx));
-                    for selection in &mut selections {
-                        if selection.is_empty() {
-                            let old_head = selection.head();
-                            let mut new_head = movement::left(
-                                &display_map,
-                                old_head.to_display_point(&display_map),
-                            )
-                            .to_point(&display_map);
-                            if let Some((buffer, line_buffer_range)) = display_map
-                                .buffer_snapshot
-                                .buffer_line_for_row(old_head.row)
-                            {
-                                let indent_size =
-                                    buffer.indent_size_for_line(line_buffer_range.start.row);
-                                let language_name =
-                                    buffer.language().map(|language| language.name());
-                                let indent_len = match indent_size.kind {
-                                    IndentKind::Space => {
-                                        cx.global::<Settings>().tab_size(language_name.as_deref())
-                                    }
-                                    IndentKind::Tab => NonZeroU32::new(1).unwrap(),
-                                };
-                                if old_head.column <= indent_size.len && old_head.column > 0 {
-                                    let indent_len = indent_len.get();
-                                    new_head = cmp::min(
-                                        new_head,
-                                        Point::new(
-                                            old_head.row,
-                                            ((old_head.column - 1) / indent_len) * indent_len,
-                                        ),
-                                    );
+            this.select_autoclose_pair(cx);
+            let mut selections = this.selections.all::<Point>(cx);
+            if !this.selections.line_mode {
+                let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx));
+                for selection in &mut selections {
+                    if selection.is_empty() {
+                        let old_head = selection.head();
+                        let mut new_head =
+                            movement::left(&display_map, old_head.to_display_point(&display_map))
+                                .to_point(&display_map);
+                        if let Some((buffer, line_buffer_range)) = display_map
+                            .buffer_snapshot
+                            .buffer_line_for_row(old_head.row)
+                        {
+                            let indent_size =
+                                buffer.indent_size_for_line(line_buffer_range.start.row);
+                            let language_name = buffer.language().map(|language| language.name());
+                            let indent_len = match indent_size.kind {
+                                IndentKind::Space => {
+                                    cx.global::<Settings>().tab_size(language_name.as_deref())
                                 }
+                                IndentKind::Tab => NonZeroU32::new(1).unwrap(),
+                            };
+                            if old_head.column <= indent_size.len && old_head.column > 0 {
+                                let indent_len = indent_len.get();
+                                new_head = cmp::min(
+                                    new_head,
+                                    Point::new(
+                                        old_head.row,
+                                        ((old_head.column - 1) / indent_len) * indent_len,
+                                    ),
+                                );
                             }
-
-                            selection.set_head(new_head, SelectionGoal::None);
                         }
+
+                        selection.set_head(new_head, SelectionGoal::None);
                     }
                 }
-
-                this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
             }
+
+            this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
             this.insert("", cx);
         });
     }
@@ -3957,17 +3953,16 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) {
         self.transact(cx, |this, cx| {
-            if !this.select_autoclose_pair(cx) {
-                this.change_selections(Some(Autoscroll::Fit), cx, |s| {
-                    let line_mode = s.line_mode;
-                    s.move_with(|map, selection| {
-                        if selection.is_empty() && !line_mode {
-                            let cursor = movement::previous_word_start(map, selection.head());
-                            selection.set_head(cursor, SelectionGoal::None);
-                        }
-                    });
+            this.select_autoclose_pair(cx);
+            this.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                let line_mode = s.line_mode;
+                s.move_with(|map, selection| {
+                    if selection.is_empty() && !line_mode {
+                        let cursor = movement::previous_word_start(map, selection.head());
+                        selection.set_head(cursor, SelectionGoal::None);
+                    }
                 });
-            }
+            });
             this.insert("", cx);
         });
     }
@@ -3978,17 +3973,16 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) {
         self.transact(cx, |this, cx| {
-            if !this.select_autoclose_pair(cx) {
-                this.change_selections(Some(Autoscroll::Fit), cx, |s| {
-                    let line_mode = s.line_mode;
-                    s.move_with(|map, selection| {
-                        if selection.is_empty() && !line_mode {
-                            let cursor = movement::previous_subword_start(map, selection.head());
-                            selection.set_head(cursor, SelectionGoal::None);
-                        }
-                    });
+            this.select_autoclose_pair(cx);
+            this.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                let line_mode = s.line_mode;
+                s.move_with(|map, selection| {
+                    if selection.is_empty() && !line_mode {
+                        let cursor = movement::previous_subword_start(map, selection.head());
+                        selection.set_head(cursor, SelectionGoal::None);
+                    }
                 });
-            }
+            });
             this.insert("", cx);
         });
     }
@@ -6495,12 +6489,6 @@ impl<T> DerefMut for InvalidationStack<T> {
     }
 }
 
-impl InvalidationRegion for BracketPairState {
-    fn ranges(&self) -> &[Range<Anchor>] {
-        &self.ranges
-    }
-}
-
 impl InvalidationRegion for SnippetState {
     fn ranges(&self) -> &[Range<Anchor>] {
         &self.ranges[self.active_index]

crates/zed/src/languages/html/config.toml 🔗

@@ -1,6 +1,6 @@
 name = "HTML"
 path_suffixes = ["html"]
-autoclose_before = ">"
+autoclose_before = ">})"
 brackets = [
     { start = "<", end = ">", close = true, newline = true },
     { start = "{", end = "}", close = true, newline = true },