highlighter: Fix injection language highlight. (#931)

Still have a bit bug for highlight:

<img width="1012" alt="image"
src="https://github.com/user-attachments/assets/7ec39c85-c20c-4231-8752-b2a7a80f5570"
/>
This commit is contained in:
Jason Lee 2025-06-11 15:26:27 +08:00 committed by GitHub
parent c333fd7106
commit 79ff6ba9cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1180 additions and 280 deletions

101
Cargo.lock generated
View file

@ -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",

View file

@ -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"),

View file

@ -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);
}
})();

View file

@ -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<string, any>;
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());
}

View file

@ -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" }

View file

@ -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<Tree>,
text: SharedString,
highlighter: Highlighter,
config: Option<Arc<HighlightConfiguration>>,
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<usize, (Range<usize>, String)>,
cache: BTreeMap<usize, (Range<usize>, 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<Self> {
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<SharedString>, 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<impl ExactSizeIterator<Item = tree_sitter::Range>>,
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<usize, (Range<usize>, String)> = BTreeMap::new();
let mut old_cache: BTreeMap<usize, (Range<usize>, 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<usize> = 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<usize>, 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<usize>,
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<usize>, HighlightStyle)>,
) -> Vec<(Range<usize>, HighlightStyle)> {
let mut result: Vec<(Range<usize>, HighlightStyle)> = vec![];
let mut current_range: Option<(Range<usize>, 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<usize>, HighlightStyle)>,
right: Vec<(Range<usize>, HighlightStyle)>,
) {
fn color_name(c: Option<Hsla>) -> 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),
],
);
}
}

View file

@ -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<SharedString>,
language: tree_sitter::Language,
injection_languages: Vec<SharedString>,
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<Self> {
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,

View file

@ -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

View file

@ -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

View file

@ -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"))
)

View file

@ -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