diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index bd45543e1b..99f1ea5184 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -17,6 +17,7 @@ doctest = false [features] decimal = ["dep:rust_decimal"] inspector = ["gpui_macros/inspector", "gpui/inspector"] +tree-sitter = ["dep:tree-sitter", "dep:tree-sitter-json"] # For syntax highlighting in Markdown and CodeEditor. tree-sitter-languages = [ @@ -56,42 +57,42 @@ tree-sitter-languages = [ "tree-sitter-yaml", "tree-sitter-zig", ] -tree-sitter-astro = ["dep:tree-sitter-astro-next"] -tree-sitter-bash = ["dep:tree-sitter-bash"] -tree-sitter-c = ["dep:tree-sitter-c"] -tree-sitter-cmake = ["dep:tree-sitter-cmake"] -tree-sitter-cpp = ["dep:tree-sitter-cpp"] -tree-sitter-csharp = ["dep:tree-sitter-c-sharp"] -tree-sitter-css = ["dep:tree-sitter-css"] -tree-sitter-diff = ["dep:tree-sitter-diff"] -tree-sitter-ejs = ["dep:tree-sitter-embedded-template"] -tree-sitter-elixir = ["dep:tree-sitter-elixir"] -tree-sitter-erb = ["dep:tree-sitter-embedded-template"] -tree-sitter-go = ["dep:tree-sitter-go"] -tree-sitter-graphql = ["dep:tree-sitter-graphql"] -tree-sitter-html = ["dep:tree-sitter-html"] -tree-sitter-java = ["dep:tree-sitter-java"] -tree-sitter-javascript = ["dep:tree-sitter-javascript"] -tree-sitter-jsdoc = ["dep:tree-sitter-jsdoc"] -tree-sitter-kotlin = ["dep:tree-sitter-kotlin-sg"] -tree-sitter-lua = ["dep:tree-sitter-lua"] -tree-sitter-make = ["dep:tree-sitter-make"] -tree-sitter-markdown = ["dep:tree-sitter-md"] +tree-sitter-astro = ["tree-sitter", "dep:tree-sitter-astro-next"] +tree-sitter-bash = ["tree-sitter", "dep:tree-sitter-bash"] +tree-sitter-c = ["tree-sitter", "dep:tree-sitter-c"] +tree-sitter-cmake = ["tree-sitter", "dep:tree-sitter-cmake"] +tree-sitter-cpp = ["tree-sitter", "dep:tree-sitter-cpp"] +tree-sitter-csharp = ["tree-sitter", "dep:tree-sitter-c-sharp"] +tree-sitter-css = ["tree-sitter", "dep:tree-sitter-css"] +tree-sitter-diff = ["tree-sitter", "dep:tree-sitter-diff"] +tree-sitter-ejs = ["tree-sitter", "dep:tree-sitter-embedded-template"] +tree-sitter-elixir = ["tree-sitter", "dep:tree-sitter-elixir"] +tree-sitter-erb = ["tree-sitter", "dep:tree-sitter-embedded-template"] +tree-sitter-go = ["tree-sitter", "dep:tree-sitter-go"] +tree-sitter-graphql = ["tree-sitter", "dep:tree-sitter-graphql"] +tree-sitter-html = ["tree-sitter", "dep:tree-sitter-html"] +tree-sitter-java = ["tree-sitter", "dep:tree-sitter-java"] +tree-sitter-javascript = ["tree-sitter", "dep:tree-sitter-javascript"] +tree-sitter-jsdoc = ["tree-sitter", "dep:tree-sitter-jsdoc"] +tree-sitter-kotlin = ["tree-sitter", "dep:tree-sitter-kotlin-sg"] +tree-sitter-lua = ["tree-sitter", "dep:tree-sitter-lua"] +tree-sitter-make = ["tree-sitter", "dep:tree-sitter-make"] +tree-sitter-markdown = ["tree-sitter", "dep:tree-sitter-md"] tree-sitter-markdown-inline = ["tree-sitter-markdown"] -tree-sitter-php = ["dep:tree-sitter-php"] -tree-sitter-proto = ["dep:tree-sitter-proto"] -tree-sitter-python = ["dep:tree-sitter-python"] -tree-sitter-ruby = ["dep:tree-sitter-ruby"] -tree-sitter-rust = ["dep:tree-sitter-rust"] -tree-sitter-scala = ["dep:tree-sitter-scala"] -tree-sitter-sql = ["dep:tree-sitter-sequel"] -tree-sitter-svelte = ["dep:tree-sitter-svelte-next"] -tree-sitter-swift = ["dep:tree-sitter-swift"] -tree-sitter-toml = ["dep:tree-sitter-toml-ng"] -tree-sitter-tsx = ["dep:tree-sitter-typescript"] -tree-sitter-typescript = ["dep:tree-sitter-typescript"] -tree-sitter-yaml = ["dep:tree-sitter-yaml"] -tree-sitter-zig = ["dep:tree-sitter-zig"] +tree-sitter-php = ["tree-sitter", "dep:tree-sitter-php"] +tree-sitter-proto = ["tree-sitter", "dep:tree-sitter-proto"] +tree-sitter-python = ["tree-sitter", "dep:tree-sitter-python"] +tree-sitter-ruby = ["tree-sitter", "dep:tree-sitter-ruby"] +tree-sitter-rust = ["tree-sitter", "dep:tree-sitter-rust"] +tree-sitter-scala = ["tree-sitter", "dep:tree-sitter-scala"] +tree-sitter-sql = ["tree-sitter", "dep:tree-sitter-sequel"] +tree-sitter-svelte = ["tree-sitter", "dep:tree-sitter-svelte-next"] +tree-sitter-swift = ["tree-sitter", "dep:tree-sitter-swift"] +tree-sitter-toml = ["tree-sitter", "dep:tree-sitter-toml-ng"] +tree-sitter-tsx = ["tree-sitter", "dep:tree-sitter-typescript"] +tree-sitter-typescript = ["tree-sitter", "dep:tree-sitter-typescript"] +tree-sitter-yaml = ["tree-sitter", "dep:tree-sitter-yaml"] +tree-sitter-zig = ["tree-sitter", "dep:tree-sitter-zig"] [dependencies] anyhow.workspace = true @@ -147,7 +148,7 @@ instant = { version = "0.1", features = ["wasm-bindgen"] } # Native-only dependencies (not available on WASM) [target.'cfg(not(target_family = "wasm"))'.dependencies] smol.workspace = true -tree-sitter = "0.26" +tree-sitter = { version = "0.26", optional = true } tree-sitter-astro-next = { version="0.1.1", optional = true } tree-sitter-bash = { version = "0.23.3", optional = true } tree-sitter-c = { version = "0.24.1", optional = true } @@ -164,7 +165,7 @@ tree-sitter-html = { version = "0.23.2", optional = true } tree-sitter-java = { version = "0.23.5", optional = true } tree-sitter-javascript = { version = "0.23.1", optional = true } tree-sitter-jsdoc = { version = "0.23.2", optional = true } -tree-sitter-json = "0.24.8" +tree-sitter-json = { version = "0.24.8", optional = true } tree-sitter-kotlin-sg = { version = "0.4.0", optional = true } tree-sitter-lua = { version = "0.4.1", optional = true } tree-sitter-make = { version = "1.1.1", optional = true } diff --git a/crates/ui/src/highlighter/mod.rs b/crates/ui/src/highlighter/mod.rs index 7684c3422c..00884c30d9 100644 --- a/crates/ui/src/highlighter/mod.rs +++ b/crates/ui/src/highlighter/mod.rs @@ -3,22 +3,22 @@ mod diagnostics; pub use diagnostics::*; // Native implementation with full tree-sitter support -#[cfg(not(target_family = "wasm"))] +#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] mod highlighter; -#[cfg(not(target_family = "wasm"))] +#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] mod languages; -#[cfg(not(target_family = "wasm"))] +#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] mod registry; -#[cfg(not(target_family = "wasm"))] +#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] pub use highlighter::*; -#[cfg(not(target_family = "wasm"))] +#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] pub use languages::*; -#[cfg(not(target_family = "wasm"))] +#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] pub use registry::*; -// WASM stub implementation (no tree-sitter support) -#[cfg(target_family = "wasm")] +// WASM stub implementation (no tree-sitter support or disabled) +#[cfg(any(target_family = "wasm", not(feature = "tree-sitter")))] mod wasm_stub; -#[cfg(target_family = "wasm")] +#[cfg(any(target_family = "wasm", not(feature = "tree-sitter")))] pub use wasm_stub::*; diff --git a/crates/ui/src/highlighter/wasm_stub.rs b/crates/ui/src/highlighter/wasm_stub.rs index 7aced8533c..ff853bc433 100644 --- a/crates/ui/src/highlighter/wasm_stub.rs +++ b/crates/ui/src/highlighter/wasm_stub.rs @@ -88,6 +88,7 @@ pub struct LanguageConfig { // For WASM, we create minimal stubs here use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; use std::{ collections::HashMap, sync::{LazyLock, Mutex}, @@ -101,7 +102,7 @@ pub enum FontStyle { Underline, } -#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, JsonSchema, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, JsonSchema, Serialize_repr, Deserialize_repr)] #[repr(u16)] pub enum FontWeightContent { Thin = 100, @@ -149,19 +150,173 @@ impl From for HighlightStyle { #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)] pub struct SyntaxColors { - // Minimal stub - actual fields are in native registry.rs - // Adding commonly accessed fields to avoid compilation errors + pub attribute: Option, + pub boolean: Option, + pub comment: Option, + pub comment_doc: Option, + pub constant: Option, + pub constructor: Option, + pub embedded: Option, + pub emphasis: Option, + #[serde(rename = "emphasis.strong")] + pub emphasis_strong: Option, + #[serde(rename = "enum")] + pub enum_: Option, + pub function: Option, + pub hint: Option, + pub keyword: Option, + pub label: Option, #[serde(rename = "link_text")] pub link_text: Option, + #[serde(rename = "link_uri")] + pub link_uri: Option, + pub number: Option, + pub operator: Option, + pub predictive: Option, + pub preproc: Option, + pub primary: Option, + pub property: Option, + pub punctuation: Option, + #[serde(rename = "punctuation.bracket")] + pub punctuation_bracket: Option, + #[serde(rename = "punctuation.delimiter")] + pub punctuation_delimiter: Option, + #[serde(rename = "punctuation.list_marker")] + pub punctuation_list_marker: Option, + #[serde(rename = "punctuation.special")] + pub punctuation_special: Option, + pub string: Option, + #[serde(rename = "string.escape")] + pub string_escape: Option, + #[serde(rename = "string.regex")] + pub string_regex: Option, + #[serde(rename = "string.special")] + pub string_special: Option, + #[serde(rename = "string.special.symbol")] + pub string_special_symbol: Option, + pub tag: Option, + #[serde(rename = "tag.doctype")] + pub tag_doctype: Option, + #[serde(rename = "text.code.span")] + pub text_code_span: Option, + #[serde(rename = "text.literal")] + pub text_literal: Option, + pub title: Option, + #[serde(rename = "type")] + pub type_: Option, + pub variable: Option, + #[serde(rename = "variable.special")] + pub variable_special: Option, + pub variant: Option, } impl SyntaxColors { - pub fn style(&self, _name: &str) -> Option { - None + pub fn style(&self, name: &str) -> Option { + if name.is_empty() { + return None; + } + + let style = match name { + "attribute" => self.attribute, + "boolean" => self.boolean, + "comment" => self.comment, + "comment.doc" => self.comment_doc, + "constant" => self.constant, + "constructor" => self.constructor, + "embedded" => self.embedded, + "emphasis" => self.emphasis, + "emphasis.strong" => self.emphasis_strong, + "enum" => self.enum_, + "function" => self.function, + "hint" => self.hint, + "keyword" => self.keyword, + "label" => self.label, + "link_text" => self.link_text, + "link_uri" => self.link_uri, + "number" => self.number, + "operator" => self.operator, + "predictive" => self.predictive, + "preproc" => self.preproc, + "primary" => self.primary, + "property" => self.property, + "punctuation" => self.punctuation, + "punctuation.bracket" => self.punctuation_bracket, + "punctuation.delimiter" => self.punctuation_delimiter, + "punctuation.list_marker" => self.punctuation_list_marker, + "punctuation.special" => self.punctuation_special, + "string" => self.string, + "string.escape" => self.string_escape, + "string.regex" => self.string_regex, + "string.special" => self.string_special, + "string.special.symbol" => self.string_special_symbol, + "tag" => self.tag, + "tag.doctype" => self.tag_doctype, + "text.code.span" => self.text_code_span, + "text.literal" => self.text_literal, + "title" => self.title, + "type" => self.type_, + "variable" => self.variable, + "variable.special" => self.variable_special, + "variant" => self.variant, + _ => None, + } + .map(|s| s.into()); + + if style.is_some() { + style + } else if name.contains('.') { + name.split('.').next().and_then(|prefix| self.style(prefix)) + } else { + None + } } - pub fn style_for_index(&self, _index: usize) -> Option { - None + pub fn style_for_index(&self, index: usize) -> Option { + const HIGHLIGHT_NAMES: [&str; 41] = [ + "attribute", + "boolean", + "comment", + "comment.doc", + "constant", + "constructor", + "embedded", + "emphasis", + "emphasis.strong", + "enum", + "function", + "hint", + "keyword", + "label", + "link_text", + "link_uri", + "number", + "operator", + "predictive", + "preproc", + "primary", + "property", + "punctuation", + "punctuation.bracket", + "punctuation.delimiter", + "punctuation.list_marker", + "punctuation.special", + "string", + "string.escape", + "string.regex", + "string.special", + "string.special.symbol", + "tag", + "tag.doctype", + "text.code.span", + "text.literal", + "title", + "type", + "variable", + "variable.special", + "variant", + ]; + + HIGHLIGHT_NAMES.get(index).and_then(|name| self.style(name)) } } diff --git a/crates/ui/src/input/display_map/folding.rs b/crates/ui/src/input/display_map/folding.rs index b05be4601a..92f9c1dc5b 100644 --- a/crates/ui/src/input/display_map/folding.rs +++ b/crates/ui/src/input/display_map/folding.rs @@ -1,15 +1,15 @@ use std::ops::Range; -#[cfg(not(target_family = "wasm"))] +#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] use tree_sitter::Node; -#[cfg(not(target_family = "wasm"))] +#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] pub use tree_sitter::Tree; -#[cfg(target_family = "wasm")] +#[cfg(any(target_family = "wasm", not(feature = "tree-sitter")))] /// Stub type for tree-sitter Tree on WASM (tree-sitter not available). pub struct Tree; -#[cfg(not(target_family = "wasm"))] +#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] /// Minimum line span for a node to be considered foldable. const MIN_FOLD_LINES: usize = 2; @@ -39,7 +39,7 @@ impl FoldRange { // ==================== Native Implementation (with tree-sitter) ==================== -#[cfg(not(target_family = "wasm"))] +#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] /// Check if a named node qualifies as a fold candidate. /// /// Uses a structural heuristic: any **named** node spanning ≥ MIN_FOLD_LINES @@ -52,7 +52,7 @@ fn is_foldable_node(node: &Node) -> bool { end.saturating_sub(start) >= MIN_FOLD_LINES } -#[cfg(not(target_family = "wasm"))] +#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] /// Extract fold ranges from a tree-sitter syntax tree (full traversal). pub fn extract_fold_ranges(tree: &Tree) -> Vec { let mut ranges = Vec::new(); @@ -68,7 +68,7 @@ pub fn extract_fold_ranges(tree: &Tree) -> Vec { ranges } -#[cfg(not(target_family = "wasm"))] +#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] /// Extract fold ranges only within a byte range (for incremental updates after edits). /// /// Skips subtrees entirely outside the range, making it O(nodes in range) @@ -87,7 +87,7 @@ pub fn extract_fold_ranges_in_range(tree: &Tree, byte_range: Range) -> Ve ranges } -#[cfg(not(target_family = "wasm"))] +#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] /// Recursively collect foldable nodes, skipping subtrees outside byte_range. fn collect_foldable_nodes_in_range( node: Node, @@ -113,7 +113,7 @@ fn collect_foldable_nodes_in_range( } } -#[cfg(not(target_family = "wasm"))] +#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] /// Recursively collect foldable nodes from the syntax tree (full traversal). fn collect_foldable_nodes(node: Node, ranges: &mut Vec) { if !is_foldable_node(&node) { @@ -133,13 +133,13 @@ fn collect_foldable_nodes(node: Node, ranges: &mut Vec) { // ==================== WASM Stub Implementation ==================== -#[cfg(target_family = "wasm")] +#[cfg(any(target_family = "wasm", not(feature = "tree-sitter")))] /// Extract fold ranges - WASM stub (returns empty, no tree-sitter). pub fn extract_fold_ranges(_tree: &Tree) -> Vec { Vec::new() } -#[cfg(target_family = "wasm")] +#[cfg(any(target_family = "wasm", not(feature = "tree-sitter")))] /// Extract fold ranges in range - WASM stub (returns empty, no tree-sitter). pub fn extract_fold_ranges_in_range(_tree: &Tree, _byte_range: Range) -> Vec { Vec::new() diff --git a/crates/ui/src/input/display_map/mod.rs b/crates/ui/src/input/display_map/mod.rs index 62ca985660..74c5978671 100644 --- a/crates/ui/src/input/display_map/mod.rs +++ b/crates/ui/src/input/display_map/mod.rs @@ -9,9 +9,9 @@ /// about `BufferPoint ↔ DisplayPoint` mapping, without worrying about internal wrap/fold complexity. mod display_map; mod fold_map; -#[cfg(not(target_family = "wasm"))] +#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] mod folding; -#[cfg(target_family = "wasm")] +#[cfg(any(target_family = "wasm", not(feature = "tree-sitter")))] pub mod folding; mod text_wrapper; mod wrap_map; @@ -22,6 +22,8 @@ pub(crate) use self::text_wrapper::LineLayout; // Re-export FoldRange and extract_fold_ranges pub use folding::{FoldRange, extract_fold_ranges}; +#[cfg(any(target_family = "wasm", not(feature = "tree-sitter")))] +pub use folding::Tree; /// Position in the buffer (logical text). /// diff --git a/crates/ui/src/input/mod.rs b/crates/ui/src/input/mod.rs index 2431b53eca..3a5a2f09f3 100644 --- a/crates/ui/src/input/mod.rs +++ b/crates/ui/src/input/mod.rs @@ -23,8 +23,8 @@ mod state; pub(crate) use clear_button::*; pub use cursor::*; -#[cfg(target_family = "wasm")] -pub use display_map::folding::Tree; +#[cfg(any(target_family = "wasm", not(feature = "tree-sitter")))] +pub use display_map::Tree; pub use display_map::{BufferPoint, DisplayMap, DisplayPoint, FoldRange}; pub use indent::TabSize; pub use input::*; diff --git a/crates/ui/src/input/mode.rs b/crates/ui/src/input/mode.rs index 0afb49aca2..2557feac61 100644 --- a/crates/ui/src/input/mode.rs +++ b/crates/ui/src/input/mode.rs @@ -351,7 +351,7 @@ mod tests { } #[test] - #[cfg(not(target_family = "wasm"))] + #[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] fn test_replacement_input_edit_shifts_tree_sitter_included_ranges() { let old_source = "[1,2]"; let new_source = "[1,2"; diff --git a/crates/ui/src/input/rope_ext.rs b/crates/ui/src/input/rope_ext.rs index beedda9bfb..6914ab3658 100644 --- a/crates/ui/src/input/rope_ext.rs +++ b/crates/ui/src/input/rope_ext.rs @@ -3,10 +3,10 @@ use std::ops::Range; use ropey::{LineType, Rope, RopeSlice}; use sum_tree::Bias; -#[cfg(not(target_family = "wasm"))] +#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] pub use tree_sitter::{InputEdit, Point}; -#[cfg(target_family = "wasm")] +#[cfg(any(target_family = "wasm", not(feature = "tree-sitter")))] /// Stub type for tree-sitter Point on WASM (tree-sitter not available). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Point { @@ -14,14 +14,14 @@ pub struct Point { pub column: usize, } -#[cfg(target_family = "wasm")] +#[cfg(any(target_family = "wasm", not(feature = "tree-sitter")))] impl Point { pub fn new(row: usize, column: usize) -> Self { Self { row, column } } } -#[cfg(target_family = "wasm")] +#[cfg(any(target_family = "wasm", not(feature = "tree-sitter")))] /// Stub type for tree-sitter InputEdit on WASM (tree-sitter not available). #[derive(Debug, Clone, Copy)] pub struct InputEdit { @@ -458,9 +458,9 @@ impl RopeExt for Rope { #[cfg(test)] mod tests { + use super::Point; use ropey::Rope; use sum_tree::Bias; - use tree_sitter::Point; use crate::{RopeExt, input::Position}; diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index 6f09fb5b51..0f0f942bc5 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -32,7 +32,7 @@ use super::{ use crate::Size; use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp}; use crate::highlighter::DiagnosticSet; -#[cfg(not(target_family = "wasm"))] +#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] use crate::highlighter::LanguageRegistry; use crate::input::blink_cursor::CURSOR_WIDTH; use crate::input::movement::MoveDirection; @@ -2439,7 +2439,7 @@ impl InputState { /// /// Dropping the returned `Task` (stored in `parse_task`) cancels the /// parse, which naturally debounces rapid edits. - #[cfg(not(target_family = "wasm"))] + #[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] fn dispatch_background_parse( pending: super::mode::PendingBackgroundParse, window: &mut Window, @@ -2530,7 +2530,7 @@ impl InputState { parse_task_rc.borrow_mut().replace(task); } - #[cfg(target_family = "wasm")] + #[cfg(any(target_family = "wasm", not(feature = "tree-sitter")))] fn dispatch_background_parse( _pending: super::mode::PendingBackgroundParse, _window: &mut Window, diff --git a/crates/ui/src/text/node.rs b/crates/ui/src/text/node.rs index 227a55caff..3d753f99fb 100644 --- a/crates/ui/src/text/node.rs +++ b/crates/ui/src/text/node.rs @@ -1455,7 +1455,7 @@ mod tests { assert_ne!(first, second); } - #[cfg(not(target_family = "wasm"))] + #[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))] #[test] fn code_block_highlighter_cache_refreshes_after_language_registration() { let lang = SharedString::from("json-cache-test");