diff --git a/crates/ui/src/highlighter/highlighter.rs b/crates/ui/src/highlighter/highlighter.rs index 5e56e791d..3b35ab137 100644 --- a/crates/ui/src/highlighter/highlighter.rs +++ b/crates/ui/src/highlighter/highlighter.rs @@ -341,6 +341,29 @@ impl SyntaxHighlighter { } } + /// Build an inert highlighter that never parses and creates no styles, + /// for languages without a grammar. + fn build_inert(language: SharedString) -> Self { + Self { + language, + query: None, + injections_query: None, + locals_pattern_index: 0, + highlights_pattern_index: 0, + non_local_variable_patterns: Vec::new(), + injection_content_capture_index: None, + injection_language_capture_index: None, + local_scope_capture_index: None, + local_def_capture_index: None, + local_def_value_capture_index: None, + local_ref_capture_index: None, + text: Rope::new(), + parser: Parser::new(), + tree: None, + injection_layers: Vec::new(), + } + } + /// Build the highlighter for the given language. /// /// https://github.com/tree-sitter/tree-sitter/blob/v0.26.8/crates/highlight/src/highlight.rs#L339 @@ -352,10 +375,14 @@ impl SyntaxHighlighter { )); }; + // Languages without grammar default to a highlighter that never + // parses and creates no styles. + let Some(grammar) = config.language.as_ref() else { + return Ok(Self::build_inert(config.name.clone())); + }; + let mut parser = Parser::new(); - parser - .set_language(&config.language) - .context("parse set_language")?; + parser.set_language(grammar).context("parse set_language")?; // Concatenate the query strings, keeping track of the start offset of each section. let mut query_source = String::new(); @@ -367,7 +394,7 @@ impl SyntaxHighlighter { // Construct a single query by concatenating the three query strings, but record the // range of pattern indices that belong to each individual string. - let mut query = Query::new(&config.language, &query_source).context("new query")?; + let mut query = Query::new(grammar, &query_source).context("new query")?; let mut locals_pattern_index = 0; let mut highlights_pattern_index = 0; @@ -384,9 +411,7 @@ impl SyntaxHighlighter { } let injections_query = if !config.injections.is_empty() { - Query::new(&config.language, &config.injections) - .ok() - .map(Arc::new) + Query::new(grammar, &config.injections).ok().map(Arc::new) } else { None }; @@ -505,6 +530,12 @@ impl SyntaxHighlighter { return true; } + // If there's no grammar for the language, just update the text. + if self.parser.language().is_none() { + self.text = text.clone(); + return true; + } + let edit = edit.unwrap_or(InputEdit { start_byte: 0, old_end_byte: 0, @@ -638,7 +669,7 @@ impl SyntaxHighlighter { return Some((config.name, query.clone())); } - let query = match Query::new(&config.language, &config.highlights) { + let query = match Query::new(config.language.as_ref()?, &config.highlights) { Ok(query) => Arc::new(query), Err(error) => { tracing::error!( @@ -808,7 +839,7 @@ impl SyntaxHighlighter { } let config = LanguageRegistry::singleton().language(language_name)?; let mut parser = Parser::new(); - parser.set_language(&config.language).ok()?; + parser.set_language(config.language.as_ref()?).ok()?; parser.set_included_ranges(&ranges).ok()?; let parse_start = Instant::now(); let mut timed_out = false; @@ -1250,6 +1281,24 @@ mod tests { style } + #[test] + fn test_plain_text_never_parses() { + // "text" has no grammar, the highlighter shouldn't parse. + let mut highlighter = SyntaxHighlighter::new("text"); + let rope = Rope::from("hello {\"a\": 1}\nworld"); + assert!(highlighter.update(None, &rope, None)); + assert!(highlighter.tree().is_none()); + assert_eq!(highlighter.text().to_string(), rope.to_string()); + + let styles = highlighter.styles(&(0..rope.len()), &HighlightTheme::default_dark()); + assert_eq!(styles, vec![(0..rope.len(), HighlightStyle::default())]); + + // Unregistered languages fall back to plain text. + let mut highlighter = SyntaxHighlighter::new("no-such-language"); + assert!(highlighter.update(None, &rope, None)); + assert!(highlighter.tree().is_none()); + } + #[cfg(feature = "tree-sitter-languages")] fn has_highlight_covering( highlights: &[HighlightItem], diff --git a/crates/ui/src/highlighter/languages.rs b/crates/ui/src/highlighter/languages.rs index 12aa219a9..ae35f5886 100644 --- a/crates/ui/src/highlighter/languages.rs +++ b/crates/ui/src/highlighter/languages.rs @@ -370,7 +370,7 @@ impl Language { /// (language, query, injection, locals) pub(super) fn config(&self) -> LanguageConfig { let (language, query, injection, locals) = match self { - Self::Plain => (tree_sitter_json::LANGUAGE, "", "", ""), + Self::Plain => return LanguageConfig::plain(self.name()), Self::Json => ( tree_sitter_json::LANGUAGE, include_str!("languages/json/highlights.scm"), diff --git a/crates/ui/src/highlighter/registry.rs b/crates/ui/src/highlighter/registry.rs index f03368d2d..8e9c83022 100644 --- a/crates/ui/src/highlighter/registry.rs +++ b/crates/ui/src/highlighter/registry.rs @@ -60,7 +60,7 @@ pub(super) const HIGHLIGHT_NAMES: [&str; 41] = [ #[derive(Debug, Clone, PartialEq, Eq)] pub struct LanguageConfig { pub name: SharedString, - pub language: tree_sitter::Language, + pub language: Option, pub injection_languages: Vec, pub highlights: SharedString, pub injections: SharedString, @@ -78,13 +78,30 @@ impl LanguageConfig { ) -> Self { Self { name: name.into(), - language, + language: Some(language), injection_languages, highlights: SharedString::from(highlights.to_string()), injections: SharedString::from(injections.to_string()), locals: SharedString::from(locals.to_string()), } } + + /// A plain text language without a grammar, it will never be parsed. + pub fn plain(name: impl Into) -> Self { + Self { + name: name.into(), + language: None, + injection_languages: vec![], + highlights: SharedString::default(), + injections: SharedString::default(), + locals: SharedString::default(), + } + } + + /// Whether this language has a grammar to parse with. + pub fn has_grammar(&self) -> bool { + self.language.is_some() + } } /// Theme for Tree-sitter Highlight diff --git a/crates/ui/src/highlighter/wasm_stub.rs b/crates/ui/src/highlighter/wasm_stub.rs index fa0b0d127..2d4898e47 100644 --- a/crates/ui/src/highlighter/wasm_stub.rs +++ b/crates/ui/src/highlighter/wasm_stub.rs @@ -88,6 +88,12 @@ pub struct LanguageConfig { pub name: SharedString, } +impl LanguageConfig { + pub fn has_grammar(&self) -> bool { + false + } +} + // Re-export theme types from registry module (which will be conditionally compiled) // For WASM, we create minimal stubs here use schemars::JsonSchema; diff --git a/crates/ui/src/input/mode.rs b/crates/ui/src/input/mode.rs index 45ea4cafc..aa5063cc6 100644 --- a/crates/ui/src/input/mode.rs +++ b/crates/ui/src/input/mode.rs @@ -7,6 +7,7 @@ use ropey::Rope; use super::display_map::DisplayMap; use crate::highlighter::DiagnosticSet; +use crate::highlighter::LanguageRegistry; use crate::highlighter::SyntaxHighlighter; use crate::input::{InputEdit, RopeExt as _, TabSize}; @@ -245,6 +246,14 @@ impl InputMode { let mut highlighter_ref = highlighter.borrow_mut(); if highlighter_ref.is_none() { + // Do not create a highlighter if the language has no grammar. + let has_grammar = LanguageRegistry::singleton() + .language(language) + .is_some_and(|config| config.has_grammar()); + if !has_grammar { + return None; + } + let new_highlighter = SyntaxHighlighter::new(language); highlighter_ref.replace(new_highlighter); } diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index 00becc10e..3fc772faf 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -2727,9 +2727,12 @@ impl InputState { let Some(config) = LanguageRegistry::singleton().language(&language) else { return None; }; + let Some(grammar) = config.language.as_ref() else { + return None; + }; let mut parser = tree_sitter::Parser::new(); - if parser.set_language(&config.language).is_err() { + if parser.set_language(grammar).is_err() { return None; }