Start on autoclosing pairs

Antonio Scandurra and Nathan Sobo created

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

Change summary

crates/buffer/src/language.rs         |  11 ++
crates/buffer/src/lib.rs              |  20 ++++
crates/buffer/src/rope.rs             |   4 
crates/editor/src/lib.rs              | 136 +++++++++++++++++++++++++++++
crates/workspace/src/lib.rs           |   1 
crates/zed/languages/rust/config.toml |   5 
6 files changed, 174 insertions(+), 3 deletions(-)

Detailed changes

crates/buffer/src/language.rs 🔗

@@ -11,6 +11,13 @@ pub use tree_sitter::{Parser, Tree};
 pub struct LanguageConfig {
     pub name: String,
     pub path_suffixes: Vec<String>,
+    pub autoclose_pairs: Vec<AutoclosePair>,
+}
+
+#[derive(Clone, Deserialize)]
+pub struct AutoclosePair {
+    pub start: String,
+    pub end: String,
 }
 
 pub struct Language {
@@ -81,6 +88,10 @@ impl Language {
         self.config.name.as_str()
     }
 
+    pub fn autoclose_pairs(&self) -> &[AutoclosePair] {
+        &self.config.autoclose_pairs
+    }
+
     pub fn highlight_map(&self) -> HighlightMap {
         self.highlight_map.lock().clone()
     }

crates/buffer/src/lib.rs 🔗

@@ -14,7 +14,7 @@ use clock::ReplicaId;
 use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task};
 pub use highlight_map::{HighlightId, HighlightMap};
 use language::Tree;
-pub use language::{Language, LanguageConfig, LanguageRegistry};
+pub use language::{AutoclosePair, Language, LanguageConfig, LanguageRegistry};
 use lazy_static::lazy_static;
 use operation_queue::OperationQueue;
 use parking_lot::Mutex;
@@ -1110,6 +1110,23 @@ impl Buffer {
         self.visible_text.chars_at(offset)
     }
 
+    pub fn bytes_at<T: ToOffset>(&self, position: T) -> impl Iterator<Item = u8> + '_ {
+        let offset = position.to_offset(self);
+        self.visible_text.bytes_at(offset)
+    }
+
+    pub fn contains_str_at<T>(&self, position: T, needle: &str) -> bool
+    where
+        T: ToOffset,
+    {
+        let position = position.to_offset(self);
+        position == self.clip_offset(position, Bias::Left)
+            && self
+                .bytes_at(position)
+                .take(needle.len())
+                .eq(needle.bytes())
+    }
+
     pub fn edits_since<'a>(&'a self, since: clock::Global) -> impl 'a + Iterator<Item = Edit> {
         let since_2 = since.clone();
         let cursor = if since == self.version {
@@ -4083,6 +4100,7 @@ mod tests {
                 LanguageConfig {
                     name: "Rust".to_string(),
                     path_suffixes: vec!["rs".to_string()],
+                    ..Default::default()
                 },
                 tree_sitter_rust::language(),
             )

crates/buffer/src/rope.rs 🔗

@@ -115,6 +115,10 @@ impl Rope {
         self.chunks_in_range(start..self.len()).flat_map(str::chars)
     }
 
+    pub fn bytes_at(&self, start: usize) -> impl Iterator<Item = u8> + '_ {
+        self.chunks_in_range(start..self.len()).flat_map(str::bytes)
+    }
+
     pub fn chunks<'a>(&'a self) -> Chunks<'a> {
         self.chunks_in_range(0..self.len())
     }

crates/editor/src/lib.rs 🔗

@@ -772,9 +772,51 @@ impl Editor {
         });
 
         self.update_selections(new_selections, true, cx);
+        self.autoclose_pairs(cx);
         self.end_transaction(cx);
     }
 
+    fn autoclose_pairs(&mut self, cx: &mut ViewContext<Self>) {
+        let selections = self.selections(cx);
+        self.buffer.update(cx, |buffer, cx| {
+            let autoclose_pair = buffer.language().and_then(|language| {
+                let first_selection_start = selections.first().unwrap().start.to_offset(&*buffer);
+                let pair = language.autoclose_pairs().iter().find(|pair| {
+                    buffer.contains_str_at(
+                        first_selection_start.saturating_sub(pair.start.len()),
+                        &pair.start,
+                    )
+                });
+                pair.and_then(|pair| {
+                    let should_autoclose = selections[1..].iter().all(|selection| {
+                        let selection_start = selection.start.to_offset(&*buffer);
+                        buffer.contains_str_at(
+                            selection_start.saturating_sub(pair.start.len()),
+                            &pair.start,
+                        )
+                    });
+
+                    if should_autoclose {
+                        Some(pair.clone())
+                    } else {
+                        None
+                    }
+                })
+            });
+
+            if let Some(pair) = autoclose_pair {
+                let mut selection_ranges = SmallVec::<[_; 32]>::new();
+                for selection in selections.as_ref() {
+                    let start = selection.start.to_offset(&*buffer);
+                    let end = selection.end.to_offset(&*buffer);
+                    selection_ranges.push(start..end);
+                }
+
+                buffer.edit(selection_ranges, &pair.end, cx);
+            }
+        });
+    }
+
     pub fn clear(&mut self, cx: &mut ViewContext<Self>) {
         self.start_transaction(cx);
         self.select_all(&SelectAll, cx);
@@ -4209,6 +4251,100 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_autoclose_pairs(mut cx: gpui::TestAppContext) {
+        let settings = cx.read(EditorSettings::test);
+        let language = Arc::new(Language::new(
+            LanguageConfig {
+                autoclose_pairs: vec![
+                    AutoclosePair {
+                        start: "{".to_string(),
+                        end: "}".to_string(),
+                    },
+                    AutoclosePair {
+                        start: "/*".to_string(),
+                        end: " */".to_string(),
+                    },
+                ],
+                ..Default::default()
+            },
+            tree_sitter_rust::language(),
+        ));
+
+        let text = r#"
+            a
+
+            /
+
+        "#
+        .unindent();
+
+        let buffer = cx.add_model(|cx| {
+            let history = History::new(text.into());
+            Buffer::from_history(0, history, None, Some(language), cx)
+        });
+        let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx));
+        view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing())
+            .await;
+
+        view.update(&mut cx, |view, cx| {
+            view.select_display_ranges(
+                &[
+                    DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+                    DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
+                ],
+                cx,
+            )
+            .unwrap();
+            view.insert(&Insert("{".to_string()), cx);
+            assert_eq!(
+                view.text(cx),
+                "
+                {}
+                {}
+                /
+
+                "
+                .unindent()
+            );
+
+            view.undo(&Undo, cx);
+            view.insert(&Insert("/".to_string()), cx);
+            view.insert(&Insert("*".to_string()), cx);
+            assert_eq!(
+                view.text(cx),
+                "
+                /* */
+                /* */
+                /
+
+                "
+                .unindent()
+            );
+
+            view.undo(&Undo, cx);
+            view.select_display_ranges(
+                &[
+                    DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
+                    DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
+                ],
+                cx,
+            )
+            .unwrap();
+            view.insert(&Insert("*".to_string()), cx);
+            assert_eq!(
+                view.text(cx),
+                "
+                a
+
+                /*
+                *
+                "
+                .unindent()
+            );
+        });
+    }
+
     impl Editor {
         fn selection_ranges(&self, cx: &mut MutableAppContext) -> Vec<Range<DisplayPoint>> {
             self.selections_in_range(

crates/workspace/src/lib.rs 🔗

@@ -275,6 +275,7 @@ impl WorkspaceParams {
             buffer::LanguageConfig {
                 name: "Rust".to_string(),
                 path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
             },
             tree_sitter_rust::language(),
         )));

crates/zed/languages/rust/config.toml 🔗

@@ -1,8 +1,9 @@
 name = "Rust"
 path_suffixes = ["rs"]
-bracket_pairs = [
+autoclose_pairs = [
     { start = "{", end = "}" },
     { start = "[", end = "]" },
     { start = "(", end = ")" },
-    { start = "<", end = ">" },
+    { start = "\"", end = "\"" },
+    { start = "/*", end = " */" },
 ]