Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 58 additions & 9 deletions crates/ui/src/highlighter/highlighter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
Expand All @@ -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;
Expand All @@ -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
};
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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],
Expand Down
2 changes: 1 addition & 1 deletion crates/ui/src/highlighter/languages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
21 changes: 19 additions & 2 deletions crates/ui/src/highlighter/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<tree_sitter::Language>,
pub injection_languages: Vec<SharedString>,
pub highlights: SharedString,
pub injections: SharedString,
Expand All @@ -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<SharedString>) -> 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
Expand Down
6 changes: 6 additions & 0 deletions crates/ui/src/highlighter/wasm_stub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions crates/ui/src/input/mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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);
}
Expand Down
5 changes: 4 additions & 1 deletion crates/ui/src/input/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Loading