From 79ff6ba9cc7d0c859238843af56719c2fe4b8d9e Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Wed, 11 Jun 2025 15:26:27 +0800 Subject: [PATCH] highlighter: Fix injection language highlight. (#931) Still have a bit bug for highlight: image --- Cargo.lock | 101 +++--- crates/story/examples/code-editor.rs | 7 +- crates/story/examples/fixtures/test.js | 125 ++++--- crates/story/examples/fixtures/test.ts | 51 +++ crates/ui/Cargo.toml | 3 +- crates/ui/src/highlighter/highlighter.rs | 336 +++++++++++++----- crates/ui/src/highlighter/languages.rs | 197 +++++----- .../highlighter/languages/go/highlights.scm | 136 +++++++ .../languages/javascript/highlights.scm | 207 +++++++++++ .../languages/javascript/injections.scm | 54 +++ .../languages/typescript/highlights.scm | 243 +++++++++++++ 11 files changed, 1180 insertions(+), 280 deletions(-) create mode 100644 crates/story/examples/fixtures/test.ts create mode 100644 crates/ui/src/highlighter/languages/go/highlights.scm create mode 100644 crates/ui/src/highlighter/languages/javascript/highlights.scm create mode 100644 crates/ui/src/highlighter/languages/javascript/injections.scm create mode 100644 crates/ui/src/highlighter/languages/typescript/highlights.scm diff --git a/Cargo.lock b/Cargo.lock index e98e9f21..2c7a9265 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -623,7 +623,7 @@ dependencies = [ "bitflags 2.9.1", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.12.1", "lazy_static", "lazycell", "log", @@ -646,7 +646,7 @@ dependencies = [ "bitflags 2.9.1", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -989,7 +989,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" dependencies = [ "heck 0.4.1", - "indexmap 2.7.1", + "indexmap 2.9.0", "log", "proc-macro2", "quote", @@ -1226,9 +1226,9 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#047a7f5d290448e026731123e912f8621c806031" +source = "git+https://github.com/zed-industries/zed.git#ee2a3299815ea29770491b34592a448d95761cdb" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc-hash 2.1.0", "workspace-hack", ] @@ -1739,7 +1739,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#047a7f5d290448e026731123e912f8621c806031" +source = "git+https://github.com/zed-industries/zed.git#ee2a3299815ea29770491b34592a448d95761cdb" dependencies = [ "proc-macro2", "quote", @@ -2313,6 +2313,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "ftree" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae0379499242d3b9355c5069b43b9417def8c9b09903b930db1fe49318dc9e9" +dependencies = [ + "serde", +] + [[package]] name = "funty" version = "2.0.0" @@ -2777,7 +2786,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#047a7f5d290448e026731123e912f8621c806031" +source = "git+https://github.com/zed-industries/zed.git#ee2a3299815ea29770491b34592a448d95761cdb" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2879,6 +2888,7 @@ dependencies = [ "gpui-component-macros", "html5ever 0.27.0", "image", + "indexset", "indoc", "itertools 0.13.0", "markdown", @@ -2909,10 +2919,10 @@ dependencies = [ "tree-sitter-embedded-template", "tree-sitter-go", "tree-sitter-graphql", - "tree-sitter-highlight", "tree-sitter-html", "tree-sitter-java", "tree-sitter-javascript", + "tree-sitter-jsdoc", "tree-sitter-json", "tree-sitter-make", "tree-sitter-md", @@ -2945,7 +2955,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#047a7f5d290448e026731123e912f8621c806031" +source = "git+https://github.com/zed-industries/zed.git#ee2a3299815ea29770491b34592a448d95761cdb" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -3024,7 +3034,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.7.1", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -3215,7 +3225,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#047a7f5d290448e026731123e912f8621c806031" +source = "git+https://github.com/zed-industries/zed.git#ee2a3299815ea29770491b34592a448d95761cdb" dependencies = [ "anyhow", "bytes", @@ -3232,7 +3242,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#047a7f5d290448e026731123e912f8621c806031" +source = "git+https://github.com/zed-industries/zed.git#ee2a3299815ea29770491b34592a448d95761cdb" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3415,15 +3425,24 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown 0.15.2", "serde", ] +[[package]] +name = "indexset" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d55bfb73496dd673d99571597bfe757c0b227339da7d842ef6159e211de1075" +dependencies = [ + "ftree", +] + [[package]] name = "indoc" version = "2.0.5" @@ -3777,7 +3796,7 @@ dependencies = [ "dashmap", "data-encoding", "getrandom 0.2.15", - "indexmap 2.7.1", + "indexmap 2.9.0", "itertools 0.10.5", "lazy_static", "lightningcss-derive", @@ -4002,7 +4021,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#047a7f5d290448e026731123e912f8621c806031" +source = "git+https://github.com/zed-industries/zed.git#ee2a3299815ea29770491b34592a448d95761cdb" dependencies = [ "anyhow", "bindgen 0.71.1", @@ -4148,7 +4167,7 @@ dependencies = [ "half", "hashbrown 0.15.2", "hexf-parse", - "indexmap 2.7.1", + "indexmap 2.9.0", "log", "num-traits", "once_cell", @@ -4395,7 +4414,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", "syn 2.0.101", @@ -5719,7 +5738,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#047a7f5d290448e026731123e912f8621c806031" +source = "git+https://github.com/zed-industries/zed.git#ee2a3299815ea29770491b34592a448d95761cdb" dependencies = [ "derive_refineable", "workspace-hack", @@ -5829,7 +5848,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#047a7f5d290448e026731123e912f8621c806031" +source = "git+https://github.com/zed-industries/zed.git#ee2a3299815ea29770491b34592a448d95761cdb" dependencies = [ "anyhow", "bytes", @@ -6267,7 +6286,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" dependencies = [ "dyn-clone", - "indexmap 2.7.1", + "indexmap 2.9.0", "schemars_derive", "serde", "serde_json", @@ -6378,7 +6397,7 @@ checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#047a7f5d290448e026731123e912f8621c806031" +source = "git+https://github.com/zed-industries/zed.git#ee2a3299815ea29770491b34592a448d95761cdb" dependencies = [ "anyhow", "serde", @@ -6437,7 +6456,7 @@ version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "itoa 1.0.11", "memchr", "ryu", @@ -6450,7 +6469,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e033097bf0d2b59a62b42c18ebbb797503839b26afdda2c4e1415cb6c813540" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "itoa 1.0.11", "memchr", "ryu", @@ -6495,7 +6514,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "itoa 1.0.11", "ryu", "serde", @@ -6876,7 +6895,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#047a7f5d290448e026731123e912f8621c806031" +source = "git+https://github.com/zed-industries/zed.git#ee2a3299815ea29770491b34592a448d95761cdb" dependencies = [ "arrayvec", "log", @@ -7389,7 +7408,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "toml_datetime", "winnow 0.5.40", ] @@ -7400,7 +7419,7 @@ version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "toml_datetime", "winnow 0.5.40", ] @@ -7411,7 +7430,7 @@ version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "serde", "serde_spanned", "toml_datetime", @@ -7630,18 +7649,6 @@ dependencies = [ "tree-sitter-language", ] -[[package]] -name = "tree-sitter-highlight" -version = "0.25.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3bdc761dfa6b50677cd1bb23b55571d3902c54671c952f7147da2642e15248" -dependencies = [ - "regex", - "streaming-iterator", - "thiserror 2.0.12", - "tree-sitter", -] - [[package]] name = "tree-sitter-html" version = "0.23.2" @@ -7672,6 +7679,16 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "tree-sitter-jsdoc" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3862dfcb1038fc5e7812d7df14190afdeb7e1415288fd5f51f58395f8cb0faf" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "tree-sitter-json" version = "0.24.8" @@ -8054,7 +8071,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#047a7f5d290448e026731123e912f8621c806031" +source = "git+https://github.com/zed-industries/zed.git#ee2a3299815ea29770491b34592a448d95761cdb" dependencies = [ "anyhow", "async-fs 2.1.2", diff --git a/crates/story/examples/code-editor.rs b/crates/story/examples/code-editor.rs index fbb9bb1f..1c56d07a 100644 --- a/crates/story/examples/code-editor.rs +++ b/crates/story/examples/code-editor.rs @@ -13,6 +13,7 @@ fn init(cx: &mut App) { LanguageRegistry::global_mut(cx).register( "navi", &LanguageConfig::new( + "navi", tree_sitter_navi::LANGUAGE.into(), vec![], tree_sitter_navi::HIGHLIGHTS_QUERY, @@ -46,7 +47,7 @@ impl Lang { } } -const LANGUAGES: [(Lang, &'static str); 8] = [ +const LANGUAGES: [(Lang, &'static str); 9] = [ ( Lang::BuiltIn(Language::Rust), include_str!("./fixtures/test.rs"), @@ -55,6 +56,10 @@ const LANGUAGES: [(Lang, &'static str); 8] = [ Lang::BuiltIn(Language::JavaScript), include_str!("./fixtures/test.js"), ), + ( + Lang::BuiltIn(Language::TypeScript), + include_str!("./fixtures/test.ts"), + ), ( Lang::BuiltIn(Language::Go), include_str!("./fixtures/test.go"), diff --git a/crates/story/examples/fixtures/test.js b/crates/story/examples/fixtures/test.js index 8cbef842..a1d9dd5e 100644 --- a/crates/story/examples/fixtures/test.js +++ b/crates/story/examples/fixtures/test.js @@ -1,76 +1,83 @@ +const fs = require("fs"); + +const getName = function () { + return "John Doe"; +}; + /** * A class representing a HelloWorld greeter with various utility methods * @class HelloWorld + * @param {string} name - The name to use for greetings */ class HelloWorld { - // Version number of the HelloWorld class - static VERSION = '1.0.0'; - // Counter to track number of class instances - static #instanceCount = 0; + // Version number of the HelloWorld class + static VERSION = "1.0.0"; + // Counter to track number of class instances + static #instanceCount = 0; - // Private instance fields - #name; - #options; - #createdAt; + // Private instance fields + #name; + #options; + #createdAt; - /** - * Creates a new HelloWorld instance - * @param {string} name - The name to use for greetings - * @param {Object} options - Configuration options - */ - constructor(name = 'World', options = {}) { - this.#name = name; - this.#options = options; - this.#createdAt = new Date(); - HelloWorld.#instanceCount++; + /** + * Creates a new HelloWorld instance + * @param {string} name - The name to use for greetings + * @param {Object} options - Configuration options + */ + constructor(name = "World", options = {}) { + this.#name = name; + this.#options = options; + this.#createdAt = new Date(); + HelloWorld.#instanceCount++; + } + + static getInstanceCount() { + return HelloWorld.#instanceCount; + } + + get name() { + return this.#name; + } + + set name(value) { + this.#name = value; + } + + async greet(...names) { + try { + for (const name of names) { + await new Promise((resolve) => setTimeout(resolve, 100)); + console.log(`Hello, ${name}!`); + } + } catch (error) { + console.error(`Error: ${error.message}`); } + } - static getInstanceCount() { - return HelloWorld.#instanceCount; - } + configure(options = {}) { + Object.assign(this.#options, options); + } - get name() { - return this.#name; - } + *generateSequence(start = 0, end = 10) { + for (let i = start; i <= end; i++) yield i; + } - set name(value) { - this.#name = value; - } - - async greet(...names) { - try { - for (const name of names) { - await new Promise(resolve => setTimeout(resolve, 100)); - console.log(`Hello, ${name}!`); - } - } catch (error) { - console.error(`Error: ${error.message}`); - } - } - - configure(options = {}) { - Object.assign(this.#options, options); - } - - *generateSequence(start = 0, end = 10) { - for (let i = start; i <= end; i++) yield i; - } - - processNames(names) { - return names - .filter(name => name.length > 0) - .map(name => name.toUpperCase()) - .sort(); - } + processNames(names) { + return names + .filter((name) => name.length > 0) + .map((name) => name.toUpperCase()) + .sort(); + } } -const greeter = new HelloWorld('JavaScript'); +const greeter = new HelloWorld("JavaScript"); (async () => { - const uniqueNames = new Set(['Alice', 'Bob']); - await greeter.greet(...uniqueNames); + const uniqueNames = new Set(["Alice", "Bob"]); + await greeter.greet(...uniqueNames); - for (const num of greeter.generateSequence(0, 5)) { - console.log(num); - } + for (const num of greeter.generateSequence(0, 5)) { + console.log(num); + } })(); diff --git a/crates/story/examples/fixtures/test.ts b/crates/story/examples/fixtures/test.ts new file mode 100644 index 00000000..c4228a3a --- /dev/null +++ b/crates/story/examples/fixtures/test.ts @@ -0,0 +1,51 @@ +import fs, { readFile } from "fs"; + +const VERSION = "1.0.0"; + +class HelloWorld { + private name: string; + private createdAt: Date; + private options: Record; + + constructor(name: string) { + this.name = name; + this.createdAt = new Date(); + this.options = {}; + } + + greet(names: string[]): void { + for (const name of names) { + console.log(`Hello, ${name}!`); + } + } + + configure(cfg: { timeout: number; retries: number; debug: boolean }): void { + this.options["timeout"] = cfg.timeout; + this.options["retries"] = cfg.retries; + this.options["debug"] = cfg.debug; + } + + generateReport(): string { + return ` + HelloWorld Report + ================= + Name: ${this.name} + Created: ${this.createdAt.toISOString()} + Options: ${JSON.stringify(this.options, null, 2)} + `; + } +} + +const timeout = 5000; + +function main() { + const greeter = new HelloWorld("TypeScript"); + greeter.configure({ + timeout: timeout, + retries: 3, + debug: true, + }); + + greeter.greet(["Alice", "Bob"]); + console.log(greeter.generateReport()); +} diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 91ef4f55..65250003 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -57,6 +57,7 @@ minify-html = "0.15.0" chrono = "0.4.38" # Code Editor +indexset = "0.12.2" tree-sitter = "0.25.4" tree-sitter-bash = "0.23.3" tree-sitter-c = "0.24.1" @@ -69,10 +70,10 @@ tree-sitter-elixir = "0.3" tree-sitter-embedded-template = "0.23.0" tree-sitter-go = "0.23.4" tree-sitter-graphql = "0.1.0" -tree-sitter-highlight = "0.25.4" tree-sitter-html = "0.23.2" tree-sitter-java = "0.23.5" tree-sitter-javascript = "0.23.1" +tree-sitter-jsdoc = "0.23.2" tree-sitter-json = "0.24.8" tree-sitter-make = "1.1.1" tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", tag = "v0.5.0" } diff --git a/crates/ui/src/highlighter/highlighter.rs b/crates/ui/src/highlighter/highlighter.rs index 37d1fca3..9bf3573c 100644 --- a/crates/ui/src/highlighter/highlighter.rs +++ b/crates/ui/src/highlighter/highlighter.rs @@ -1,17 +1,14 @@ +use super::HighlightTheme; +use crate::highlighter::LanguageRegistry; use gpui::{App, HighlightStyle, SharedString}; +use indexset::BTreeMap; use std::{ - collections::{BTreeMap, HashMap}, - ops::Range, - sync::Arc, + collections::HashMap, + ops::{Bound, Range}, }; use tree_sitter::{ InputEdit, Node, Parser, Point, Query, QueryCursor, QueryMatch, StreamingIterator, Tree, }; -use tree_sitter_highlight::{HighlightConfiguration, Highlighter}; - -use crate::highlighter::LanguageRegistry; - -use super::{HighlightTheme, Language}; /// A syntax highlighter that supports incremental parsing, multiline text, /// and caching of highlight results. @@ -23,8 +20,6 @@ pub struct SyntaxHighlighter { parser: Parser, old_tree: Option, text: SharedString, - highlighter: Highlighter, - config: Option>, locals_pattern_index: usize, highlights_pattern_index: usize, @@ -43,7 +38,7 @@ pub struct SyntaxHighlighter { /// /// - The `key` is the `start` of the range. /// -The `value` is a tuple of the range (in the entire text) and the highlight name. - cache: BTreeMap, String)>, + cache: BTreeMap, SharedString)>, } impl SyntaxHighlighter { @@ -60,10 +55,7 @@ impl SyntaxHighlighter { /// https://github.com/tree-sitter/tree-sitter/blob/v0.25.5/highlight/src/lib.rs#L336 fn build_combined_injections_query(lang: &str, cx: &App) -> Option { let registry = LanguageRegistry::global(cx); - let config = registry.language(&lang); - let Some(config) = config else { - return None; - }; + let config = registry.language(&lang)?; let mut parser = Parser::new(); _ = parser.set_language(&config.language); @@ -78,9 +70,12 @@ 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 Some(query) = Query::new(&config.language, &query_source).ok() else { - return None; - }; + let query = match Query::new(&config.language, &query_source) { + Ok(query) => Some(query), + Err(err) => { + panic!("failed create Query for language {}, err: {}", lang, err); + } + }?; let mut locals_pattern_index = 0; let mut highlights_pattern_index = 0; @@ -176,8 +171,6 @@ impl SyntaxHighlighter { parser, old_tree: None, text: SharedString::new(""), - highlighter: Highlighter::new(), - config: None, cache: BTreeMap::new(), locals_pattern_index, highlights_pattern_index, @@ -191,35 +184,6 @@ impl SyntaxHighlighter { }) } - pub fn set_language(&mut self, language: impl Into, cx: &App) { - let language = language.into(); - - if self.language == language { - return; - } - - // FIXME: use build_combined_injections_query to build the query. - self.query = None; - if let Some(language) = Language::from_str(&language) { - self.query = Some(language.query()); - _ = self.parser.set_language(&language.config().language); - } else { - if let Some(config) = LanguageRegistry::global(cx).language(&language) { - _ = self.parser.set_language(&config.language); - if let Ok(query) = tree_sitter::Query::new(&config.language, &config.highlights) { - self.query = Some(query); - } - } - } - - self.language = language; - self.old_tree = None; - self.text = SharedString::new(""); - self.highlighter = Highlighter::new(); - self.config = None; - self.cache.clear(); - } - pub fn is_empty(&self) -> bool { self.text.is_empty() } @@ -283,7 +247,7 @@ impl SyntaxHighlighter { &mut self, changed_ranges: Option>, changed_len: isize, - _: &mut App, + cx: &mut App, ) { let Some(tree) = &self.old_tree else { return; @@ -298,7 +262,6 @@ impl SyntaxHighlighter { let mut root_node = tree.root_node(); // Incremental parsing to only update changed ranges. - let mut last_end = 0; if let Some(changed_ranges) = changed_ranges { let mut total_range = 0..0; for change_range in changed_ranges { @@ -338,7 +301,7 @@ impl SyntaxHighlighter { }); // Apply changed_len to reorder the cache to move the range offset - let mut old_cache: BTreeMap, String)> = BTreeMap::new(); + let mut old_cache: BTreeMap, SharedString)> = BTreeMap::new(); std::mem::swap(&mut self.cache, &mut old_cache); // NOTE: 10K lines, about 35ms @@ -362,24 +325,19 @@ impl SyntaxHighlighter { let mut matches = query_cursor.matches(&query, root_node, source); - // TODO: Merge duplicate ranges. - while let Some(m) = matches.next() { // Ref: // https://github.com/tree-sitter/tree-sitter/blob/460118b4c82318b083b4d527c9c750426730f9c0/highlight/src/lib.rs#L556 let (language_name, content_node, _) = self.injection_for_match(None, query, m, source); if let Some(language_name) = language_name { if let Some(content_node) = content_node { - if content_node.start_byte() < last_end { - continue; - } - - let styles = self.handle_injection(&language_name, content_node, source); + let styles = self.handle_injection(&language_name, content_node, source, cx); for (node_range, highlight_name) in styles { - self.cache - .insert(node_range.start, (node_range, highlight_name.to_string())); + self.cache.insert( + node_range.start, + (node_range, highlight_name.to_string().into()), + ); } - last_end = content_node.end_byte(); } continue; @@ -387,23 +345,36 @@ impl SyntaxHighlighter { for cap in m.captures { let node = cap.node; - if node.start_byte() < last_end { + + let Some(highlight_name) = query.capture_names().get(cap.index as usize) else { continue; - } + }; - let highlight_name = query.capture_names()[cap.index as usize]; let node_range: Range = node.start_byte()..node.end_byte(); + let highlight_name = SharedString::from(highlight_name.to_string()); - self.cache.insert( - node_range.start, - (node_range.clone(), highlight_name.to_string()), - ); - last_end = node_range.end; + // Merge near range and same highlight name + let last_item = self.cache.last_key_value().map(|kv| kv.1); + let last_range = last_item.map(|(range, _)| range).unwrap_or(&(0..0)); + let last_highlight_name = last_item.map(|(_, name)| name.clone()); + + if last_range.end <= node_range.start + && last_highlight_name.as_ref() == Some(&highlight_name) + { + self.cache.insert( + last_range.start, + (last_range.start..node_range.end, highlight_name.clone()), + ); + } else { + self.cache + .insert(node_range.start, (node_range, highlight_name.clone())); + } } } + // DO NOT REMOVE THIS PRINT, it's useful for debugging // for item in self.cache.iter() { - // println!("---------- item: {:?}", item); + // println!("item: {:?}", item); // } } @@ -413,6 +384,7 @@ impl SyntaxHighlighter { injection_language: &str, node: Node, source: &[u8], + cx: &App, ) -> Vec<(Range, String)> { let start_offset = node.start_byte(); let end_offset = node.end_byte(); @@ -426,12 +398,11 @@ impl SyntaxHighlighter { if content.is_empty() { return cache; }; - let Some(lang) = super::Language::from_str(injection_language) else { + let Some(config) = LanguageRegistry::global(cx).language(injection_language) else { return cache; }; - let lang_config = lang.config(); let mut parser = Parser::new(); - if parser.set_language(&lang_config.language).is_err() { + if parser.set_language(&config.language).is_err() { return cache; } let Some(tree) = parser.parse(content, None) else { @@ -456,9 +427,10 @@ impl SyntaxHighlighter { break; } - let highlight_name = query.capture_names()[cap.index as usize]; - last_end = node_range.end; - cache.push((node_range, highlight_name.to_string())); + if let Some(highlight_name) = query.capture_names().get(cap.index as usize) { + last_end = node_range.end; + cache.push((node_range, highlight_name.to_string())); + } } } @@ -549,7 +521,7 @@ impl SyntaxHighlighter { /// The argument `range` is the range of the line in the text. /// /// Returns `range` is the range in the line. - pub fn styles( + pub(crate) fn styles( &self, range: &Range, theme: &HighlightTheme, @@ -558,25 +530,42 @@ impl SyntaxHighlighter { let start_offset = range.start; let mut last_range = start_offset..start_offset; - // NOTE: the ranges in the cache may have duplicates, so we need to merge them. - for (_, (node_range, name)) in self.cache.range(range.start..) { - // TODO: If break, the `comment.doc` will not work. - // Ref: https://github.com/longbridge/gpui-component/pull/904/commits/d8f886939d3b472f228c1ce72154a951e98f32c5 - if node_range.end > range.end { + // NOTE: Iterate over the cache and print the range and style for each item. + // for (_, (range, style)) in self.cache.iter() { + // println!("-- range: {:?}, style: {:?}", range, style); + // } + + let mut cursor = self.cache.lower_bound(Bound::Included(&range.start)); + // Move to the previous item if the current item is not the start of the range. + // This is for case like JsDoc, where token may contains multiple lines. + if cursor.key() != Some(&range.start) { + cursor.move_prev(); + } + + while let Some((node_range, name)) = cursor.value() { + // Break loop if the node_range is out of the range + if node_range.start > range.end { break; } - // let range_in_line = node_range.start..node_range.end; + let mut node_range = node_range.start.max(range.start)..node_range.end.min(range.end); + // Avoid start larger than end + if node_range.start > node_range.end { + node_range.end = node_range.start; + } // Ensure every range is connected. if last_range.end < node_range.start { styles.push((last_range.end..node_range.start, HighlightStyle::default())); } - let style = theme.style(&name).unwrap_or_default(); - - styles.push((node_range.clone(), style)); last_range = node_range.clone(); + styles.push(( + node_range.clone(), + theme.style(name.as_ref()).unwrap_or_default(), + )); + + cursor.move_next(); } // If the matched styles is empty, return a default range. @@ -589,6 +578,179 @@ impl SyntaxHighlighter { styles.push((last_range.end..range.end, HighlightStyle::default())); } + let styles = unique_styles(styles); + + // NOTE: DO NOT remove this comment, it is used for debugging. + // for style in &result { + // println!("---- style: {:?} - {:?}", style.0, style.1.color); + // } + // println!("--------------------------------"); + styles } } + +/// To merge intersection ranges +/// +/// ``` +/// vec![ +/// (0..10, clean), +/// (0..10, clean), +/// (5..11, red), +/// (10..15, green), +/// (15..30, clean), +/// (29..35, blue), +/// (35..40, green), +/// ]; +/// ``` +/// +/// to +/// +/// ``` +/// vec![ +/// (0..5, clean), +/// (5..10, red), +/// (10..11, green), +/// (11..15, green), +/// (15..29, clean), +/// (29..30, blue), +/// (30..35, blue), +/// (35..40, green), +/// ]; +/// ``` +pub(crate) fn unique_styles( + styles: Vec<(Range, HighlightStyle)>, +) -> Vec<(Range, HighlightStyle)> { + let mut result: Vec<(Range, HighlightStyle)> = vec![]; + let mut current_range: Option<(Range, HighlightStyle)> = None; + + for (range, style) in styles.into_iter() { + if range.is_empty() { + continue; + } + + if let Some((last_range, last_style)) = current_range.as_mut() { + if last_style.color == style.color && range.start <= last_range.end { + // Merge overlapping or adjacent ranges with the same style + last_range.end = last_range.end.max(range.end); + } else if range.start < last_range.end { + // Split overlapping ranges with different styles + let overlap_start = range.start; + let overlap_end = last_range.end.min(range.end); + + if overlap_start > last_range.start { + result.push((last_range.start..overlap_start, *last_style)); + } + + result.push((overlap_start..overlap_end, style)); + + last_range.end = overlap_start; + if overlap_end < range.end { + current_range = Some((overlap_end..range.end, style)); + } else { + current_range = None; + } + } else { + // Push the completed range and start a new one + result.push((last_range.clone(), *last_style)); + current_range = Some((range, style)); + } + } else { + current_range = Some((range, style)); + } + } + + if let Some((last_range, last_style)) = current_range { + result.push((last_range, last_style)); + } + + result +} + +#[cfg(test)] +mod tests { + use gpui::Hsla; + + use super::*; + use crate::Colorize as _; + + fn color_style(color: Hsla) -> HighlightStyle { + let mut style = HighlightStyle::default(); + style.color = Some(color); + style + } + + #[track_caller] + fn assert_unique_styles( + left: Vec<(Range, HighlightStyle)>, + right: Vec<(Range, HighlightStyle)>, + ) { + fn color_name(c: Option) -> String { + match c { + Some(c) => { + if c == gpui::red() { + "red".to_string() + } else if c == gpui::green() { + "green".to_string() + } else if c == gpui::blue() { + "blue".to_string() + } else { + c.to_hex() + } + } + None => "clean".to_string(), + } + } + + let left = unique_styles(left); + if left.len() != right.len() { + println!("\n---------------------------------------------"); + for (range, style) in left.iter() { + println!("({:?}, {})", range, color_name(style.color)); + } + println!("---------------------------------------------"); + panic!("left {} styles, right {} styles", left.len(), right.len()); + } + for (left, right) in left.into_iter().zip(right) { + if left.1.color != right.1.color || left.0 != right.0 { + panic!( + "\n left: ({:?}, {})\nright: ({:?}, {})\n", + left.0, + color_name(left.1.color), + right.0, + color_name(right.1.color) + ); + } + } + } + + #[test] + fn test_unique_styles() { + let red = color_style(gpui::red()); + let green = color_style(gpui::green()); + let blue = color_style(gpui::blue()); + let clean = HighlightStyle::default(); + + assert_unique_styles( + vec![ + (0..10, clean), + (0..10, clean), + (5..11, red), + (10..15, green), + (15..30, clean), + (29..35, blue), + (35..40, green), + ], + vec![ + (0..5, clean), + (5..10, red), + (10..11, green), + (11..15, green), + (15..29, clean), + (29..30, blue), + (30..35, blue), + (35..40, green), + ], + ); + } +} diff --git a/crates/ui/src/highlighter/languages.rs b/crates/ui/src/highlighter/languages.rs index 115960cd..a52ae918 100644 --- a/crates/ui/src/highlighter/languages.rs +++ b/crates/ui/src/highlighter/languages.rs @@ -1,39 +1,39 @@ use gpui::SharedString; -use tree_sitter::Query; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, enum_iterator::Sequence)] pub enum Language { - Json, - Markdown, - MarkdownInline, - Toml, - Yaml, - Rust, - Go, - C, - Cpp, - JavaScript, - Zig, - Java, - Python, - Ruby, Bash, - Html, - Css, - Swift, - Scala, - Sql, - CSharp, - GraphQL, - Proto, - Make, + C, CMake, - TypeScript, - Tsx, + CSharp, + Cpp, + Css, Diff, + Ejs, Elixir, Erb, - Ejs, + Go, + GraphQL, + Html, + Java, + JavaScript, + JsDoc, + Json, + Make, + Markdown, + MarkdownInline, + Proto, + Python, + Ruby, + Rust, + Scala, + Sql, + Swift, + Toml, + Tsx, + TypeScript, + Yaml, + Zig, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -48,6 +48,7 @@ pub struct LanguageConfig { impl LanguageConfig { pub fn new( + name: impl Into, language: tree_sitter::Language, injection_languages: Vec, highlights: &str, @@ -55,7 +56,7 @@ impl LanguageConfig { locals: &str, ) -> Self { Self { - name: language.name().unwrap_or_default().into(), + name: name.into(), language, injection_languages, highlights: SharedString::from(highlights.to_string()), @@ -77,73 +78,75 @@ impl Language { pub fn name(&self) -> &'static str { match self { - Self::Json => "json", - Self::Markdown => "markdown", - Self::MarkdownInline => "markdown_inline", - Self::Toml => "toml", - Self::Yaml => "yaml", - Self::Rust => "rust", - Self::Go => "go", - Self::C => "c", - Self::Cpp => "cpp", - Self::JavaScript => "javascript", - Self::Zig => "zig", - Self::Java => "java", - Self::Python => "python", - Self::Ruby => "ruby", Self::Bash => "bash", - Self::Html => "html", - Self::Css => "css", - Self::Swift => "swift", - Self::Scala => "scala", - Self::Sql => "sql", - Self::CSharp => "csharp", - Self::GraphQL => "graphql", - Self::Proto => "proto", - Self::Make => "make", + Self::C => "c", Self::CMake => "cmake", - Self::TypeScript => "typescript", - Self::Tsx => "tsx", + Self::CSharp => "csharp", + Self::Cpp => "cpp", + Self::Css => "css", Self::Diff => "diff", + Self::Ejs => "ejs", Self::Elixir => "elixir", Self::Erb => "erb", - Self::Ejs => "ejs", + Self::Go => "go", + Self::GraphQL => "graphql", + Self::Html => "html", + Self::Java => "java", + Self::JavaScript => "javascript", + Self::JsDoc => "jsdoc", + Self::Json => "json", + Self::Make => "make", + Self::Markdown => "markdown", + Self::MarkdownInline => "markdown_inline", + Self::Proto => "proto", + Self::Python => "python", + Self::Ruby => "ruby", + Self::Rust => "rust", + Self::Scala => "scala", + Self::Sql => "sql", + Self::Swift => "swift", + Self::Toml => "toml", + Self::Tsx => "tsx", + Self::TypeScript => "typescript", + Self::Yaml => "yaml", + Self::Zig => "zig", } } pub fn from_str(s: &str) -> Option { match s { - "json" | "jsonc" => Some(Self::Json), - "markdown" | "md" | "mdx" => Some(Self::Markdown), - "markdown_inline" | "markdown-inline" => Some(Self::MarkdownInline), - "toml" => Some(Self::Toml), - "yaml" | "yml" => Some(Self::Yaml), - "rust" | "rs" => Some(Self::Rust), - "go" => Some(Self::Go), - "c" => Some(Self::C), - "cpp" | "c++" => Some(Self::Cpp), - "javascript" | "js" => Some(Self::JavaScript), - "zig" => Some(Self::Zig), - "java" => Some(Self::Java), - "python" | "py" => Some(Self::Python), - "ruby" | "rb" => Some(Self::Ruby), "bash" | "sh" => Some(Self::Bash), - "html" => Some(Self::Html), - "css" | "scss" => Some(Self::Css), - "swift" => Some(Self::Swift), - "scala" => Some(Self::Scala), - "sql" => Some(Self::Sql), - "csharp" | "cs" => Some(Self::CSharp), - "graphql" => Some(Self::GraphQL), - "proto" | "protobuf" => Some(Self::Proto), - "make" | "makefile" => Some(Self::Make), + "c" => Some(Self::C), "cmake" => Some(Self::CMake), - "typescript" | "ts" => Some(Self::TypeScript), - "tsx" => Some(Self::Tsx), + "cpp" | "c++" => Some(Self::Cpp), + "csharp" | "cs" => Some(Self::CSharp), + "css" | "scss" => Some(Self::Css), "diff" => Some(Self::Diff), + "ejs" => Some(Self::Ejs), "elixir" | "ex" => Some(Self::Elixir), "erb" => Some(Self::Erb), - "ejs" => Some(Self::Ejs), + "go" => Some(Self::Go), + "graphql" => Some(Self::GraphQL), + "html" => Some(Self::Html), + "java" => Some(Self::Java), + "javascript" | "js" => Some(Self::JavaScript), + "jsdoc" => Some(Self::JsDoc), + "json" | "jsonc" => Some(Self::Json), + "make" | "makefile" => Some(Self::Make), + "markdown" | "md" | "mdx" => Some(Self::Markdown), + "markdown_inline" | "markdown-inline" => Some(Self::MarkdownInline), + "proto" | "protobuf" => Some(Self::Proto), + "python" | "py" => Some(Self::Python), + "ruby" | "rb" => Some(Self::Ruby), + "rust" | "rs" => Some(Self::Rust), + "scala" => Some(Self::Scala), + "sql" => Some(Self::Sql), + "swift" => Some(Self::Swift), + "toml" => Some(Self::Toml), + "tsx" => Some(Self::Tsx), + "typescript" | "ts" => Some(Self::TypeScript), + "yaml" | "yml" => Some(Self::Yaml), + "zig" => Some(Self::Zig), _ => None, } } @@ -155,6 +158,18 @@ impl Language { Self::MarkdownInline => vec![], Self::Html => vec!["javascript", "css"], Self::Rust => vec!["rust"], + Self::JavaScript | Self::TypeScript => vec![ + "jsdoc", + "json", + "css", + "html", + "sql", + "typescript", + "javascript", + "tsx", + "yaml", + "graphql", + ], _ => vec![], } .into_iter() @@ -162,11 +177,6 @@ impl Language { .collect() } - pub(super) fn query(&self) -> Query { - let config = self.config(); - Query::new(&config.language, &config.highlights).unwrap() - } - /// Return the language info for the language. /// /// (language, query, injection, locals) @@ -210,7 +220,7 @@ impl Language { ), Self::Go => ( tree_sitter_go::LANGUAGE, - tree_sitter_go::HIGHLIGHTS_QUERY, + include_str!("languages/go/highlights.scm"), "", "", ), @@ -228,10 +238,16 @@ impl Language { ), Self::JavaScript => ( tree_sitter_javascript::LANGUAGE, - tree_sitter_javascript::HIGHLIGHT_QUERY, - tree_sitter_javascript::INJECTIONS_QUERY, + include_str!("languages/javascript/highlights.scm"), + include_str!("languages/javascript/injections.scm"), tree_sitter_javascript::LOCALS_QUERY, ), + Self::JsDoc => ( + tree_sitter_jsdoc::LANGUAGE, + tree_sitter_jsdoc::HIGHLIGHTS_QUERY, + "", + "", + ), Self::Zig => ( tree_sitter_zig::LANGUAGE, tree_sitter_zig::HIGHLIGHTS_QUERY, @@ -299,8 +315,8 @@ impl Language { Self::CMake => (tree_sitter_cmake::LANGUAGE, "", "", ""), Self::TypeScript => ( tree_sitter_typescript::LANGUAGE_TYPESCRIPT, - tree_sitter_typescript::HIGHLIGHTS_QUERY, - "", + include_str!("languages/typescript/highlights.scm"), + include_str!("languages/javascript/injections.scm"), tree_sitter_typescript::LOCALS_QUERY, ), Self::Tsx => ( @@ -338,6 +354,7 @@ impl Language { let language = tree_sitter::Language::new(language); LanguageConfig::new( + self.name(), language, self.injection_languages(), query, diff --git a/crates/ui/src/highlighter/languages/go/highlights.scm b/crates/ui/src/highlighter/languages/go/highlights.scm new file mode 100644 index 00000000..f73e5171 --- /dev/null +++ b/crates/ui/src/highlighter/languages/go/highlights.scm @@ -0,0 +1,136 @@ +; Function calls + +(call_expression + function: (identifier) @function) + +(call_expression + function: (identifier) @function.builtin + (#match? @function.builtin "^(append|cap|close|complex|copy|delete|imag|len|make|new|panic|print|println|real|recover)$")) + +(call_expression + function: (selector_expression + field: (field_identifier) @function.method)) + +; Function definitions + +(function_declaration + name: (identifier) @function) + +(method_declaration + name: (field_identifier) @function.method) +(method_elem + name: (field_identifier) @function.method) + +; Identifiers +(keyed_element + . + (literal_element + (identifier) @variable.member)) + +(type_identifier) @type +(field_identifier) @variable.member +(identifier) @variable +(package_identifier) @namespace + +; Operators + +[ + "--" + "-" + "-=" + ":=" + "!" + "!=" + "..." + "*" + "*" + "*=" + "/" + "/=" + "&" + "&&" + "&=" + "%" + "%=" + "^" + "^=" + "+" + "++" + "+=" + "<-" + "<" + "<<" + "<<=" + "<=" + "=" + "==" + ">" + ">=" + ">>" + ">>=" + "|" + "|=" + "||" + "~" +] @operator + +; Keywords + +[ + "break" + "case" + "chan" + "const" + "continue" + "default" + "defer" + "else" + "fallthrough" + "for" + "func" + "go" + "goto" + "if" + "import" + "interface" + "map" + "package" + "range" + "return" + "select" + "struct" + "switch" + "type" + "var" +] @keyword + +; Literals + +[ + (interpreted_string_literal) + (raw_string_literal) + (rune_literal) +] @string + +(escape_sequence) @string.escape + +[ + (int_literal) + (float_literal) + (imaginary_literal) +] @number + +(const_spec + name: (identifier) @constant) + +[ + (true) + (false) +] @boolean + +[ + (nil) + (iota) +] @constant.builtin + +(comment) @comment diff --git a/crates/ui/src/highlighter/languages/javascript/highlights.scm b/crates/ui/src/highlighter/languages/javascript/highlights.scm new file mode 100644 index 00000000..b7b6106d --- /dev/null +++ b/crates/ui/src/highlighter/languages/javascript/highlights.scm @@ -0,0 +1,207 @@ +; Variables +;---------- + +(identifier) @variable + +; Properties +;----------- + +(property_identifier) @property +(shorthand_property_identifier) @property +(shorthand_property_identifier_pattern) @property +(private_property_identifier) @property + +; Function and method definitions +;-------------------------------- + +(function_expression + name: (identifier) @function) +(function_declaration + name: (identifier) @function) +(method_definition + name: (property_identifier) @function.method) + +(pair + key: (property_identifier) @function.method + value: [(function_expression) (arrow_function)]) + +(assignment_expression + left: (member_expression + property: (property_identifier) @function.method) + right: [(function_expression) (arrow_function)]) + +(variable_declarator + name: (identifier) @function + value: [(function_expression) (arrow_function)]) + +(assignment_expression + left: (identifier) @function + right: [(function_expression) (arrow_function)]) + +; Function and method calls +;-------------------------- + +(call_expression + function: (identifier) @function) + +(call_expression + function: (member_expression + property: (property_identifier) @function.method)) + +; Special identifiers +;-------------------- + +((identifier) @type + (#match? @type "^[A-Z]")) + +([ + (identifier) + (shorthand_property_identifier) + (shorthand_property_identifier_pattern) + ] @constant + (#match? @constant "^_*[A-Z_][A-Z\\d_]*$")) + +((identifier) @variable.builtin + (#match? @variable.builtin "^(arguments|module|console|window|document)$") + (#is-not? local)) + +((identifier) @function.builtin + (#eq? @function.builtin "require") + (#is-not? local)) + +; Literals +;--------- + +(this) @variable.builtin +(super) @variable.builtin + +[ + (true) + (false) + (null) + (undefined) +] @constant.builtin + +(comment) @comment + +[ + (string) + (template_string) +] @string + +(regex) @string.special +(number) @number + +; Tokens +;------- + +[ + ";" + (optional_chain) + "." + "," +] @punctuation.delimiter + +[ + "-" + "--" + "-=" + "+" + "++" + "+=" + "*" + "*=" + "**" + "**=" + "/" + "/=" + "%" + "%=" + "<" + "<=" + "<<" + "<<=" + "=" + "==" + "===" + "!" + "!=" + "!==" + "=>" + ">" + ">=" + ">>" + ">>=" + ">>>" + ">>>=" + "~" + "^" + "&" + "|" + "^=" + "&=" + "|=" + "&&" + "||" + "??" + "&&=" + "||=" + "??=" +] @operator + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +(template_substitution + "${" @punctuation.special + "}" @punctuation.special) @embedded + +[ + "as" + "async" + "await" + "break" + "case" + "catch" + "class" + "const" + "continue" + "debugger" + "default" + "delete" + "do" + "else" + "export" + "extends" + "finally" + "for" + "from" + "function" + "get" + "if" + "import" + "in" + "instanceof" + "let" + "new" + "of" + "return" + "set" + "static" + "switch" + "target" + "throw" + "try" + "typeof" + "var" + "void" + "while" + "with" + "yield" +] @keyword diff --git a/crates/ui/src/highlighter/languages/javascript/injections.scm b/crates/ui/src/highlighter/languages/javascript/injections.scm new file mode 100644 index 00000000..15e2f3b2 --- /dev/null +++ b/crates/ui/src/highlighter/languages/javascript/injections.scm @@ -0,0 +1,54 @@ +(((comment) @_jsdoc_comment + (#match? @_jsdoc_comment "(?s)^/[*][*][^*].*[*]/$")) @injection.content + (#set! injection.language "jsdoc")) + +((regex) @injection.content + (#set! injection.language "regex")) + +(call_expression + function: (identifier) @_name (#eq? @_name "css") + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "css")) +) + +(call_expression + function: (identifier) @_name (#eq? @_name "html") + arguments: (template_string) @injection.content + (#set! injection.language "html") +) + +(call_expression + function: (identifier) @_name (#eq? @_name "js") + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "javascript")) +) + +(call_expression + function: (identifier) @_name (#eq? @_name "json") + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "json")) +) + +(call_expression + function: (identifier) @_name (#eq? @_name "sql") + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "sql")) +) + +(call_expression + function: (identifier) @_name (#eq? @_name "ts") + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "typescript")) +) + +(call_expression + function: (identifier) @_name (#match? @_name "^ya?ml$") + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "yaml")) +) + +(call_expression + function: (identifier) @_name (#match? @_name "^g(raph)?ql$") + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "graphql")) +) diff --git a/crates/ui/src/highlighter/languages/typescript/highlights.scm b/crates/ui/src/highlighter/languages/typescript/highlights.scm new file mode 100644 index 00000000..eee3f80d --- /dev/null +++ b/crates/ui/src/highlighter/languages/typescript/highlights.scm @@ -0,0 +1,243 @@ +; Types +; Variables +;---------- + +(identifier) @variable + +; Properties +;----------- + +(property_identifier) @property +(shorthand_property_identifier) @property +(shorthand_property_identifier_pattern) @property +(private_property_identifier) @property + +; Function and method definitions +;-------------------------------- + +(function_expression + name: (identifier) @function) +(function_declaration + name: (identifier) @function) +(method_definition + name: (property_identifier) @function.method) + +(pair + key: (property_identifier) @function.method + value: [(function_expression) (arrow_function)]) + +(assignment_expression + left: (member_expression + property: (property_identifier) @function.method) + right: [(function_expression) (arrow_function)]) + +(variable_declarator + name: (identifier) @function + value: [(function_expression) (arrow_function)]) + +(assignment_expression + left: (identifier) @function + right: [(function_expression) (arrow_function)]) + +; Function and method calls +;-------------------------- + +(call_expression + function: (identifier) @function) + +(call_expression + function: (member_expression + property: (property_identifier) @function.method)) + +; Special identifiers +;-------------------- + +((identifier) @type + (#match? @type "^[A-Z]")) + +([ + (identifier) + (shorthand_property_identifier) + (shorthand_property_identifier_pattern) + ] @constant + (#match? @constant "^_*[A-Z_][A-Z\\d_]*$")) + +((identifier) @variable.builtin + (#match? @variable.builtin "^(arguments|module|console|window|document)$") + (#is-not? local)) + +((identifier) @function.builtin + (#eq? @function.builtin "require") + (#is-not? local)) + +; Literals +;--------- + +(this) @variable.builtin +(super) @variable.builtin + +[ + (true) + (false) + (null) + (undefined) +] @constant.builtin + +(comment) @comment + +[ + (string) + (template_string) +] @string + +(regex) @string.special +(number) @number + +; Tokens +;------- + +[ + ";" + (optional_chain) + "." + "," +] @punctuation.delimiter + +[ + "-" + "--" + "-=" + "+" + "++" + "+=" + "*" + "*=" + "**" + "**=" + "/" + "/=" + "%" + "%=" + "<" + "<=" + "<<" + "<<=" + "=" + "==" + "===" + "!" + "!=" + "!==" + "=>" + ">" + ">=" + ">>" + ">>=" + ">>>" + ">>>=" + "~" + "^" + "&" + "|" + "^=" + "&=" + "|=" + "&&" + "||" + "??" + "&&=" + "||=" + "??=" +] @operator + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +(template_substitution + "${" @punctuation.special + "}" @punctuation.special) @embedded + +[ + "as" + "async" + "await" + "break" + "case" + "catch" + "class" + "const" + "continue" + "debugger" + "default" + "delete" + "do" + "else" + "export" + "extends" + "finally" + "for" + "from" + "function" + "get" + "if" + "import" + "in" + "instanceof" + "let" + "new" + "of" + "return" + "set" + "static" + "switch" + "target" + "throw" + "try" + "typeof" + "var" + "void" + "while" + "with" + "yield" +] @keyword + + +(type_identifier) @type +(predefined_type) @type.builtin + +((identifier) @type + (#match? @type "^[A-Z]")) + +(type_arguments + "<" @punctuation.bracket + ">" @punctuation.bracket) + +; Variables + +(required_parameter (identifier) @variable.parameter) +(optional_parameter (identifier) @variable.parameter) + +; Keywords + +[ "abstract" + "declare" + "enum" + "export" + "implements" + "interface" + "keyof" + "namespace" + "private" + "protected" + "public" + "type" + "readonly" + "override" + "satisfies" +] @keyword