Hide Markdown-Inline language from users with a new 'hidden' flag on language configs (#17104)

Max Brunsfeld and Marshall created

/cc @mrnugget 

Release Notes:

- Fixed an issue where toggling inline completions in a markdown file
did not work correctly

---------

Co-authored-by: Marshall <marshall@zed.dev>

Change summary

crates/language/src/buffer.rs                      | 17 ++-
crates/language/src/buffer_tests.rs                | 78 ++++++++++++++++
crates/language/src/language.rs                    | 11 ++
crates/language/src/syntax_map.rs                  | 23 ++--
crates/language/src/syntax_map/syntax_map_tests.rs | 45 ++-------
crates/languages/src/markdown-inline/config.toml   | 13 --
6 files changed, 124 insertions(+), 63 deletions(-)

Detailed changes

crates/language/src/buffer.rs 🔗

@@ -1018,7 +1018,7 @@ impl Buffer {
         let offset = position.to_offset(self);
         self.syntax_map
             .lock()
-            .layers_for_range(offset..offset, &self.text)
+            .layers_for_range(offset..offset, &self.text, false)
             .last()
             .map(|info| info.language.clone())
             .or_else(|| self.language.clone())
@@ -2625,13 +2625,14 @@ impl BufferSnapshot {
 
     /// Iterates over every [`SyntaxLayer`] in the buffer.
     pub fn syntax_layers(&self) -> impl Iterator<Item = SyntaxLayer> + '_ {
-        self.syntax.layers_for_range(0..self.len(), &self.text)
+        self.syntax
+            .layers_for_range(0..self.len(), &self.text, true)
     }
 
     pub fn syntax_layer_at<D: ToOffset>(&self, position: D) -> Option<SyntaxLayer> {
         let offset = position.to_offset(self);
         self.syntax
-            .layers_for_range(offset..offset, &self.text)
+            .layers_for_range(offset..offset, &self.text, false)
             .filter(|l| l.node().end_byte() > offset)
             .last()
     }
@@ -2664,7 +2665,10 @@ impl BufferSnapshot {
         let mut smallest_range: Option<Range<usize>> = None;
 
         // Use the layer that has the smallest node intersecting the given point.
-        for layer in self.syntax.layers_for_range(offset..offset, &self.text) {
+        for layer in self
+            .syntax
+            .layers_for_range(offset..offset, &self.text, false)
+        {
             let mut cursor = layer.node().walk();
 
             let mut range = None;
@@ -2740,7 +2744,10 @@ impl BufferSnapshot {
     pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
         let range = range.start.to_offset(self)..range.end.to_offset(self);
         let mut result: Option<Range<usize>> = None;
-        'outer: for layer in self.syntax.layers_for_range(range.clone(), &self.text) {
+        'outer: for layer in self
+            .syntax
+            .layers_for_range(range.clone(), &self.text, true)
+        {
             let mut cursor = layer.node().walk();
 
             // Descend to the first leaf that touches the start of the range,

crates/language/src/buffer_tests.rs 🔗

@@ -2216,6 +2216,45 @@ fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) {
     });
 }
 
+#[gpui::test]
+fn test_language_at_with_hidden_languages(cx: &mut AppContext) {
+    init_settings(cx, |_| {});
+
+    cx.new_model(|cx| {
+        let text = r#"
+            this is an *emphasized* word.
+        "#
+        .unindent();
+
+        let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
+        language_registry.add(Arc::new(markdown_lang()));
+        language_registry.add(Arc::new(markdown_inline_lang()));
+
+        let mut buffer = Buffer::local(text, cx);
+        buffer.set_language_registry(language_registry.clone());
+        buffer.set_language(
+            language_registry
+                .language_for_name("Markdown")
+                .now_or_never()
+                .unwrap()
+                .ok(),
+            cx,
+        );
+
+        let snapshot = buffer.snapshot();
+
+        for point in [Point::new(0, 4), Point::new(0, 16)] {
+            let config = snapshot.language_scope_at(point).unwrap();
+            assert_eq!(config.language_name().as_ref(), "Markdown");
+
+            let language = snapshot.language_at(point).unwrap();
+            assert_eq!(language.name().as_ref(), "Markdown");
+        }
+
+        buffer
+    });
+}
+
 #[gpui::test]
 fn test_serialization(cx: &mut gpui::AppContext) {
     let mut now = Instant::now();
@@ -2868,6 +2907,45 @@ fn javascript_lang() -> Language {
     .unwrap()
 }
 
+pub fn markdown_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "Markdown".into(),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["md".into()],
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        Some(tree_sitter_md::language()),
+    )
+    .with_injection_query(
+        r#"
+            (fenced_code_block
+                (info_string
+                    (language) @language)
+                (code_fence_content) @content)
+
+            ((inline) @content
+                (#set! "language" "markdown-inline"))
+        "#,
+    )
+    .unwrap()
+}
+
+pub fn markdown_inline_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "Markdown-Inline".into(),
+            hidden: true,
+            ..LanguageConfig::default()
+        },
+        Some(tree_sitter_md::inline_language()),
+    )
+    .with_highlights_query("(emphasis) @emphasis")
+    .unwrap()
+}
+
 fn get_tree_sexp(buffer: &Model<Buffer>, cx: &mut gpui::TestAppContext) -> String {
     buffer.update(cx, |buffer, _| {
         let snapshot = buffer.snapshot();

crates/language/src/language.rs 🔗

@@ -17,7 +17,7 @@ mod syntax_map;
 mod task_context;
 
 #[cfg(test)]
-mod buffer_tests;
+pub mod buffer_tests;
 pub mod markdown;
 
 use crate::language_settings::SoftWrap;
@@ -627,6 +627,10 @@ pub struct LanguageConfig {
     /// If there's a parser name in the language settings, that will be used instead.
     #[serde(default)]
     pub prettier_parser_name: Option<String>,
+    /// If true, this language is only for syntax highlighting via an injection into other
+    /// languages, but should not appear to the user as a distinct language.
+    #[serde(default)]
+    pub hidden: bool,
 }
 
 #[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
@@ -713,6 +717,7 @@ impl Default for LanguageConfig {
             tab_size: None,
             soft_wrap: None,
             prettier_parser_name: None,
+            hidden: false,
         }
     }
 }
@@ -1414,6 +1419,10 @@ impl Language {
 }
 
 impl LanguageScope {
+    pub fn language_name(&self) -> Arc<str> {
+        self.language.config.name.clone()
+    }
+
     pub fn collapsed_placeholder(&self) -> &str {
         self.language.config.collapsed_placeholder.as_ref()
     }

crates/language/src/syntax_map.rs 🔗

@@ -782,7 +782,7 @@ impl SyntaxSnapshot {
         SyntaxMapCaptures::new(
             range.clone(),
             buffer.as_rope(),
-            self.layers_for_range(range, buffer),
+            self.layers_for_range(range, buffer, true),
             query,
         )
     }
@@ -796,20 +796,22 @@ impl SyntaxSnapshot {
         SyntaxMapMatches::new(
             range.clone(),
             buffer.as_rope(),
-            self.layers_for_range(range, buffer),
+            self.layers_for_range(range, buffer, true),
             query,
         )
     }
 
     #[cfg(test)]
     pub fn layers<'a>(&'a self, buffer: &'a BufferSnapshot) -> Vec<SyntaxLayer> {
-        self.layers_for_range(0..buffer.len(), buffer).collect()
+        self.layers_for_range(0..buffer.len(), buffer, true)
+            .collect()
     }
 
     pub fn layers_for_range<'a, T: ToOffset>(
         &'a self,
         range: Range<T>,
         buffer: &'a BufferSnapshot,
+        include_hidden: bool,
     ) -> impl 'a + Iterator<Item = SyntaxLayer> {
         let start_offset = range.start.to_offset(buffer);
         let end_offset = range.end.to_offset(buffer);
@@ -833,13 +835,14 @@ impl SyntaxSnapshot {
                 if let SyntaxLayerContent::Parsed { tree, language } = &layer.content {
                     let layer_start_offset = layer.range.start.to_offset(buffer);
                     let layer_start_point = layer.range.start.to_point(buffer).to_ts_point();
-
-                    info = Some(SyntaxLayer {
-                        tree,
-                        language,
-                        depth: layer.depth,
-                        offset: (layer_start_offset, layer_start_point),
-                    });
+                    if include_hidden || !language.config.hidden {
+                        info = Some(SyntaxLayer {
+                            tree,
+                            language,
+                            depth: layer.depth,
+                            offset: (layer_start_offset, layer_start_point),
+                        });
+                    }
                 }
                 cursor.next(buffer);
                 if info.is_some() {

crates/language/src/syntax_map/syntax_map_tests.rs 🔗

@@ -1,5 +1,8 @@
 use super::*;
-use crate::{LanguageConfig, LanguageMatcher};
+use crate::{
+    buffer_tests::{markdown_inline_lang, markdown_lang},
+    LanguageConfig, LanguageMatcher,
+};
 use gpui::AppContext;
 use rand::rngs::StdRng;
 use std::{env, ops::Range, sync::Arc};
@@ -779,8 +782,13 @@ fn test_empty_combined_injections_inside_injections(cx: &mut AppContext) {
         &buffer,
         Point::new(0, 0)..Point::new(5, 0),
         &[
+            // Markdown document
             "(document (section (fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (block_continuation) (code_fence_content (block_continuation)) (fenced_code_block_delimiter)) (paragraph (inline))))",
+            // ERB template in the code block
             "(template...",
+            // Markdown inline content
+            "(inline)",
+            // HTML within the ERB
             "(document (text))",
             // The ruby syntax tree should be empty, since there are
             // no interpolations in the ERB template.
@@ -1229,39 +1237,6 @@ fn rust_lang() -> Language {
     .unwrap()
 }
 
-fn markdown_lang() -> Language {
-    Language::new(
-        LanguageConfig {
-            name: "Markdown".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["md".into()],
-                ..Default::default()
-            },
-            ..Default::default()
-        },
-        Some(tree_sitter_md::language()),
-    )
-    .with_injection_query(
-        r#"
-            (fenced_code_block
-                (info_string
-                    (language) @language)
-                (code_fence_content) @content)
-        "#,
-    )
-    .unwrap()
-}
-
-fn markdown_inline_lang() -> Language {
-    Language::new(
-        LanguageConfig {
-            name: "Markdown-Inline".into(),
-            ..LanguageConfig::default()
-        },
-        Some(tree_sitter_md::inline_language()),
-    )
-}
-
 fn elixir_lang() -> Language {
     Language::new(
         LanguageConfig {
@@ -1327,7 +1302,7 @@ fn assert_layers_for_range(
     expected_layers: &[&str],
 ) {
     let layers = syntax_map
-        .layers_for_range(range, &buffer)
+        .layers_for_range(range, &buffer, true)
         .collect::<Vec<_>>();
     assert_eq!(
         layers.len(),

crates/languages/src/markdown-inline/config.toml 🔗

@@ -1,14 +1,3 @@
 name = "Markdown-Inline"
 grammar = "markdown-inline"
-path_suffixes = []
-brackets = [
-    { start = "{", end = "}", close = true, newline = true },
-    { start = "[", end = "]", close = true, newline = true },
-    { start = "(", end = ")", close = true, newline = true },
-    { start = "<", end = ">", close = true, newline = true },
-    { start = "\"", end = "\"", close = false, newline = false },
-    { start = "'", end = "'", close = false, newline = false },
-    { start = "`", end = "`", close = false, newline = false },
-]
-
-tab_size = 2
+hidden = true