Implement scope-specific bracket matching and comment toggling

Max Brunsfeld and Julia Risley created

Co-authored-by: Julia Risley <julia@zed.dev>

Change summary

crates/editor/src/editor.rs                |  6 
crates/editor/src/multi_buffer.rs          | 10 +-
crates/language/src/buffer.rs              |  8 +-
crates/language/src/buffer_tests.rs        | 83 ++++++++++++++++++++++++
crates/language/src/language.rs            | 48 ++++++-------
crates/zed/src/languages.rs                |  5 +
crates/zed/src/languages/tsx/overrides.scm |  7 +
7 files changed, 129 insertions(+), 38 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -1737,7 +1737,7 @@ impl Editor {
         for (selection, autoclose_region) in
             self.selections_with_autoclose_regions(selections, &snapshot)
         {
-            if let Some(language) = snapshot.language_config_at(selection.head()) {
+            if let Some(language) = snapshot.language_scope_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;
@@ -1898,7 +1898,7 @@ impl Editor {
                         let end = selection.end;
 
                         let mut insert_extra_newline = false;
-                        if let Some(language) = buffer.language_config_at(start) {
+                        if let Some(language) = buffer.language_scope_at(start) {
                             let leading_whitespace_len = buffer
                                 .reversed_chars_at(start)
                                 .take_while(|c| c.is_whitespace() && *c != '\n')
@@ -4535,7 +4535,7 @@ impl Editor {
             for selection in &mut selections {
                 let start_column = snapshot.indent_size_for_line(selection.start.row).len;
                 let language = if let Some(language) =
-                    snapshot.language_config_at(Point::new(selection.start.row, start_column))
+                    snapshot.language_scope_at(Point::new(selection.start.row, start_column))
                 {
                     language
                 } else {

crates/editor/src/multi_buffer.rs 🔗

@@ -10,9 +10,9 @@ use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
 pub use language::Completion;
 use language::{
     char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
-    DiagnosticEntry, IndentSize, Language, LanguageConfigYeet, OffsetRangeExt, OffsetUtf16,
-    Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _,
-    ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
+    DiagnosticEntry, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline,
+    OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _,
+    ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
 };
 use std::{
     borrow::Cow,
@@ -2691,9 +2691,9 @@ impl MultiBufferSnapshot {
             .and_then(|(buffer, offset)| buffer.language_at(offset))
     }
 
-    pub fn language_config_at<'a, T: ToOffset>(&'a self, point: T) -> Option<LanguageConfigYeet> {
+    pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option<LanguageScope> {
         self.point_to_buffer_offset(point)
-            .and_then(|(buffer, offset)| buffer.language_config_at(offset))
+            .and_then(|(buffer, offset)| buffer.language_scope_at(offset))
     }
 
     pub fn is_dirty(&self) -> bool {

crates/language/src/buffer.rs 🔗

@@ -9,7 +9,7 @@ use crate::{
     syntax_map::{
         SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxSnapshot, ToTreeSitterPoint,
     },
-    CodeLabel, LanguageConfigYeet, Outline,
+    CodeLabel, LanguageScope, Outline,
 };
 use anyhow::{anyhow, Result};
 use clock::ReplicaId;
@@ -2015,7 +2015,7 @@ impl BufferSnapshot {
             .or(self.language.as_ref())
     }
 
-    pub fn language_config_at<D: ToOffset>(&self, position: D) -> Option<LanguageConfigYeet> {
+    pub fn language_scope_at<D: ToOffset>(&self, position: D) -> Option<LanguageScope> {
         let offset = position.to_offset(self);
 
         if let Some(layer_info) = self
@@ -2024,12 +2024,12 @@ impl BufferSnapshot {
             .filter(|l| l.node.end_byte() > offset)
             .last()
         {
-            Some(LanguageConfigYeet {
+            Some(LanguageScope {
                 language: layer_info.language.clone(),
                 override_id: layer_info.override_id(offset, &self.text),
             })
         } else {
-            self.language.clone().map(|language| LanguageConfigYeet {
+            self.language.clone().map(|language| LanguageScope {
                 language,
                 override_id: None,
             })

crates/language/src/buffer_tests.rs 🔗

@@ -1369,6 +1369,89 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut MutableAppContext) {
     });
 }
 
+#[gpui::test]
+fn test_language_config_at(cx: &mut MutableAppContext) {
+    cx.set_global(Settings::test(cx));
+    cx.add_model(|cx| {
+        let language = Language::new(
+            LanguageConfig {
+                name: "JavaScript".into(),
+                line_comment: Some("// ".into()),
+                brackets: vec![
+                    BracketPair {
+                        start: "{".into(),
+                        end: "}".into(),
+                        close: true,
+                        newline: false,
+                    },
+                    BracketPair {
+                        start: "'".into(),
+                        end: "'".into(),
+                        close: true,
+                        newline: false,
+                    },
+                ],
+                overrides: [
+                    (
+                        "element".into(),
+                        LanguageConfigOverride {
+                            line_comment: Override::Remove { remove: true },
+                            block_comment: Override::Set(("{/*".into(), "*/}".into())),
+                            ..Default::default()
+                        },
+                    ),
+                    (
+                        "string".into(),
+                        LanguageConfigOverride {
+                            brackets: Override::Set(vec![BracketPair {
+                                start: "{".into(),
+                                end: "}".into(),
+                                close: true,
+                                newline: false,
+                            }]),
+                            ..Default::default()
+                        },
+                    ),
+                ]
+                .into_iter()
+                .collect(),
+                ..Default::default()
+            },
+            Some(tree_sitter_javascript::language()),
+        )
+        .with_override_query(
+            r#"
+                (jsx_element) @override.element
+                (string) @override.string
+            "#,
+        )
+        .unwrap();
+
+        let text = r#"a["b"] = <C d="e"></C>;"#;
+
+        let buffer = Buffer::new(0, text, cx).with_language(Arc::new(language), cx);
+        let snapshot = buffer.snapshot();
+
+        let config = snapshot.language_scope_at(0).unwrap();
+        assert_eq!(config.line_comment_prefix().unwrap().as_ref(), "// ");
+        assert_eq!(config.brackets().len(), 2);
+
+        let string_config = snapshot.language_scope_at(3).unwrap();
+        assert_eq!(config.line_comment_prefix().unwrap().as_ref(), "// ");
+        assert_eq!(string_config.brackets().len(), 1);
+
+        let element_config = snapshot.language_scope_at(10).unwrap();
+        assert_eq!(element_config.line_comment_prefix(), None);
+        assert_eq!(
+            element_config.block_comment_delimiters(),
+            Some((&"{/*".into(), &"*/}".into()))
+        );
+        assert_eq!(element_config.brackets().len(), 2);
+
+        buffer
+    });
+}
+
 #[gpui::test]
 fn test_serialization(cx: &mut gpui::MutableAppContext) {
     let mut now = Instant::now();

crates/language/src/language.rs 🔗

@@ -22,10 +22,7 @@ use lazy_static::lazy_static;
 use parking_lot::{Mutex, RwLock};
 use postage::watch;
 use regex::Regex;
-use serde::{
-    de::{self},
-    Deserialize, Deserializer,
-};
+use serde::{de, Deserialize, Deserializer};
 use serde_json::Value;
 use std::{
     any::Any,
@@ -251,20 +248,22 @@ pub struct LanguageConfig {
 }
 
 #[derive(Clone)]
-pub struct LanguageConfigYeet {
+pub struct LanguageScope {
     language: Arc<Language>,
     override_id: Option<u32>,
 }
 
-#[derive(Deserialize)]
+#[derive(Deserialize, Default, Debug)]
 pub struct LanguageConfigOverride {
     #[serde(default)]
     pub line_comment: Override<Arc<str>>,
     #[serde(default)]
     pub block_comment: Override<(Arc<str>, Arc<str>)>,
+    #[serde(default)]
+    pub brackets: Override<Vec<BracketPair>>,
 }
 
-#[derive(Deserialize)]
+#[derive(Deserialize, Debug)]
 #[serde(untagged)]
 pub enum Override<T> {
     Remove { remove: bool },
@@ -278,11 +277,11 @@ impl<T> Default for Override<T> {
 }
 
 impl<T> Override<T> {
-    fn as_option<'a>(this: Option<&'a Self>, original: &'a Option<T>) -> Option<&'a T> {
+    fn as_option<'a>(this: Option<&'a Self>, original: Option<&'a T>) -> Option<&'a T> {
         match this {
             Some(Self::Set(value)) => Some(value),
             Some(Self::Remove { remove: true }) => None,
-            Some(Self::Remove { remove: false }) | None => original.as_ref(),
+            Some(Self::Remove { remove: false }) | None => original,
         }
     }
 }
@@ -966,40 +965,39 @@ impl Language {
     }
 }
 
-impl LanguageConfigYeet {
+impl LanguageScope {
     pub fn line_comment_prefix(&self) -> Option<&Arc<str>> {
         Override::as_option(
-            self.over_ride().map(|o| &o.line_comment),
-            &self.language.config.line_comment,
+            self.config_override().map(|o| &o.line_comment),
+            self.language.config.line_comment.as_ref(),
         )
     }
 
     pub fn block_comment_delimiters(&self) -> Option<(&Arc<str>, &Arc<str>)> {
         Override::as_option(
-            self.over_ride().map(|o| &o.block_comment),
-            &self.language.config.block_comment,
+            self.config_override().map(|o| &o.block_comment),
+            self.language.config.block_comment.as_ref(),
         )
         .map(|e| (&e.0, &e.1))
     }
 
     pub fn brackets(&self) -> &[BracketPair] {
-        &self.language.config.brackets
+        Override::as_option(
+            self.config_override().map(|o| &o.brackets),
+            Some(&self.language.config.brackets),
+        )
+        .map_or(&[], Vec::as_slice)
     }
 
     pub fn should_autoclose_before(&self, c: char) -> bool {
         c.is_whitespace() || self.language.config.autoclose_before.contains(c)
     }
 
-    fn over_ride(&self) -> Option<&LanguageConfigOverride> {
-        self.override_id.and_then(|id| {
-            self.language
-                .grammar
-                .as_ref()?
-                .override_config
-                .as_ref()?
-                .values
-                .get(&id)
-        })
+    fn config_override(&self) -> Option<&LanguageConfigOverride> {
+        let id = self.override_id?;
+        let grammar = self.language.grammar.as_ref()?;
+        let override_config = grammar.override_config.as_ref()?;
+        override_config.values.get(&id)
     }
 }
 

crates/zed/src/languages.rs 🔗

@@ -173,6 +173,11 @@ pub(crate) fn language(
             .with_injection_query(query.as_ref())
             .expect("failed to load injection query");
     }
+    if let Some(query) = load_query(name, "/overrides") {
+        language = language
+            .with_override_query(query.as_ref())
+            .expect("failed to load override query");
+    }
     if let Some(lsp_adapter) = lsp_adapter {
         language = language.with_lsp_adapter(lsp_adapter)
     }