Add basic TypeScript and TSX support

Max Brunsfeld and Keith Simmons created

Co-Authored-By: Keith Simmons <keith@zed.dev>

Change summary

Cargo.lock                                     |  11 +
crates/zed/Cargo.toml                          |   1 
crates/zed/languages/tsx/brackets.scm          |   1 
crates/zed/languages/tsx/config.toml           |  12 +
crates/zed/languages/tsx/highlights-jsx.scm    |   0 
crates/zed/languages/tsx/highlights.scm        |   1 
crates/zed/languages/tsx/indents.scm           |   1 
crates/zed/languages/tsx/outline.scm           |   1 
crates/zed/languages/typescript/brackets.scm   |   5 
crates/zed/languages/typescript/config.toml    |  12 +
crates/zed/languages/typescript/highlights.scm | 219 ++++++++++++++++++++
crates/zed/languages/typescript/indents.scm    |  15 +
crates/zed/languages/typescript/outline.scm    |  55 +++++
crates/zed/src/language.rs                     |  52 +++-
14 files changed, 373 insertions(+), 13 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5415,6 +5415,16 @@ dependencies = [
  "tree-sitter",
 ]
 
+[[package]]
+name = "tree-sitter-typescript"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e8ed0ecb931cdff13c6a13f45ccd615156e2779d9ffb0395864e05505e6e86d"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "ttf-parser"
 version = "0.9.0"
@@ -6025,6 +6035,7 @@ dependencies = [
  "tree-sitter-json",
  "tree-sitter-markdown",
  "tree-sitter-rust",
+ "tree-sitter-typescript",
  "unindent",
  "url",
  "util",

crates/zed/Cargo.toml 🔗

@@ -98,6 +98,7 @@ tree-sitter-c = "0.20.1"
 tree-sitter-json = "0.19.0"
 tree-sitter-rust = "0.20.1"
 tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
+tree-sitter-typescript = "0.20.1"
 url = "2.2"
 
 [dev-dependencies]

crates/zed/languages/tsx/config.toml 🔗

@@ -0,0 +1,12 @@
+name = "TSX"
+path_suffixes = ["tsx"]
+line_comment = "// "
+autoclose_before = ";:.,=}])>"
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "<", end = ">", close = false, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false },
+    { start = "/*", end = " */", close = true, newline = false },
+]

crates/zed/languages/typescript/config.toml 🔗

@@ -0,0 +1,12 @@
+name = "TypeScript"
+path_suffixes = ["ts"]
+line_comment = "// "
+autoclose_before = ";:.,=}])>"
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "<", end = ">", close = false, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false },
+    { start = "/*", end = " */", close = true, newline = false },
+]

crates/zed/languages/typescript/highlights.scm 🔗

@@ -0,0 +1,219 @@
+; Variables
+
+(identifier) @variable
+
+; Properties
+
+(property_identifier) @property
+
+; Function and method calls
+
+(call_expression
+  function: (identifier) @function)
+
+(call_expression
+  function: (member_expression
+    property: (property_identifier) @function.method))
+
+; Function and method definitions
+
+(function
+  name: (identifier) @function)
+(function_declaration
+  name: (identifier) @function)
+(method_definition
+  name: (property_identifier) @function.method)
+
+(pair
+  key: (property_identifier) @function.method
+  value: [(function) (arrow_function)])
+
+(assignment_expression
+  left: (member_expression
+    property: (property_identifier) @function.method)
+  right: [(function) (arrow_function)])
+
+(variable_declarator
+  name: (identifier) @function
+  value: [(function) (arrow_function)])
+
+(assignment_expression
+  left: (identifier) @function
+  right: [(function) (arrow_function)])
+
+; Special identifiers
+
+((identifier) @constructor
+ (#match? @constructor "^[A-Z]"))
+
+([
+  (identifier)
+  (shorthand_property_identifier)
+  (shorthand_property_identifier_pattern)
+ ] @constant
+ (#match? @constant "^[A-Z_][A-Z\\d_]+$"))
+
+; 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
+
+(template_substitution
+  "${" @punctuation.special
+  "}" @punctuation.special) @embedded
+
+[
+  ";"
+  "?."
+  "."
+  ","
+] @punctuation.delimiter
+
+[
+  "-"
+  "--"
+  "-="
+  "+"
+  "++"
+  "+="
+  "*"
+  "*="
+  "**"
+  "**="
+  "/"
+  "/="
+  "%"
+  "%="
+  "<"
+  "<="
+  "<<"
+  "<<="
+  "="
+  "=="
+  "==="
+  "!"
+  "!="
+  "!=="
+  "=>"
+  ">"
+  ">="
+  ">>"
+  ">>="
+  ">>>"
+  ">>>="
+  "~"
+  "^"
+  "&"
+  "|"
+  "^="
+  "&="
+  "|="
+  "&&"
+  "||"
+  "??"
+  "&&="
+  "||="
+  "??="
+] @operator
+
+[
+  "("
+  ")"
+  "["
+  "]"
+  "{"
+  "}"
+]  @punctuation.bracket
+
+[
+  "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
+
+; Types
+
+(type_identifier) @type
+(predefined_type) @type.builtin
+
+((identifier) @type
+ (#match? @type "^[A-Z]"))
+
+(type_arguments
+  "<" @punctuation.bracket
+  ">" @punctuation.bracket)
+
+; Keywords
+
+[ "abstract"
+  "declare"
+  "enum"
+  "export"
+  "implements"
+  "interface"
+  "keyof"
+  "namespace"
+  "private"
+  "protected"
+  "public"
+  "type"
+  "readonly"
+  "override"
+] @keyword

crates/zed/languages/typescript/indents.scm 🔗

@@ -0,0 +1,15 @@
+[
+    (call_expression)
+    (assignment_expression)
+    (member_expression)
+    (lexical_declaration)
+    (variable_declaration)
+    (assignment_expression)
+    (if_statement)
+    (for_statement)
+] @indent
+
+(_ "[" "]" @end) @indent
+(_ "<" ">" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent

crates/zed/languages/typescript/outline.scm 🔗

@@ -0,0 +1,55 @@
+(internal_module
+    "namespace" @context
+    name: (_) @name) @item
+
+(enum_declaration
+    "enum" @context
+    name: (_) @name) @item
+
+(function_declaration
+    "async"? @context
+    "function" @context
+    name: (_) @name
+    parameters: (formal_parameters
+      "(" @context
+      ")" @context)) @item
+
+(interface_declaration
+    "interface" @context
+    name: (_) @name) @item
+
+(program
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (_) @name) @item))
+
+(class_declaration
+    "class" @context
+    name: (_) @name) @item
+
+(method_definition
+    [
+        "get"
+        "set"
+        "async"
+        "*"
+        "readonly"
+        "static"
+        (override_modifier)
+        (accessibility_modifier)
+    ]* @context
+    name: (_) @name
+    parameters: (formal_parameters
+      "(" @context
+      ")" @context)) @item
+
+(public_field_definition
+    [
+        "declare"
+        "readonly"
+        "abstract"
+        "static"
+        (accessibility_modifier)
+    ]* @context
+    name: (_) @name) @item

crates/zed/src/language.rs 🔗

@@ -555,6 +555,11 @@ pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegi
             tree_sitter_rust::language(),
             Some(Arc::new(RustLspAdapter)),
         ),
+        (
+            "tsx",
+            tree_sitter_typescript::language_tsx(),
+            None, //
+        ),
         (
             "typescript",
             tree_sitter_typescript::language_typescript(),
@@ -578,17 +583,26 @@ fn language(
     )
     .unwrap();
     let mut language = Language::new(config, Some(grammar));
-    if let Some(query) = load_query(&format!("{}/highlights.scm", name)) {
-        language = language.with_highlights_query(query.as_ref()).unwrap();
+
+    if let Some(query) = load_query(name, "/highlights") {
+        language = language
+            .with_highlights_query(query.as_ref())
+            .expect("failed to evaluate highlights query");
     }
-    if let Some(query) = load_query(&format!("{}/brackets.scm", name)) {
-        language = language.with_brackets_query(query.as_ref()).unwrap();
+    if let Some(query) = load_query(name, "/brackets") {
+        language = language
+            .with_brackets_query(query.as_ref())
+            .expect("failed to load brackets query");
     }
-    if let Some(query) = load_query(&format!("{}/indents.scm", name)) {
-        language = language.with_indents_query(query.as_ref()).unwrap();
+    if let Some(query) = load_query(name, "/indents") {
+        language = language
+            .with_indents_query(query.as_ref())
+            .expect("failed to load indents query");
     }
-    if let Some(query) = load_query(&format!("{}/outline.scm", name)) {
-        language = language.with_outline_query(query.as_ref()).unwrap();
+    if let Some(query) = load_query(name, "/outline") {
+        language = language
+            .with_outline_query(query.as_ref())
+            .expect("failed to load outline query");
     }
     if let Some(lsp_adapter) = lsp_adapter {
         language = language.with_lsp_adapter(lsp_adapter)
@@ -596,11 +610,23 @@ fn language(
     language
 }
 
-fn load_query(path: &str) -> Option<Cow<'static, str>> {
-    LanguageDir::get(path).map(|item| match item.data {
-        Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
-        Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
-    })
+fn load_query(name: &str, filename_prefix: &str) -> Option<Cow<'static, str>> {
+    let mut result = None;
+    for path in LanguageDir::iter() {
+        if let Some(remainder) = path.strip_prefix(name) {
+            if remainder.starts_with(filename_prefix) {
+                let contents = match LanguageDir::get(path.as_ref()).unwrap().data {
+                    Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
+                    Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
+                };
+                match &mut result {
+                    None => result = Some(contents),
+                    Some(r) => r.to_mut().push_str(contents.as_ref()),
+                }
+            }
+        }
+    }
+    result
 }
 
 #[cfg(test)]