Merge pull request #203 from zed-industries/autoclose-pairs

Max Brunsfeld created

Autoclose pairs

Change summary

crates/buffer/src/language.rs         |  44 +++
crates/buffer/src/lib.rs              |  45 ++-
crates/buffer/src/rope.rs             |   4 
crates/editor/src/display_map.rs      |  74 +++---
crates/editor/src/element.rs          |   4 
crates/editor/src/lib.rs              | 324 +++++++++++++++++++++++++---
crates/file_finder/src/lib.rs         |   8 
crates/server/src/rpc.rs              |   4 
crates/workspace/src/lib.rs           |  26 -
crates/zed/languages/rust/config.toml |   5 
crates/zed/src/language.rs            |  28 +-
11 files changed, 425 insertions(+), 141 deletions(-)

Detailed changes

crates/buffer/src/language.rs πŸ”—

@@ -1,29 +1,31 @@
-use crate::{HighlightMap};
+use crate::HighlightMap;
+use anyhow::Result;
 use parking_lot::Mutex;
 use serde::Deserialize;
 use std::{path::Path, str, sync::Arc};
+use theme::SyntaxTheme;
 use tree_sitter::{Language as Grammar, Query};
 pub use tree_sitter::{Parser, Tree};
-use theme::SyntaxTheme;
 
 #[derive(Default, Deserialize)]
 pub struct LanguageConfig {
     pub name: String,
     pub path_suffixes: Vec<String>,
+    pub autoclose_pairs: Vec<AutoclosePair>,
 }
 
-#[derive(Deserialize)]
-pub struct BracketPair {
+#[derive(Clone, Deserialize)]
+pub struct AutoclosePair {
     pub start: String,
     pub end: String,
 }
 
 pub struct Language {
-    pub config: LanguageConfig,
-    pub grammar: Grammar,
-    pub highlight_query: Query,
-    pub brackets_query: Query,
-    pub highlight_map: Mutex<HighlightMap>,
+    pub(crate) config: LanguageConfig,
+    pub(crate) grammar: Grammar,
+    pub(crate) highlight_query: Query,
+    pub(crate) brackets_query: Query,
+    pub(crate) highlight_map: Mutex<HighlightMap>,
 }
 
 #[derive(Default)]
@@ -62,10 +64,34 @@ impl LanguageRegistry {
 }
 
 impl Language {
+    pub fn new(config: LanguageConfig, grammar: Grammar) -> Self {
+        Self {
+            config,
+            brackets_query: Query::new(grammar, "").unwrap(),
+            highlight_query: Query::new(grammar, "").unwrap(),
+            grammar,
+            highlight_map: Default::default(),
+        }
+    }
+
+    pub fn with_highlights_query(mut self, highlights_query_source: &str) -> Result<Self> {
+        self.highlight_query = Query::new(self.grammar, highlights_query_source)?;
+        Ok(self)
+    }
+
+    pub fn with_brackets_query(mut self, brackets_query_source: &str) -> Result<Self> {
+        self.brackets_query = Query::new(self.grammar, brackets_query_source)?;
+        Ok(self)
+    }
+
     pub fn name(&self) -> &str {
         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 {
@@ -4078,19 +4095,17 @@ mod tests {
     }
 
     fn rust_lang() -> Arc<Language> {
-        let lang = tree_sitter_rust::language();
-        let brackets_query = r#"
-        ("{" @open "}" @close)
-        "#;
-        Arc::new(Language {
-            config: LanguageConfig {
-                name: "Rust".to_string(),
-                path_suffixes: vec!["rs".to_string()],
-            },
-            grammar: tree_sitter_rust::language(),
-            highlight_query: tree_sitter::Query::new(lang.clone(), "").unwrap(),
-            brackets_query: tree_sitter::Query::new(lang.clone(), brackets_query).unwrap(),
-            highlight_map: Default::default(),
-        })
+        Arc::new(
+            Language::new(
+                LanguageConfig {
+                    name: "Rust".to_string(),
+                    path_suffixes: vec!["rs".to_string()],
+                    ..Default::default()
+                },
+                tree_sitter_rust::language(),
+            )
+            .with_brackets_query(r#" ("{" @open "}" @close) "#)
+            .unwrap(),
+        )
     }
 }

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/display_map.rs πŸ”—

@@ -670,7 +670,6 @@ mod tests {
     async fn test_highlighted_chunks_at(mut cx: gpui::TestAppContext) {
         use unindent::Unindent as _;
 
-        let grammar = tree_sitter_rust::language();
         let text = r#"
             fn outer() {}
 
@@ -678,28 +677,28 @@ mod tests {
                 fn inner() {}
             }"#
         .unindent();
-        let highlight_query = tree_sitter::Query::new(
-            grammar,
-            r#"
-            (mod_item name: (identifier) body: _ @mod.body)
-            (function_item name: (identifier) @fn.name)"#,
-        )
-        .unwrap();
+
         let theme = SyntaxTheme::new(vec![
             ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()),
             ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()),
         ]);
-        let lang = Arc::new(Language {
-            config: LanguageConfig {
-                name: "Test".to_string(),
-                path_suffixes: vec![".test".to_string()],
-                ..Default::default()
-            },
-            grammar: grammar.clone(),
-            highlight_query,
-            brackets_query: tree_sitter::Query::new(grammar, "").unwrap(),
-            highlight_map: Default::default(),
-        });
+        let lang = Arc::new(
+            Language::new(
+                LanguageConfig {
+                    name: "Test".to_string(),
+                    path_suffixes: vec![".test".to_string()],
+                    ..Default::default()
+                },
+                tree_sitter_rust::language(),
+            )
+            .with_highlights_query(
+                r#"
+                (mod_item name: (identifier) body: _ @mod.body)
+                (function_item name: (identifier) @fn.name)
+                "#,
+            )
+            .unwrap(),
+        );
         lang.set_theme(&theme);
 
         let buffer = cx.add_model(|cx| {
@@ -759,7 +758,6 @@ mod tests {
 
         cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
 
-        let grammar = tree_sitter_rust::language();
         let text = r#"
             fn outer() {}
 
@@ -767,28 +765,28 @@ mod tests {
                 fn inner() {}
             }"#
         .unindent();
-        let highlight_query = tree_sitter::Query::new(
-            grammar,
-            r#"
-            (mod_item name: (identifier) body: _ @mod.body)
-            (function_item name: (identifier) @fn.name)"#,
-        )
-        .unwrap();
+
         let theme = SyntaxTheme::new(vec![
             ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()),
             ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()),
         ]);
-        let lang = Arc::new(Language {
-            config: LanguageConfig {
-                name: "Test".to_string(),
-                path_suffixes: vec![".test".to_string()],
-                ..Default::default()
-            },
-            grammar: grammar.clone(),
-            highlight_query,
-            brackets_query: tree_sitter::Query::new(grammar, "").unwrap(),
-            highlight_map: Default::default(),
-        });
+        let lang = Arc::new(
+            Language::new(
+                LanguageConfig {
+                    name: "Test".to_string(),
+                    path_suffixes: vec![".test".to_string()],
+                    ..Default::default()
+                },
+                tree_sitter_rust::language(),
+            )
+            .with_highlights_query(
+                r#"
+                (mod_item name: (identifier) body: _ @mod.body)
+                (function_item name: (identifier) @fn.name)
+                "#,
+            )
+            .unwrap(),
+        );
         lang.set_theme(&theme);
 
         let buffer = cx.add_model(|cx| {

crates/editor/src/element.rs πŸ”—

@@ -1,5 +1,5 @@
 use super::{
-    DisplayPoint, Editor, EditorMode, EditorSettings, EditorStyle, Insert, Scroll, Select,
+    DisplayPoint, Editor, EditorMode, EditorSettings, EditorStyle, Input, Scroll, Select,
     SelectPhase, Snapshot, MAX_LINE_LEN,
 };
 use buffer::HighlightId;
@@ -143,7 +143,7 @@ impl EditorElement {
                 if chars.chars().any(|c| c.is_control()) || keystroke.cmd || keystroke.ctrl {
                     false
                 } else {
-                    cx.dispatch_action(Insert(chars.to_string()));
+                    cx.dispatch_action(Input(chars.to_string()));
                     true
                 }
             }

crates/editor/src/lib.rs πŸ”—

@@ -37,7 +37,7 @@ const MAX_LINE_LEN: usize = 1024;
 action!(Cancel);
 action!(Backspace);
 action!(Delete);
-action!(Insert, String);
+action!(Input, String);
 action!(DeleteLine);
 action!(DeleteToPreviousWordBoundary);
 action!(DeleteToNextWordBoundary);
@@ -95,13 +95,13 @@ pub fn init(cx: &mut MutableAppContext) {
         Binding::new("ctrl-h", Backspace, Some("Editor")),
         Binding::new("delete", Delete, Some("Editor")),
         Binding::new("ctrl-d", Delete, Some("Editor")),
-        Binding::new("enter", Insert("\n".into()), Some("Editor && mode == full")),
+        Binding::new("enter", Input("\n".into()), Some("Editor && mode == full")),
         Binding::new(
             "alt-enter",
-            Insert("\n".into()),
+            Input("\n".into()),
             Some("Editor && mode == auto_height"),
         ),
-        Binding::new("tab", Insert("\t".into()), Some("Editor")),
+        Binding::new("tab", Input("\t".into()), Some("Editor")),
         Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")),
         Binding::new(
             "alt-backspace",
@@ -192,7 +192,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx));
     cx.add_action(Editor::select);
     cx.add_action(Editor::cancel);
-    cx.add_action(Editor::insert);
+    cx.add_action(Editor::handle_input);
     cx.add_action(Editor::backspace);
     cx.add_action(Editor::delete);
     cx.add_action(Editor::delete_line);
@@ -292,6 +292,7 @@ pub struct Editor {
     pending_selection: Option<Selection>,
     next_selection_id: usize,
     add_selections_state: Option<AddSelectionsState>,
+    autoclose_stack: Vec<AutoclosePairState>,
     select_larger_syntax_node_stack: Vec<Arc<[Selection]>>,
     scroll_position: Vector2F,
     scroll_top_anchor: Anchor,
@@ -319,6 +320,11 @@ struct AddSelectionsState {
     stack: Vec<usize>,
 }
 
+struct AutoclosePairState {
+    ranges: SmallVec<[Range<Anchor>; 32]>,
+    pair: AutoclosePair,
+}
+
 #[derive(Serialize, Deserialize)]
 struct ClipboardSelection {
     len: usize,
@@ -404,6 +410,7 @@ impl Editor {
             pending_selection: None,
             next_selection_id,
             add_selections_state: None,
+            autoclose_stack: Default::default(),
             select_larger_syntax_node_stack: Vec::new(),
             build_settings,
             scroll_position: Vector2F::zero(),
@@ -733,7 +740,18 @@ impl Editor {
         Ok(())
     }
 
-    pub fn insert(&mut self, action: &Insert, cx: &mut ViewContext<Self>) {
+    pub fn handle_input(&mut self, action: &Input, cx: &mut ViewContext<Self>) {
+        let text = action.0.as_ref();
+        if !self.skip_autoclose_end(text, cx) {
+            self.start_transaction(cx);
+            self.insert(text, cx);
+            self.autoclose_pairs(cx);
+            self.end_transaction(cx);
+        }
+    }
+
+    fn insert(&mut self, text: &str, cx: &mut ViewContext<Self>) {
+        self.start_transaction(cx);
         let mut old_selections = SmallVec::<[_; 32]>::new();
         {
             let selections = self.selections(cx);
@@ -745,12 +763,11 @@ impl Editor {
             }
         }
 
-        self.start_transaction(cx);
         let mut new_selections = Vec::new();
         self.buffer.update(cx, |buffer, cx| {
             let edit_ranges = old_selections.iter().map(|(_, range)| range.clone());
-            buffer.edit(edit_ranges, action.0.as_str(), cx);
-            let text_len = action.0.len() as isize;
+            buffer.edit(edit_ranges, text, cx);
+            let text_len = text.len() as isize;
             let mut delta = 0_isize;
             new_selections = old_selections
                 .into_iter()
@@ -775,10 +792,115 @@ impl Editor {
         self.end_transaction(cx);
     }
 
+    fn autoclose_pairs(&mut self, cx: &mut ViewContext<Self>) {
+        let selections = self.selections(cx);
+        let new_autoclose_pair_state = self.buffer.update(cx, |buffer, cx| {
+            let autoclose_pair = buffer.language().and_then(|language| {
+                let first_selection_start = selections.first().unwrap().start.to_offset(&*buffer);
+                let pair = language.autoclose_pairs().iter().find(|pair| {
+                    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
+                    }
+                })
+            });
+
+            autoclose_pair.and_then(|pair| {
+                let selection_ranges = selections
+                    .iter()
+                    .map(|selection| {
+                        let start = selection.start.to_offset(&*buffer);
+                        start..start
+                    })
+                    .collect::<SmallVec<[_; 32]>>();
+
+                buffer.edit(selection_ranges, &pair.end, cx);
+
+                if pair.end.len() == 1 {
+                    Some(AutoclosePairState {
+                        ranges: selections
+                            .iter()
+                            .map(|selection| {
+                                selection.start.bias_left(buffer)
+                                    ..selection.start.bias_right(buffer)
+                            })
+                            .collect(),
+                        pair,
+                    })
+                } else {
+                    None
+                }
+            })
+        });
+        self.autoclose_stack.extend(new_autoclose_pair_state);
+    }
+
+    fn skip_autoclose_end(&mut self, text: &str, cx: &mut ViewContext<Self>) -> bool {
+        let old_selections = self.selections(cx);
+        let autoclose_pair_state = if let Some(autoclose_pair_state) = self.autoclose_stack.last() {
+            autoclose_pair_state
+        } else {
+            return false;
+        };
+        if text != autoclose_pair_state.pair.end {
+            return false;
+        }
+
+        debug_assert_eq!(old_selections.len(), autoclose_pair_state.ranges.len());
+
+        let buffer = self.buffer.read(cx);
+        let old_selection_ranges: SmallVec<[_; 32]> = old_selections
+            .iter()
+            .map(|selection| (selection.id, selection.offset_range(buffer)))
+            .collect();
+        if old_selection_ranges
+            .iter()
+            .zip(&autoclose_pair_state.ranges)
+            .all(|((_, selection_range), autoclose_range)| {
+                let autoclose_range_end = autoclose_range.end.to_offset(buffer);
+                selection_range.is_empty() && selection_range.start == autoclose_range_end
+            })
+        {
+            let new_selections = old_selection_ranges
+                .into_iter()
+                .map(|(id, range)| {
+                    let new_head = buffer.anchor_before(range.start + 1);
+                    Selection {
+                        id,
+                        start: new_head.clone(),
+                        end: new_head,
+                        reversed: false,
+                        goal: SelectionGoal::None,
+                    }
+                })
+                .collect();
+            self.autoclose_stack.pop();
+            self.update_selections(new_selections, true, cx);
+            true
+        } else {
+            false
+        }
+    }
+
     pub fn clear(&mut self, cx: &mut ViewContext<Self>) {
         self.start_transaction(cx);
         self.select_all(&SelectAll, cx);
-        self.insert(&Insert(String::new()), cx);
+        self.insert("", cx);
         self.end_transaction(cx);
     }
 
@@ -801,7 +923,7 @@ impl Editor {
         }
 
         self.update_selections(selections, true, cx);
-        self.insert(&Insert(String::new()), cx);
+        self.insert("", cx);
         self.end_transaction(cx);
     }
 
@@ -824,7 +946,7 @@ impl Editor {
         }
 
         self.update_selections(selections, true, cx);
-        self.insert(&Insert(String::new()), cx);
+        self.insert(&"", cx);
         self.end_transaction(cx);
     }
 
@@ -1172,7 +1294,7 @@ impl Editor {
             }
         }
         self.update_selections(selections, true, cx);
-        self.insert(&Insert(String::new()), cx);
+        self.insert("", cx);
         self.end_transaction(cx);
 
         cx.as_mut()
@@ -1219,7 +1341,6 @@ impl Editor {
                     clipboard_selections.clear();
                 }
 
-                self.start_transaction(cx);
                 let mut start_offset = 0;
                 let mut new_selections = Vec::with_capacity(selections.len());
                 for (i, selection) in selections.iter().enumerate() {
@@ -1262,9 +1383,8 @@ impl Editor {
                     });
                 }
                 self.update_selections(new_selections, true, cx);
-                self.end_transaction(cx);
             } else {
-                self.insert(&Insert(clipboard_text.into()), cx);
+                self.insert(clipboard_text, cx);
             }
         }
     }
@@ -1506,7 +1626,7 @@ impl Editor {
         }
 
         self.update_selections(selections, true, cx);
-        self.insert(&Insert(String::new()), cx);
+        self.insert("", cx);
         self.end_transaction(cx);
     }
 
@@ -1576,7 +1696,7 @@ impl Editor {
         }
 
         self.update_selections(selections, true, cx);
-        self.insert(&Insert(String::new()), cx);
+        self.insert("", cx);
         self.end_transaction(cx);
     }
 
@@ -2104,20 +2224,41 @@ impl Editor {
             }
         }
 
-        self.buffer.update(cx, |buffer, cx| {
-            buffer
-                .update_selection_set(self.selection_set_id, selections, cx)
-                .unwrap();
-        });
-        self.pause_cursor_blinking(cx);
+        self.add_selections_state = None;
+        self.select_larger_syntax_node_stack.clear();
+        while let Some(autoclose_pair_state) = self.autoclose_stack.last() {
+            let all_selections_inside_autoclose_ranges =
+                if selections.len() == autoclose_pair_state.ranges.len() {
+                    selections.iter().zip(&autoclose_pair_state.ranges).all(
+                        |(selection, autoclose_range)| {
+                            let head = selection.head();
+                            autoclose_range.start.cmp(head, buffer).unwrap() <= Ordering::Equal
+                                && autoclose_range.end.cmp(head, buffer).unwrap() >= Ordering::Equal
+                        },
+                    )
+                } else {
+                    false
+                };
+
+            if all_selections_inside_autoclose_ranges {
+                break;
+            } else {
+                self.autoclose_stack.pop();
+            }
+        }
 
         if autoscroll {
             self.autoscroll_requested = true;
             cx.notify();
         }
 
-        self.add_selections_state = None;
-        self.select_larger_syntax_node_stack.clear();
+        self.pause_cursor_blinking(cx);
+
+        self.buffer.update(cx, |buffer, cx| {
+            buffer
+                .update_selection_set(self.selection_set_id, selections, cx)
+                .unwrap();
+        });
     }
 
     fn start_transaction(&self, cx: &mut ViewContext<Self>) {
@@ -3666,9 +3807,9 @@ mod tests {
         // is pasted at each cursor.
         view.update(cx, |view, cx| {
             view.select_ranges(vec![0..0, 31..31], false, cx);
-            view.insert(&Insert("( ".into()), cx);
+            view.handle_input(&Input("( ".into()), cx);
             view.paste(&Paste, cx);
-            view.insert(&Insert(") ".into()), cx);
+            view.handle_input(&Input(") ".into()), cx);
             assert_eq!(
                 view.display_text(cx),
                 "( oneβœ… three five ) two oneβœ… four three six five ( oneβœ… three five ) "
@@ -3677,7 +3818,7 @@ mod tests {
 
         view.update(cx, |view, cx| {
             view.select_ranges(vec![0..0], false, cx);
-            view.insert(&Insert("123\n4567\n89\n".into()), cx);
+            view.handle_input(&Input("123\n4567\n89\n".into()), cx);
             assert_eq!(
                 view.display_text(cx),
                 "123\n4567\n89\n( oneβœ… three five ) two oneβœ… four three six five ( oneβœ… three five ) "
@@ -4068,15 +4209,10 @@ mod tests {
     #[gpui::test]
     async fn test_select_larger_smaller_syntax_node(mut cx: gpui::TestAppContext) {
         let settings = cx.read(EditorSettings::test);
-
-        let grammar = tree_sitter_rust::language();
-        let language = Arc::new(Language {
-            config: LanguageConfig::default(),
-            brackets_query: tree_sitter::Query::new(grammar, "").unwrap(),
-            highlight_query: tree_sitter::Query::new(grammar, "").unwrap(),
-            highlight_map: Default::default(),
-            grammar,
-        });
+        let language = Arc::new(Language::new(
+            LanguageConfig::default(),
+            tree_sitter_rust::language(),
+        ));
 
         let text = r#"
             use mod1::mod2::{mod3, mod4};
@@ -4086,6 +4222,7 @@ mod tests {
             }
         "#
         .unindent();
+
         let buffer = cx.add_model(|cx| {
             let history = History::new(text.into());
             Buffer::from_history(0, history, None, Some(language), cx)
@@ -4213,6 +4350,117 @@ 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.handle_input(&Input("{".to_string()), cx);
+            view.handle_input(&Input("{".to_string()), cx);
+            view.handle_input(&Input("{".to_string()), cx);
+            assert_eq!(
+                view.text(cx),
+                "
+                {{{}}}
+                {{{}}}
+                /
+
+                "
+                .unindent()
+            );
+
+            view.move_right(&MoveRight, cx);
+            view.handle_input(&Input("}".to_string()), cx);
+            view.handle_input(&Input("}".to_string()), cx);
+            view.handle_input(&Input("}".to_string()), cx);
+            assert_eq!(
+                view.text(cx),
+                "
+                {{{}}}}
+                {{{}}}}
+                /
+
+                "
+                .unindent()
+            );
+
+            view.undo(&Undo, cx);
+            view.handle_input(&Input("/".to_string()), cx);
+            view.handle_input(&Input("*".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.handle_input(&Input("*".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/file_finder/src/lib.rs πŸ”—

@@ -422,7 +422,7 @@ impl FileFinder {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use editor::Insert;
+    use editor::Input;
     use serde_json::json;
     use std::path::PathBuf;
     use workspace::{Workspace, WorkspaceParams};
@@ -471,9 +471,9 @@ mod tests {
         let query_buffer = cx.read(|cx| finder.read(cx).query_editor.clone());
 
         let chain = vec![finder.id(), query_buffer.id()];
-        cx.dispatch_action(window_id, chain.clone(), Insert("b".into()));
-        cx.dispatch_action(window_id, chain.clone(), Insert("n".into()));
-        cx.dispatch_action(window_id, chain.clone(), Insert("a".into()));
+        cx.dispatch_action(window_id, chain.clone(), Input("b".into()));
+        cx.dispatch_action(window_id, chain.clone(), Input("n".into()));
+        cx.dispatch_action(window_id, chain.clone(), Input("a".into()));
         finder
             .condition(&cx, |finder, _| finder.matches.len() == 2)
             .await;

crates/server/src/rpc.rs πŸ”—

@@ -981,7 +981,7 @@ mod tests {
             self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Credentials,
             EstablishConnectionError, UserStore,
         },
-        editor::{Editor, EditorSettings, Insert},
+        editor::{Editor, EditorSettings, Input},
         fs::{FakeFs, Fs as _},
         people_panel::JoinWorktree,
         project::{ProjectPath, Worktree},
@@ -1068,7 +1068,7 @@ mod tests {
 
         // Edit the buffer as client B and see that edit as client A.
         editor_b.update(&mut cx_b, |editor, cx| {
-            editor.insert(&Insert("ok, ".into()), cx)
+            editor.handle_input(&Input("ok, ".into()), cx)
         });
         buffer_a
             .condition(&cx_a, |buffer, _| buffer.text() == "ok, b-contents")

crates/workspace/src/lib.rs πŸ”—

@@ -270,19 +270,15 @@ pub struct WorkspaceParams {
 impl WorkspaceParams {
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &mut MutableAppContext) -> Self {
-        let grammar = tree_sitter_rust::language();
-        let language = Arc::new(buffer::Language {
-            config: buffer::LanguageConfig {
+        let mut languages = LanguageRegistry::new();
+        languages.add(Arc::new(buffer::Language::new(
+            buffer::LanguageConfig {
                 name: "Rust".to_string(),
                 path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
             },
-            brackets_query: tree_sitter::Query::new(grammar, "").unwrap(),
-            highlight_query: tree_sitter::Query::new(grammar, "").unwrap(),
-            highlight_map: Default::default(),
-            grammar,
-        });
-        let mut languages = LanguageRegistry::new();
-        languages.add(language);
+            tree_sitter_rust::language(),
+        )));
 
         let client = Client::new();
         let http_client = client::test::FakeHttpClient::new(|_| async move {
@@ -1074,7 +1070,7 @@ impl WorkspaceHandle for ViewHandle<Workspace> {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use editor::{Editor, Insert};
+    use editor::{Editor, Input};
     use serde_json::json;
     use std::collections::HashSet;
 
@@ -1286,7 +1282,7 @@ mod tests {
             item.to_any().downcast::<Editor>().unwrap()
         });
 
-        cx.update(|cx| editor.update(cx, |editor, cx| editor.insert(&Insert("x".into()), cx)));
+        cx.update(|cx| editor.update(cx, |editor, cx| editor.handle_input(&Input("x".into()), cx)));
         fs.insert_file("/root/a.txt", "changed".to_string())
             .await
             .unwrap();
@@ -1339,7 +1335,7 @@ mod tests {
             assert!(!editor.is_dirty(cx.as_ref()));
             assert_eq!(editor.title(cx.as_ref()), "untitled");
             assert!(editor.language(cx).is_none());
-            editor.insert(&Insert("hi".into()), cx);
+            editor.handle_input(&Input("hi".into()), cx);
             assert!(editor.is_dirty(cx.as_ref()));
         });
 
@@ -1371,7 +1367,7 @@ mod tests {
 
         // Edit the file and save it again. This time, there is no filename prompt.
         editor.update(&mut cx, |editor, cx| {
-            editor.insert(&Insert(" there".into()), cx);
+            editor.handle_input(&Input(" there".into()), cx);
             assert_eq!(editor.is_dirty(cx.as_ref()), true);
         });
         workspace.update(&mut cx, |workspace, cx| {
@@ -1432,7 +1428,7 @@ mod tests {
 
         editor.update(&mut cx, |editor, cx| {
             assert!(editor.language(cx).is_none());
-            editor.insert(&Insert("hi".into()), cx);
+            editor.handle_input(&Input("hi".into()), cx);
             assert!(editor.is_dirty(cx.as_ref()));
         });
 

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 = " */" },
 ]

crates/zed/src/language.rs πŸ”—

@@ -1,8 +1,7 @@
-use buffer::{HighlightMap, Language, LanguageRegistry};
-use parking_lot::Mutex;
+use buffer::{Language, LanguageRegistry};
 use rust_embed::RustEmbed;
+use std::borrow::Cow;
 use std::{str, sync::Arc};
-use tree_sitter::Query;
 
 #[derive(RustEmbed)]
 #[folder = "languages"]
@@ -18,19 +17,16 @@ fn rust() -> Language {
     let grammar = tree_sitter_rust::language();
     let rust_config =
         toml::from_slice(&LanguageDir::get("rust/config.toml").unwrap().data).unwrap();
-    Language {
-        config: rust_config,
-        grammar,
-        highlight_query: load_query(grammar, "rust/highlights.scm"),
-        brackets_query: load_query(grammar, "rust/brackets.scm"),
-        highlight_map: Mutex::new(HighlightMap::default()),
-    }
+    Language::new(rust_config, grammar)
+        .with_highlights_query(load_query("rust/highlights.scm").as_ref())
+        .unwrap()
+        .with_brackets_query(load_query("rust/brackets.scm").as_ref())
+        .unwrap()
 }
 
-fn load_query(grammar: tree_sitter::Language, path: &str) -> Query {
-    Query::new(
-        grammar,
-        str::from_utf8(&LanguageDir::get(path).unwrap().data).unwrap(),
-    )
-    .unwrap()
+fn load_query(path: &str) -> Cow<'static, str> {
+    match LanguageDir::get(path).unwrap().data {
+        Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
+        Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
+    }
 }