Port `zed/src/languages` to zed2

Julia created

Change summary

Cargo.lock                                          |   5 
crates/gpui2/src/app.rs                             |   4 
crates/language2/src/language2.rs                   |  46 
crates/prettier2/src/prettier2.rs                   | 238 +++---
crates/project2/src/project2.rs                     |  36 
crates/settings2/src/keymap_file.rs                 |   4 
crates/theme2/src/theme2.rs                         |  18 
crates/zed2/Cargo.toml                              |   5 
crates/zed2/src/languages.rs                        | 273 +++++++
crates/zed2/src/languages/bash/brackets.scm         |   3 
crates/zed2/src/languages/bash/config.toml          |   9 
crates/zed2/src/languages/bash/highlights.scm       |  59 +
crates/zed2/src/languages/c.rs                      | 321 ++++++++
crates/zed2/src/languages/c/brackets.scm            |   3 
crates/zed2/src/languages/c/config.toml             |  12 
crates/zed2/src/languages/c/embedding.scm           |  43 +
crates/zed2/src/languages/c/highlights.scm          | 109 ++
crates/zed2/src/languages/c/indents.scm             |   9 
crates/zed2/src/languages/c/injections.scm          |   7 
crates/zed2/src/languages/c/outline.scm             |  70 +
crates/zed2/src/languages/c/overrides.scm           |   2 
crates/zed2/src/languages/cpp/brackets.scm          |   3 
crates/zed2/src/languages/cpp/config.toml           |  12 
crates/zed2/src/languages/cpp/embedding.scm         |  61 +
crates/zed2/src/languages/cpp/highlights.scm        | 158 ++++
crates/zed2/src/languages/cpp/indents.scm           |   7 
crates/zed2/src/languages/cpp/injections.scm        |   7 
crates/zed2/src/languages/cpp/outline.scm           | 149 +++
crates/zed2/src/languages/cpp/overrides.scm         |   2 
crates/zed2/src/languages/css.rs                    | 130 +++
crates/zed2/src/languages/css/brackets.scm          |   3 
crates/zed2/src/languages/css/config.toml           |  13 
crates/zed2/src/languages/css/highlights.scm        |  78 ++
crates/zed2/src/languages/css/indents.scm           |   1 
crates/zed2/src/languages/css/overrides.scm         |   2 
crates/zed2/src/languages/elixir.rs                 | 546 ++++++++++++++
crates/zed2/src/languages/elixir/brackets.scm       |   5 
crates/zed2/src/languages/elixir/config.toml        |  16 
crates/zed2/src/languages/elixir/embedding.scm      |  27 
crates/zed2/src/languages/elixir/highlights.scm     | 153 ++++
crates/zed2/src/languages/elixir/indents.scm        |   6 
crates/zed2/src/languages/elixir/injections.scm     |   7 
crates/zed2/src/languages/elixir/outline.scm        |  26 
crates/zed2/src/languages/elixir/overrides.scm      |   2 
crates/zed2/src/languages/elm/config.toml           |  11 
crates/zed2/src/languages/elm/highlights.scm        |  72 +
crates/zed2/src/languages/elm/injections.scm        |   2 
crates/zed2/src/languages/elm/outline.scm           |  22 
crates/zed2/src/languages/erb/config.toml           |   8 
crates/zed2/src/languages/erb/highlights.scm        |  12 
crates/zed2/src/languages/erb/injections.scm        |   7 
crates/zed2/src/languages/glsl/config.toml          |   9 
crates/zed2/src/languages/glsl/highlights.scm       | 118 +++
crates/zed2/src/languages/go.rs                     | 464 ++++++++++++
crates/zed2/src/languages/go/brackets.scm           |   3 
crates/zed2/src/languages/go/config.toml            |  12 
crates/zed2/src/languages/go/embedding.scm          |  24 
crates/zed2/src/languages/go/highlights.scm         | 107 ++
crates/zed2/src/languages/go/indents.scm            |   9 
crates/zed2/src/languages/go/outline.scm            |  43 +
crates/zed2/src/languages/go/overrides.scm          |   6 
crates/zed2/src/languages/heex/config.toml          |  12 
crates/zed2/src/languages/heex/highlights.scm       |  57 +
crates/zed2/src/languages/heex/injections.scm       |  13 
crates/zed2/src/languages/heex/overrides.scm        |   4 
crates/zed2/src/languages/html.rs                   | 130 +++
crates/zed2/src/languages/html/brackets.scm         |   2 
crates/zed2/src/languages/html/config.toml          |  14 
crates/zed2/src/languages/html/highlights.scm       |  15 
crates/zed2/src/languages/html/indents.scm          |   6 
crates/zed2/src/languages/html/injections.scm       |   7 
crates/zed2/src/languages/html/outline.scm          |   0 
crates/zed2/src/languages/html/overrides.scm        |   2 
crates/zed2/src/languages/javascript/brackets.scm   |   5 
crates/zed2/src/languages/javascript/config.toml    |  26 
crates/zed2/src/languages/javascript/contexts.scm   |   0 
crates/zed2/src/languages/javascript/embedding.scm  |  71 +
crates/zed2/src/languages/javascript/highlights.scm | 217 +++++
crates/zed2/src/languages/javascript/indents.scm    |  15 
crates/zed2/src/languages/javascript/outline.scm    |  62 +
crates/zed2/src/languages/javascript/overrides.scm  |  13 
crates/zed2/src/languages/json.rs                   | 184 ++++
crates/zed2/src/languages/json/brackets.scm         |   3 
crates/zed2/src/languages/json/config.toml          |  10 
crates/zed2/src/languages/json/embedding.scm        |  14 
crates/zed2/src/languages/json/highlights.scm       |  21 
crates/zed2/src/languages/json/indents.scm          |   2 
crates/zed2/src/languages/json/outline.scm          |   2 
crates/zed2/src/languages/json/overrides.scm        |   1 
crates/zed2/src/languages/language_plugin.rs        | 168 ++++
crates/zed2/src/languages/lua.rs                    | 135 +++
crates/zed2/src/languages/lua/brackets.scm          |   3 
crates/zed2/src/languages/lua/config.toml           |  10 
crates/zed2/src/languages/lua/embedding.scm         |  10 
crates/zed2/src/languages/lua/highlights.scm        | 198 +++++
crates/zed2/src/languages/lua/indents.scm           |  10 
crates/zed2/src/languages/lua/outline.scm           |   3 
crates/zed2/src/languages/markdown/config.toml      |  11 
crates/zed2/src/languages/markdown/highlights.scm   |  24 
crates/zed2/src/languages/markdown/injections.scm   |   4 
crates/zed2/src/languages/nix/config.toml           |  11 
crates/zed2/src/languages/nix/highlights.scm        |  89 ++
crates/zed2/src/languages/nu/brackets.scm           |   4 
crates/zed2/src/languages/nu/config.toml            |   9 
crates/zed2/src/languages/nu/highlights.scm         | 302 +++++++
crates/zed2/src/languages/nu/indents.scm            |   3 
crates/zed2/src/languages/php.rs                    | 137 +++
crates/zed2/src/languages/php/config.toml           |  14 
crates/zed2/src/languages/php/embedding.scm         |  36 
crates/zed2/src/languages/php/highlights.scm        | 123 +++
crates/zed2/src/languages/php/injections.scm        |   3 
crates/zed2/src/languages/php/outline.scm           |  29 
crates/zed2/src/languages/php/tags.scm              |  40 +
crates/zed2/src/languages/python.rs                 | 296 +++++++
crates/zed2/src/languages/python/brackets.scm       |   3 
crates/zed2/src/languages/python/config.toml        |  16 
crates/zed2/src/languages/python/embedding.scm      |   9 
crates/zed2/src/languages/python/highlights.scm     | 125 +++
crates/zed2/src/languages/python/indents.scm        |   3 
crates/zed2/src/languages/python/outline.scm        |   9 
crates/zed2/src/languages/python/overrides.scm      |   2 
crates/zed2/src/languages/racket/brackets.scm       |   3 
crates/zed2/src/languages/racket/config.toml        |   9 
crates/zed2/src/languages/racket/highlights.scm     |  34 
crates/zed2/src/languages/racket/indents.scm        |   3 
crates/zed2/src/languages/racket/outline.scm        |  10 
crates/zed2/src/languages/ruby.rs                   | 160 ++++
crates/zed2/src/languages/ruby/brackets.scm         |  14 
crates/zed2/src/languages/ruby/config.toml          |  13 
crates/zed2/src/languages/ruby/embedding.scm        |  22 
crates/zed2/src/languages/ruby/highlights.scm       | 181 ++++
crates/zed2/src/languages/ruby/indents.scm          |  17 
crates/zed2/src/languages/ruby/outline.scm          |  17 
crates/zed2/src/languages/ruby/overrides.scm        |   2 
crates/zed2/src/languages/rust.rs                   | 568 +++++++++++++++
crates/zed2/src/languages/rust/brackets.scm         |   6 
crates/zed2/src/languages/rust/config.toml          |  13 
crates/zed2/src/languages/rust/embedding.scm        |  32 
crates/zed2/src/languages/rust/highlights.scm       | 116 +++
crates/zed2/src/languages/rust/indents.scm          |  14 
crates/zed2/src/languages/rust/injections.scm       |   7 
crates/zed2/src/languages/rust/outline.scm          |  63 +
crates/zed2/src/languages/rust/overrides.scm        |   8 
crates/zed2/src/languages/scheme/brackets.scm       |   3 
crates/zed2/src/languages/scheme/config.toml        |   9 
crates/zed2/src/languages/scheme/highlights.scm     |  28 
crates/zed2/src/languages/scheme/indents.scm        |   3 
crates/zed2/src/languages/scheme/outline.scm        |  10 
crates/zed2/src/languages/scheme/overrides.scm      |   6 
crates/zed2/src/languages/svelte.rs                 | 133 +++
crates/zed2/src/languages/svelte/config.toml        |  20 
crates/zed2/src/languages/svelte/folds.scm          |   9 
crates/zed2/src/languages/svelte/highlights.scm     |  42 +
crates/zed2/src/languages/svelte/indents.scm        |   8 
crates/zed2/src/languages/svelte/injections.scm     |  28 
crates/zed2/src/languages/svelte/overrides.scm      |   7 
crates/zed2/src/languages/tailwind.rs               | 167 ++++
crates/zed2/src/languages/toml/brackets.scm         |   3 
crates/zed2/src/languages/toml/config.toml          |  10 
crates/zed2/src/languages/toml/highlights.scm       |  37 
crates/zed2/src/languages/toml/indents.scm          |   0 
crates/zed2/src/languages/toml/outline.scm          |  15 
crates/zed2/src/languages/toml/overrides.scm        |   2 
crates/zed2/src/languages/tsx/brackets.scm          |   1 
crates/zed2/src/languages/tsx/config.toml           |  25 
crates/zed2/src/languages/tsx/embedding.scm         |  85 ++
crates/zed2/src/languages/tsx/highlights-jsx.scm    |   0 
crates/zed2/src/languages/tsx/highlights.scm        |   1 
crates/zed2/src/languages/tsx/indents.scm           |   1 
crates/zed2/src/languages/tsx/outline.scm           |   1 
crates/zed2/src/languages/tsx/overrides.scm         |  13 
crates/zed2/src/languages/typescript.rs             | 384 ++++++++++
crates/zed2/src/languages/typescript/brackets.scm   |   5 
crates/zed2/src/languages/typescript/config.toml    |  16 
crates/zed2/src/languages/typescript/embedding.scm  |  85 ++
crates/zed2/src/languages/typescript/highlights.scm | 221 +++++
crates/zed2/src/languages/typescript/indents.scm    |  15 
crates/zed2/src/languages/typescript/outline.scm    |  65 +
crates/zed2/src/languages/typescript/overrides.scm  |   2 
crates/zed2/src/languages/vue.rs                    | 220 +++++
crates/zed2/src/languages/vue/brackets.scm          |   2 
crates/zed2/src/languages/vue/config.toml           |  14 
crates/zed2/src/languages/vue/highlights.scm        |  15 
crates/zed2/src/languages/vue/injections.scm        |   7 
crates/zed2/src/languages/yaml.rs                   | 142 +++
crates/zed2/src/languages/yaml/brackets.scm         |   3 
crates/zed2/src/languages/yaml/config.toml          |  12 
crates/zed2/src/languages/yaml/highlights.scm       |  49 +
crates/zed2/src/languages/yaml/outline.scm          |   1 
crates/zed2/src/main.rs                             |   8 
crates/zed2/src/zed2.rs                             |   1 
191 files changed, 9,433 insertions(+), 179 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -10946,7 +10946,7 @@ dependencies = [
  "ctor",
  "db2",
  "env_logger 0.9.3",
- "feature_flags",
+ "feature_flags2",
  "fs2",
  "fsevent",
  "futures 0.3.28",
@@ -10963,7 +10963,7 @@ dependencies = [
  "lazy_static",
  "libc",
  "log",
- "lsp",
+ "lsp2",
  "node_runtime",
  "num_cpus",
  "parking_lot 0.11.2",
@@ -11016,6 +11016,7 @@ dependencies = [
  "tree-sitter-svelte",
  "tree-sitter-toml",
  "tree-sitter-typescript",
+ "tree-sitter-vue",
  "tree-sitter-yaml",
  "unindent",
  "url",

crates/gpui2/src/app.rs 🔗

@@ -637,6 +637,10 @@ impl AppContext {
         )
     }
 
+    pub fn all_action_names<'a>(&'a self) -> impl Iterator<Item = SharedString> + 'a {
+        self.action_builders.keys().cloned()
+    }
+
     /// Move the global of the given type to the stack.
     pub(crate) fn lease_global<G: 'static>(&mut self) -> GlobalLease<G> {
         GlobalLease::new(

crates/language2/src/language2.rs 🔗

@@ -230,8 +230,8 @@ impl CachedLspAdapter {
         self.adapter.label_for_symbol(name, kind, language).await
     }
 
-    pub fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        self.adapter.enabled_formatters()
+    pub fn prettier_plugins(&self) -> &[&'static str] {
+        self.adapter.prettier_plugins()
     }
 }
 
@@ -340,31 +340,8 @@ pub trait LspAdapter: 'static + Send + Sync {
         Default::default()
     }
 
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        Vec::new()
-    }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum BundledFormatter {
-    Prettier {
-        // See https://prettier.io/docs/en/options.html#parser for a list of valid values.
-        // Usually, every language has a single parser (standard or plugin-provided), hence `Some("parser_name")` can be used.
-        // There can not be multiple parsers for a single language, in case of a conflict, we would attempt to select the one with most plugins.
-        //
-        // But exceptions like Tailwind CSS exist, which uses standard parsers for CSS/JS/HTML/etc. but require an extra plugin to be installed.
-        // For those cases, `None` will install the plugin but apply other, regular parser defined for the language, and this would not be a conflict.
-        parser_name: Option<&'static str>,
-        plugin_names: Vec<&'static str>,
-    },
-}
-
-impl BundledFormatter {
-    pub fn prettier(parser_name: &'static str) -> Self {
-        Self::Prettier {
-            parser_name: Some(parser_name),
-            plugin_names: Vec::new(),
-        }
+    fn prettier_plugins(&self) -> &[&'static str] {
+        &[]
     }
 }
 
@@ -402,6 +379,8 @@ pub struct LanguageConfig {
     pub overrides: HashMap<String, LanguageConfigOverride>,
     #[serde(default)]
     pub word_characters: HashSet<char>,
+    #[serde(default)]
+    pub prettier_parser_name: Option<String>,
 }
 
 #[derive(Debug, Default)]
@@ -475,6 +454,7 @@ impl Default for LanguageConfig {
             overrides: Default::default(),
             collapsed_placeholder: Default::default(),
             word_characters: Default::default(),
+            prettier_parser_name: None,
         }
     }
 }
@@ -500,7 +480,7 @@ pub struct FakeLspAdapter {
     pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp2::FakeLanguageServer)>>,
     pub disk_based_diagnostics_progress_token: Option<String>,
     pub disk_based_diagnostics_sources: Vec<String>,
-    pub enabled_formatters: Vec<BundledFormatter>,
+    pub prettier_plugins: Vec<&'static str>,
 }
 
 #[derive(Clone, Debug, Default)]
@@ -1604,6 +1584,10 @@ impl Language {
             override_id: None,
         }
     }
+
+    pub fn prettier_parser_name(&self) -> Option<&str> {
+        self.config.prettier_parser_name.as_deref()
+    }
 }
 
 impl LanguageScope {
@@ -1766,7 +1750,7 @@ impl Default for FakeLspAdapter {
             disk_based_diagnostics_progress_token: None,
             initialization_options: None,
             disk_based_diagnostics_sources: Vec::new(),
-            enabled_formatters: Vec::new(),
+            prettier_plugins: Vec::new(),
         }
     }
 }
@@ -1824,8 +1808,8 @@ impl LspAdapter for Arc<FakeLspAdapter> {
         self.initialization_options.clone()
     }
 
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        self.enabled_formatters.clone()
+    fn prettier_plugins(&self) -> &[&'static str] {
+        &self.prettier_plugins
     }
 }
 

crates/prettier2/src/prettier2.rs 🔗

@@ -1,8 +1,8 @@
 use anyhow::Context;
-use collections::{HashMap, HashSet};
+use collections::HashMap;
 use fs2::Fs;
 use gpui2::{AsyncAppContext, Model};
-use language2::{language_settings::language_settings, Buffer, BundledFormatter, Diff};
+use language2::{language_settings::language_settings, Buffer, Diff};
 use lsp2::{LanguageServer, LanguageServerId};
 use node_runtime::NodeRuntime;
 use serde::{Deserialize, Serialize};
@@ -189,128 +189,134 @@ impl Prettier {
     ) -> anyhow::Result<Diff> {
         match self {
             Self::Real(local) => {
-                let params = buffer.update(cx, |buffer, cx| {
-                    let buffer_language = buffer.language();
-                    let parsers_with_plugins = buffer_language
-                        .into_iter()
-                        .flat_map(|language| {
-                            language
+                let params = buffer
+                    .update(cx, |buffer, cx| {
+                        let buffer_language = buffer.language();
+                        let parser_with_plugins = buffer_language.and_then(|l| {
+                            let prettier_parser = l.prettier_parser_name()?;
+                            let mut prettier_plugins = l
                                 .lsp_adapters()
                                 .iter()
-                                .flat_map(|adapter| adapter.enabled_formatters())
-                                .filter_map(|formatter| match formatter {
-                                    BundledFormatter::Prettier {
-                                        parser_name,
-                                        plugin_names,
-                                    } => Some((parser_name, plugin_names)),
-                                })
-                        })
-                        .fold(
-                            HashMap::default(),
-                            |mut parsers_with_plugins, (parser_name, plugins)| {
-                                match parser_name {
-                                    Some(parser_name) => parsers_with_plugins
-                                        .entry(parser_name)
-                                        .or_insert_with(HashSet::default)
-                                        .extend(plugins),
-                                    None => parsers_with_plugins.values_mut().for_each(|existing_plugins| {
-                                        existing_plugins.extend(plugins.iter());
-                                    }),
-                                }
-                                parsers_with_plugins
-                            },
+                                .flat_map(|adapter| adapter.prettier_plugins())
+                                .collect::<Vec<_>>();
+                            prettier_plugins.dedup();
+                            Some((prettier_parser, prettier_plugins))
+                        });
+
+                        let prettier_node_modules = self.prettier_dir().join("node_modules");
+                        anyhow::ensure!(
+                            prettier_node_modules.is_dir(),
+                            "Prettier node_modules dir does not exist: {prettier_node_modules:?}"
                         );
-
-                    let selected_parser_with_plugins = parsers_with_plugins.iter().max_by_key(|(_, plugins)| plugins.len());
-                    if parsers_with_plugins.len() > 1 {
-                        log::warn!("Found multiple parsers with plugins {parsers_with_plugins:?}, will select only one: {selected_parser_with_plugins:?}");
-                    }
-
-                    let prettier_node_modules = self.prettier_dir().join("node_modules");
-                    anyhow::ensure!(prettier_node_modules.is_dir(), "Prettier node_modules dir does not exist: {prettier_node_modules:?}");
-                    let plugin_name_into_path = |plugin_name: &str| {
-                        let prettier_plugin_dir = prettier_node_modules.join(plugin_name);
-                        for possible_plugin_path in [
-                            prettier_plugin_dir.join("dist").join("index.mjs"),
-                            prettier_plugin_dir.join("dist").join("index.js"),
-                            prettier_plugin_dir.join("dist").join("plugin.js"),
-                            prettier_plugin_dir.join("index.mjs"),
-                            prettier_plugin_dir.join("index.js"),
-                            prettier_plugin_dir.join("plugin.js"),
-                            prettier_plugin_dir,
-                        ] {
-                            if possible_plugin_path.is_file() {
-                                return Some(possible_plugin_path);
+                        let plugin_name_into_path = |plugin_name: &str| {
+                            let prettier_plugin_dir = prettier_node_modules.join(plugin_name);
+                            for possible_plugin_path in [
+                                prettier_plugin_dir.join("dist").join("index.mjs"),
+                                prettier_plugin_dir.join("dist").join("index.js"),
+                                prettier_plugin_dir.join("dist").join("plugin.js"),
+                                prettier_plugin_dir.join("index.mjs"),
+                                prettier_plugin_dir.join("index.js"),
+                                prettier_plugin_dir.join("plugin.js"),
+                                prettier_plugin_dir,
+                            ] {
+                                if possible_plugin_path.is_file() {
+                                    return Some(possible_plugin_path);
+                                }
                             }
-                        }
-                        None
-                    };
-                    let (parser, located_plugins) = match selected_parser_with_plugins {
-                        Some((parser, plugins)) => {
-                            // Tailwind plugin requires being added last
-                            // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
-                            let mut add_tailwind_back = false;
-
-                            let mut plugins = plugins.into_iter().filter(|&&plugin_name| {
-                                if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
-                                    add_tailwind_back = true;
-                                    false
-                                } else {
-                                    true
+                            None
+                        };
+                        let (parser, located_plugins) = match parser_with_plugins {
+                            Some((parser, plugins)) => {
+                                // Tailwind plugin requires being added last
+                                // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
+                                let mut add_tailwind_back = false;
+
+                                let mut plugins = plugins
+                                    .into_iter()
+                                    .filter(|&&plugin_name| {
+                                        if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
+                                            add_tailwind_back = true;
+                                            false
+                                        } else {
+                                            true
+                                        }
+                                    })
+                                    .map(|plugin_name| {
+                                        (plugin_name, plugin_name_into_path(plugin_name))
+                                    })
+                                    .collect::<Vec<_>>();
+                                if add_tailwind_back {
+                                    plugins.push((
+                                        &TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME,
+                                        plugin_name_into_path(
+                                            TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME,
+                                        ),
+                                    ));
                                 }
-                            }).map(|plugin_name| (plugin_name, plugin_name_into_path(plugin_name))).collect::<Vec<_>>();
-                            if add_tailwind_back {
-                                plugins.push((&TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME, plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME)));
+                                (Some(parser.to_string()), plugins)
                             }
-                            (Some(parser.to_string()), plugins)
-                        },
-                        None => (None, Vec::new()),
-                    };
-
-                    let prettier_options = if self.is_default() {
-                        let language_settings = language_settings(buffer_language, buffer.file(), cx);
-                        let mut options = language_settings.prettier.clone();
-                        if !options.contains_key("tabWidth") {
-                            options.insert(
-                                "tabWidth".to_string(),
-                                serde_json::Value::Number(serde_json::Number::from(
-                                    language_settings.tab_size.get(),
-                                )),
-                            );
-                        }
-                        if !options.contains_key("printWidth") {
-                            options.insert(
-                                "printWidth".to_string(),
-                                serde_json::Value::Number(serde_json::Number::from(
-                                    language_settings.preferred_line_length,
-                                )),
-                            );
-                        }
-                        Some(options)
-                    } else {
-                        None
-                    };
-
-                    let plugins = located_plugins.into_iter().filter_map(|(plugin_name, located_plugin_path)| {
-                        match located_plugin_path {
-                            Some(path) => Some(path),
-                            None => {
-                                log::error!("Have not found plugin path for {plugin_name:?} inside {prettier_node_modules:?}");
-                                None},
-                        }
-                    }).collect();
-                    log::debug!("Formatting file {:?} with prettier, plugins :{plugins:?}, options: {prettier_options:?}", buffer.file().map(|f| f.full_path(cx)));
-
-                    anyhow::Ok(FormatParams {
-                        text: buffer.text(),
-                        options: FormatOptions {
-                            parser,
+                            None => (None, Vec::new()),
+                        };
+
+                        let prettier_options = if self.is_default() {
+                            let language_settings =
+                                language_settings(buffer_language, buffer.file(), cx);
+                            let mut options = language_settings.prettier.clone();
+                            if !options.contains_key("tabWidth") {
+                                options.insert(
+                                    "tabWidth".to_string(),
+                                    serde_json::Value::Number(serde_json::Number::from(
+                                        language_settings.tab_size.get(),
+                                    )),
+                                );
+                            }
+                            if !options.contains_key("printWidth") {
+                                options.insert(
+                                    "printWidth".to_string(),
+                                    serde_json::Value::Number(serde_json::Number::from(
+                                        language_settings.preferred_line_length,
+                                    )),
+                                );
+                            }
+                            Some(options)
+                        } else {
+                            None
+                        };
+
+                        let plugins = located_plugins
+                            .into_iter()
+                            .filter_map(|(plugin_name, located_plugin_path)| {
+                                match located_plugin_path {
+                                    Some(path) => Some(path),
+                                    None => {
+                                        log::error!(
+                                            "Have not found plugin path for {:?} inside {:?}",
+                                            plugin_name,
+                                            prettier_node_modules
+                                        );
+                                        None
+                                    }
+                                }
+                            })
+                            .collect();
+                        log::debug!(
+                            "Formatting file {:?} with prettier, plugins :{:?}, options: {:?}",
                             plugins,
-                            path: buffer_path,
                             prettier_options,
-                        },
-                    })
-                })?.context("prettier params calculation")?;
+                            buffer.file().map(|f| f.full_path(cx))
+                        );
+
+                        anyhow::Ok(FormatParams {
+                            text: buffer.text(),
+                            options: FormatOptions {
+                                parser,
+                                plugins,
+                                path: buffer_path,
+                                prettier_options,
+                            },
+                        })
+                    })?
+                    .context("prettier params calculation")?;
                 let response = local
                     .server
                     .request::<Format>(params)

crates/project2/src/project2.rs 🔗

@@ -39,11 +39,11 @@ use language2::{
         deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
         serialize_anchor, serialize_version, split_operations,
     },
-    range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, BundledFormatter, CachedLspAdapter,
-    CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff,
-    Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
-    LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16,
-    TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
+    range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction,
+    CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent,
+    File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate,
+    OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot,
+    ToOffset, ToPointUtf16, Transaction, Unclipped,
 };
 use log::error;
 use lsp2::{
@@ -8410,12 +8410,7 @@ impl Project {
         let Some(buffer_language) = buffer.language() else {
             return Task::ready(None);
         };
-        if !buffer_language
-            .lsp_adapters()
-            .iter()
-            .flat_map(|adapter| adapter.enabled_formatters())
-            .any(|formatter| matches!(formatter, BundledFormatter::Prettier { .. }))
-        {
+        if buffer_language.prettier_parser_name().is_none() {
             return Task::ready(None);
         }
 
@@ -8574,16 +8569,15 @@ impl Project {
         };
 
         let mut prettier_plugins = None;
-        for formatter in new_language
-            .lsp_adapters()
-            .into_iter()
-            .flat_map(|adapter| adapter.enabled_formatters())
-        {
-            match formatter {
-                BundledFormatter::Prettier { plugin_names, .. } => prettier_plugins
-                    .get_or_insert_with(|| HashSet::default())
-                    .extend(plugin_names),
-            }
+        if new_language.prettier_parser_name().is_some() {
+            prettier_plugins
+                .get_or_insert_with(|| HashSet::default())
+                .extend(
+                    new_language
+                        .lsp_adapters()
+                        .iter()
+                        .flat_map(|adapter| adapter.prettier_plugins()),
+                )
         }
         let Some(prettier_plugins) = prettier_plugins else {
             return Task::ready(Ok(()));

crates/settings2/src/keymap_file.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{settings_store::parse_json_with_comments, SettingsAssets};
 use anyhow::{anyhow, Context, Result};
 use collections::BTreeMap;
-use gpui2::{AppContext, KeyBinding};
+use gpui2::{AppContext, KeyBinding, SharedString};
 use schemars::{
     gen::{SchemaGenerator, SchemaSettings},
     schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
@@ -96,7 +96,7 @@ impl KeymapFile {
         Ok(())
     }
 
-    pub fn generate_json_schema(action_names: &[&'static str]) -> serde_json::Value {
+    pub fn generate_json_schema(action_names: &[SharedString]) -> serde_json::Value {
         let mut root_schema = SchemaSettings::draft07()
             .with(|settings| settings.option_add_null_type = false)
             .into_generator()

crates/theme2/src/theme2.rs 🔗

@@ -108,6 +108,24 @@ pub struct SyntaxTheme {
 }
 
 impl SyntaxTheme {
+    // TOOD: Get this working with `#[cfg(test)]`. Why isn't it?
+    pub fn new_test(colors: impl IntoIterator<Item = (&'static str, Hsla)>) -> Self {
+        SyntaxTheme {
+            highlights: colors
+                .into_iter()
+                .map(|(key, color)| {
+                    (
+                        key.to_owned(),
+                        HighlightStyle {
+                            color: Some(color),
+                            ..Default::default()
+                        },
+                    )
+                })
+                .collect(),
+        }
+    }
+
     pub fn get(&self, name: &str) -> HighlightStyle {
         self.highlights
             .iter()

crates/zed2/Cargo.toml 🔗

@@ -47,7 +47,7 @@ install_cli = { path = "../install_cli" }
 journal2 = { path = "../journal2" }
 language2 = { path = "../language2" }
 # language_selector = { path = "../language_selector" }
-lsp = { path = "../lsp" }
+lsp2 = { path = "../lsp2" }
 language_tools = { path = "../language_tools" }
 node_runtime = { path = "../node_runtime" }
 # assistant = { path = "../assistant" }
@@ -60,7 +60,7 @@ project2 = { path = "../project2" }
 # recent_projects = { path = "../recent_projects" }
 rpc2 = { path = "../rpc2" }
 settings2 = { path = "../settings2" }
-feature_flags = { path = "../feature_flags" }
+feature_flags2 = { path = "../feature_flags2" }
 sum_tree = { path = "../sum_tree" }
 shellexpand = "2.1.0"
 text = { path = "../text" }
@@ -135,6 +135,7 @@ tree-sitter-yaml.workspace = true
 tree-sitter-lua.workspace = true
 tree-sitter-nix.workspace = true
 tree-sitter-nu.workspace = true
+tree-sitter-vue.workspace = true
 
 url = "2.2"
 urlencoding = "2.1.2"

crates/zed2/src/languages.rs 🔗

@@ -0,0 +1,273 @@
+use anyhow::Context;
+use gpui2::AppContext;
+pub use language2::*;
+use node_runtime::NodeRuntime;
+use rust_embed::RustEmbed;
+use settings2::Settings;
+use std::{borrow::Cow, str, sync::Arc};
+use util::asset_str;
+
+use self::elixir::ElixirSettings;
+
+mod c;
+mod css;
+mod elixir;
+mod go;
+mod html;
+mod json;
+#[cfg(feature = "plugin_runtime")]
+mod language_plugin;
+mod lua;
+mod php;
+mod python;
+mod ruby;
+mod rust;
+mod svelte;
+mod tailwind;
+mod typescript;
+mod vue;
+mod yaml;
+
+// 1. Add tree-sitter-{language} parser to zed crate
+// 2. Create a language directory in zed/crates/zed/src/languages and add the language to init function below
+// 3. Add config.toml to the newly created language directory using existing languages as a template
+// 4. Copy highlights from tree sitter repo for the language into a highlights.scm file.
+//      Note: github highlights take the last match while zed takes the first
+// 5. Add indents.scm, outline.scm, and brackets.scm to implement indent on newline, outline/breadcrumbs,
+//    and autoclosing brackets respectively
+// 6. If the language has injections add an injections.scm query file
+
+#[derive(RustEmbed)]
+#[folder = "src/languages"]
+#[exclude = "*.rs"]
+struct LanguageDir;
+
+pub fn init(
+    languages: Arc<LanguageRegistry>,
+    node_runtime: Arc<dyn NodeRuntime>,
+    cx: &mut AppContext,
+) {
+    ElixirSettings::register(cx);
+
+    let language = |name, grammar, adapters| {
+        languages.register(name, load_config(name), grammar, adapters, load_queries)
+    };
+
+    language("bash", tree_sitter_bash::language(), vec![]);
+    language(
+        "c",
+        tree_sitter_c::language(),
+        vec![Arc::new(c::CLspAdapter) as Arc<dyn LspAdapter>],
+    );
+    language(
+        "cpp",
+        tree_sitter_cpp::language(),
+        vec![Arc::new(c::CLspAdapter)],
+    );
+    language(
+        "css",
+        tree_sitter_css::language(),
+        vec![
+            Arc::new(css::CssLspAdapter::new(node_runtime.clone())),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
+    );
+
+    match &ElixirSettings::get(None, cx).lsp {
+        elixir::ElixirLspSetting::ElixirLs => language(
+            "elixir",
+            tree_sitter_elixir::language(),
+            vec![
+                Arc::new(elixir::ElixirLspAdapter),
+                Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+            ],
+        ),
+        elixir::ElixirLspSetting::NextLs => language(
+            "elixir",
+            tree_sitter_elixir::language(),
+            vec![Arc::new(elixir::NextLspAdapter)],
+        ),
+        elixir::ElixirLspSetting::Local { path, arguments } => language(
+            "elixir",
+            tree_sitter_elixir::language(),
+            vec![Arc::new(elixir::LocalLspAdapter {
+                path: path.clone(),
+                arguments: arguments.clone(),
+            })],
+        ),
+    }
+
+    language(
+        "go",
+        tree_sitter_go::language(),
+        vec![Arc::new(go::GoLspAdapter)],
+    );
+    language(
+        "heex",
+        tree_sitter_heex::language(),
+        vec![
+            Arc::new(elixir::ElixirLspAdapter),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
+    );
+    language(
+        "json",
+        tree_sitter_json::language(),
+        vec![Arc::new(json::JsonLspAdapter::new(
+            node_runtime.clone(),
+            languages.clone(),
+        ))],
+    );
+    language("markdown", tree_sitter_markdown::language(), vec![]);
+    language(
+        "python",
+        tree_sitter_python::language(),
+        vec![Arc::new(python::PythonLspAdapter::new(
+            node_runtime.clone(),
+        ))],
+    );
+    language(
+        "rust",
+        tree_sitter_rust::language(),
+        vec![Arc::new(rust::RustLspAdapter)],
+    );
+    language("toml", tree_sitter_toml::language(), vec![]);
+    language(
+        "tsx",
+        tree_sitter_typescript::language_tsx(),
+        vec![
+            Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
+            Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
+    );
+    language(
+        "typescript",
+        tree_sitter_typescript::language_typescript(),
+        vec![
+            Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
+            Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
+        ],
+    );
+    language(
+        "javascript",
+        tree_sitter_typescript::language_tsx(),
+        vec![
+            Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
+            Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
+    );
+    language(
+        "html",
+        tree_sitter_html::language(),
+        vec![
+            Arc::new(html::HtmlLspAdapter::new(node_runtime.clone())),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
+    );
+    language(
+        "ruby",
+        tree_sitter_ruby::language(),
+        vec![Arc::new(ruby::RubyLanguageServer)],
+    );
+    language(
+        "erb",
+        tree_sitter_embedded_template::language(),
+        vec![
+            Arc::new(ruby::RubyLanguageServer),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
+    );
+    language("scheme", tree_sitter_scheme::language(), vec![]);
+    language("racket", tree_sitter_racket::language(), vec![]);
+    language(
+        "lua",
+        tree_sitter_lua::language(),
+        vec![Arc::new(lua::LuaLspAdapter)],
+    );
+    language(
+        "yaml",
+        tree_sitter_yaml::language(),
+        vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))],
+    );
+    language(
+        "svelte",
+        tree_sitter_svelte::language(),
+        vec![
+            Arc::new(svelte::SvelteLspAdapter::new(node_runtime.clone())),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
+    );
+    language(
+        "php",
+        tree_sitter_php::language(),
+        vec![
+            Arc::new(php::IntelephenseLspAdapter::new(node_runtime.clone())),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
+    );
+
+    language("elm", tree_sitter_elm::language(), vec![]);
+    language("glsl", tree_sitter_glsl::language(), vec![]);
+    language("nix", tree_sitter_nix::language(), vec![]);
+    language("nu", tree_sitter_nu::language(), vec![]);
+    language(
+        "vue",
+        tree_sitter_vue::language(),
+        vec![Arc::new(vue::VueLspAdapter::new(node_runtime))],
+    );
+}
+
+#[cfg(any(test, feature = "test-support"))]
+pub async fn language(
+    name: &str,
+    grammar: tree_sitter::Language,
+    lsp_adapter: Option<Arc<dyn LspAdapter>>,
+) -> Arc<Language> {
+    Arc::new(
+        Language::new(load_config(name), Some(grammar))
+            .with_lsp_adapters(lsp_adapter.into_iter().collect())
+            .await
+            .with_queries(load_queries(name))
+            .unwrap(),
+    )
+}
+
+fn load_config(name: &str) -> LanguageConfig {
+    toml::from_slice(
+        &LanguageDir::get(&format!("{}/config.toml", name))
+            .unwrap()
+            .data,
+    )
+    .with_context(|| format!("failed to load config.toml for language {name:?}"))
+    .unwrap()
+}
+
+fn load_queries(name: &str) -> LanguageQueries {
+    LanguageQueries {
+        highlights: load_query(name, "/highlights"),
+        brackets: load_query(name, "/brackets"),
+        indents: load_query(name, "/indents"),
+        outline: load_query(name, "/outline"),
+        embedding: load_query(name, "/embedding"),
+        injections: load_query(name, "/injections"),
+        overrides: load_query(name, "/overrides"),
+    }
+}
+
+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 = asset_str::<LanguageDir>(path.as_ref());
+                match &mut result {
+                    None => result = Some(contents),
+                    Some(r) => r.to_mut().push_str(contents.as_ref()),
+                }
+            }
+        }
+    }
+    result
+}

crates/zed2/src/languages/bash/config.toml 🔗

@@ -0,0 +1,9 @@
+name = "Shell Script"
+path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin", "zprofile"]
+line_comment = "# "
+first_line_pattern = "^#!.*\\b(?:ba|z)?sh\\b"
+brackets = [
+    { start = "[", end = "]", close = true, newline = false },
+    { start = "(", end = ")", close = true, newline = false },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] },
+]

crates/zed2/src/languages/bash/highlights.scm 🔗

@@ -0,0 +1,59 @@
+[
+  (string)
+  (raw_string)
+  (heredoc_body)
+  (heredoc_start)
+  (ansi_c_string)
+] @string
+
+(command_name) @function
+
+(variable_name) @property
+
+[
+  "case"
+  "do"
+  "done"
+  "elif"
+  "else"
+  "esac"
+  "export"
+  "fi"
+  "for"
+  "function"
+  "if"
+  "in"
+  "select"
+  "then"
+  "unset"
+  "until"
+  "while"
+  "local"
+  "declare"
+] @keyword
+
+(comment) @comment
+
+(function_definition name: (word) @function)
+
+(file_descriptor) @number
+
+[
+  (command_substitution)
+  (process_substitution)
+  (expansion)
+]@embedded
+
+[
+  "$"
+  "&&"
+  ">"
+  ">>"
+  "<"
+  "|"
+] @operator
+
+(
+  (command (_) @constant)
+  (#match? @constant "^-")
+)

crates/zed2/src/languages/c.rs 🔗

@@ -0,0 +1,321 @@
+use anyhow::{anyhow, Context, Result};
+use async_trait::async_trait;
+use futures::StreamExt;
+pub use language2::*;
+use lsp2::LanguageServerBinary;
+use smol::fs::{self, File};
+use std::{any::Any, path::PathBuf, sync::Arc};
+use util::{
+    fs::remove_matching,
+    github::{latest_github_release, GitHubLspBinaryVersion},
+    ResultExt,
+};
+
+pub struct CLspAdapter;
+
+#[async_trait]
+impl super::LspAdapter for CLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("clangd".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "clangd"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let release = latest_github_release("clangd/clangd", false, delegate.http_client()).await?;
+        let asset_name = format!("clangd-mac-{}.zip", release.name);
+        let asset = release
+            .assets
+            .iter()
+            .find(|asset| asset.name == asset_name)
+            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+        let version = GitHubLspBinaryVersion {
+            name: release.name,
+            url: asset.browser_download_url.clone(),
+        };
+        Ok(Box::new(version) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+        let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
+        let version_dir = container_dir.join(format!("clangd_{}", version.name));
+        let binary_path = version_dir.join("bin/clangd");
+
+        if fs::metadata(&binary_path).await.is_err() {
+            let mut response = delegate
+                .http_client()
+                .get(&version.url, Default::default(), true)
+                .await
+                .context("error downloading release")?;
+            let mut file = File::create(&zip_path).await?;
+            if !response.status().is_success() {
+                Err(anyhow!(
+                    "download failed with status {}",
+                    response.status().to_string()
+                ))?;
+            }
+            futures::io::copy(response.body_mut(), &mut file).await?;
+
+            let unzip_status = smol::process::Command::new("unzip")
+                .current_dir(&container_dir)
+                .arg(&zip_path)
+                .output()
+                .await?
+                .status;
+            if !unzip_status.success() {
+                Err(anyhow!("failed to unzip clangd archive"))?;
+            }
+
+            remove_matching(&container_dir, |entry| entry != version_dir).await;
+        }
+
+        Ok(LanguageServerBinary {
+            path: binary_path,
+            arguments: vec![],
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--help".into()];
+                binary
+            })
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp2::CompletionItem,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        let label = completion
+            .label
+            .strip_prefix('•')
+            .unwrap_or(&completion.label)
+            .trim();
+
+        match completion.kind {
+            Some(lsp2::CompletionItemKind::FIELD) if completion.detail.is_some() => {
+                let detail = completion.detail.as_ref().unwrap();
+                let text = format!("{} {}", detail, label);
+                let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
+                let runs = language.highlight_text(&source, 11..11 + text.len());
+                return Some(CodeLabel {
+                    filter_range: detail.len() + 1..text.len(),
+                    text,
+                    runs,
+                });
+            }
+            Some(lsp2::CompletionItemKind::CONSTANT | lsp2::CompletionItemKind::VARIABLE)
+                if completion.detail.is_some() =>
+            {
+                let detail = completion.detail.as_ref().unwrap();
+                let text = format!("{} {}", detail, label);
+                let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
+                return Some(CodeLabel {
+                    filter_range: detail.len() + 1..text.len(),
+                    text,
+                    runs,
+                });
+            }
+            Some(lsp2::CompletionItemKind::FUNCTION | lsp2::CompletionItemKind::METHOD)
+                if completion.detail.is_some() =>
+            {
+                let detail = completion.detail.as_ref().unwrap();
+                let text = format!("{} {}", detail, label);
+                let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
+                return Some(CodeLabel {
+                    filter_range: detail.len() + 1..text.rfind('(').unwrap_or(text.len()),
+                    text,
+                    runs,
+                });
+            }
+            Some(kind) => {
+                let highlight_name = match kind {
+                    lsp2::CompletionItemKind::STRUCT
+                    | lsp2::CompletionItemKind::INTERFACE
+                    | lsp2::CompletionItemKind::CLASS
+                    | lsp2::CompletionItemKind::ENUM => Some("type"),
+                    lsp2::CompletionItemKind::ENUM_MEMBER => Some("variant"),
+                    lsp2::CompletionItemKind::KEYWORD => Some("keyword"),
+                    lsp2::CompletionItemKind::VALUE | lsp2::CompletionItemKind::CONSTANT => {
+                        Some("constant")
+                    }
+                    _ => None,
+                };
+                if let Some(highlight_id) = language
+                    .grammar()
+                    .and_then(|g| g.highlight_id_for_name(highlight_name?))
+                {
+                    let mut label = CodeLabel::plain(label.to_string(), None);
+                    label.runs.push((
+                        0..label.text.rfind('(').unwrap_or(label.text.len()),
+                        highlight_id,
+                    ));
+                    return Some(label);
+                }
+            }
+            _ => {}
+        }
+        Some(CodeLabel::plain(label.to_string(), None))
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        kind: lsp2::SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        let (text, filter_range, display_range) = match kind {
+            lsp2::SymbolKind::METHOD | lsp2::SymbolKind::FUNCTION => {
+                let text = format!("void {} () {{}}", name);
+                let filter_range = 0..name.len();
+                let display_range = 5..5 + name.len();
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::STRUCT => {
+                let text = format!("struct {} {{}}", name);
+                let filter_range = 7..7 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::ENUM => {
+                let text = format!("enum {} {{}}", name);
+                let filter_range = 5..5 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::INTERFACE | lsp2::SymbolKind::CLASS => {
+                let text = format!("class {} {{}}", name);
+                let filter_range = 6..6 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::CONSTANT => {
+                let text = format!("const int {} = 0;", name);
+                let filter_range = 10..10 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::MODULE => {
+                let text = format!("namespace {} {{}}", name);
+                let filter_range = 10..10 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::TYPE_PARAMETER => {
+                let text = format!("typename {} {{}};", name);
+                let filter_range = 9..9 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            _ => return None,
+        };
+
+        Some(CodeLabel {
+            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
+            text: text[display_range].to_string(),
+            filter_range,
+        })
+    }
+}
+
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last_clangd_dir = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_dir() {
+                last_clangd_dir = Some(entry.path());
+            }
+        }
+        let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+        let clangd_bin = clangd_dir.join("bin/clangd");
+        if clangd_bin.exists() {
+            Ok(LanguageServerBinary {
+                path: clangd_bin,
+                arguments: vec![],
+            })
+        } else {
+            Err(anyhow!(
+                "missing clangd binary in directory {:?}",
+                clangd_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}
+
+#[cfg(test)]
+mod tests {
+    use gpui2::{Context, TestAppContext};
+    use language2::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
+    use settings2::SettingsStore;
+    use std::num::NonZeroU32;
+
+    #[gpui2::test]
+    async fn test_c_autoindent(cx: &mut TestAppContext) {
+        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
+        cx.update(|cx| {
+            let test_settings = SettingsStore::test(cx);
+            cx.set_global(test_settings);
+            language2::init(cx);
+            cx.update_global::<SettingsStore, _>(|store, cx| {
+                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
+                    s.defaults.tab_size = NonZeroU32::new(2);
+                });
+            });
+        });
+        let language = crate::languages::language("c", tree_sitter_c::language(), None).await;
+
+        cx.build_model(|cx| {
+            let mut buffer =
+                Buffer::new(0, cx.entity_id().as_u64(), "").with_language(language, cx);
+
+            // empty function
+            buffer.edit([(0..0, "int main() {}")], None, cx);
+
+            // indent inside braces
+            let ix = buffer.len() - 1;
+            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(buffer.text(), "int main() {\n  \n}");
+
+            // indent body of single-statement if statement
+            let ix = buffer.len() - 2;
+            buffer.edit([(ix..ix, "if (a)\nb;")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(buffer.text(), "int main() {\n  if (a)\n    b;\n}");
+
+            // indent inside field expression
+            let ix = buffer.len() - 3;
+            buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(buffer.text(), "int main() {\n  if (a)\n    b\n      .c;\n}");
+
+            buffer
+        });
+    }
+}

crates/zed2/src/languages/c/config.toml 🔗

@@ -0,0 +1,12 @@
+name = "C"
+path_suffixes = ["c"]
+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 = true, newline = false, not_in = ["string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
+    { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
+]

crates/zed2/src/languages/c/embedding.scm 🔗

@@ -0,0 +1,43 @@
+(
+    (comment)* @context
+    .
+    (declaration
+        declarator: [
+            (function_declarator
+                declarator: (_) @name)
+            (pointer_declarator
+                "*" @name
+                declarator: (function_declarator
+                    declarator: (_) @name))
+            (pointer_declarator
+                "*" @name
+                declarator: (pointer_declarator
+                    "*" @name
+                    declarator: (function_declarator
+                        declarator: (_) @name)))
+            ]
+        ) @item
+    )
+
+(
+    (comment)* @context
+    .
+    (function_definition
+        declarator: [
+            (function_declarator
+                declarator: (_) @name
+                )
+            (pointer_declarator
+                "*" @name
+                declarator: (function_declarator
+                    declarator: (_) @name
+                    ))
+            (pointer_declarator
+                "*" @name
+                declarator: (pointer_declarator
+                    "*" @name
+                    declarator: (function_declarator
+                        declarator: (_) @name)))
+            ]
+        ) @item
+    )

crates/zed2/src/languages/c/highlights.scm 🔗

@@ -0,0 +1,109 @@
+[
+  "break"
+  "case"
+  "const"
+  "continue"
+  "default"
+  "do"
+  "else"
+  "enum"
+  "extern"
+  "for"
+  "if"
+  "inline"
+  "return"
+  "sizeof"
+  "static"
+  "struct"
+  "switch"
+  "typedef"
+  "union"
+  "volatile"
+  "while"
+] @keyword
+
+[
+  "#define"
+  "#elif"
+  "#else"
+  "#endif"
+  "#if"
+  "#ifdef"
+  "#ifndef"
+  "#include"
+  (preproc_directive)
+] @keyword
+
+[
+  "--"
+  "-"
+  "-="
+  "->"
+  "="
+  "!="
+  "*"
+  "&"
+  "&&"
+  "+"
+  "++"
+  "+="
+  "<"
+  "=="
+  ">"
+  "||"
+] @operator
+
+[
+  "."
+  ";"
+] @punctuation.delimiter
+
+[
+  "{"
+  "}"
+  "("
+  ")"
+  "["
+  "]"
+] @punctuation.bracket
+
+[
+  (string_literal)
+  (system_lib_string)
+  (char_literal)
+] @string
+
+(comment) @comment
+
+(number_literal) @number
+
+[
+  (true)
+  (false)
+  (null)
+] @constant
+
+(identifier) @variable
+
+((identifier) @constant
+ (#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
+
+(call_expression
+  function: (identifier) @function)
+(call_expression
+  function: (field_expression
+    field: (field_identifier) @function))
+(function_declarator
+  declarator: (identifier) @function)
+(preproc_function_def
+  name: (identifier) @function.special)
+
+(field_identifier) @property
+(statement_identifier) @label
+
+[
+  (type_identifier)
+  (primitive_type)
+  (sized_type_specifier)
+] @type
+

crates/zed2/src/languages/c/outline.scm 🔗

@@ -0,0 +1,70 @@
+(preproc_def
+    "#define" @context
+    name: (_) @name) @item
+
+(preproc_function_def
+    "#define" @context
+    name: (_) @name
+    parameters: (preproc_params
+        "(" @context
+        ")" @context)) @item
+
+(type_definition
+    "typedef" @context
+    declarator: (_) @name) @item
+
+(declaration
+    (type_qualifier)? @context
+    type: (_)? @context
+    declarator: [
+        (function_declarator
+            declarator: (_) @name
+            parameters: (parameter_list
+                "(" @context
+                ")" @context))
+        (pointer_declarator
+            "*" @context
+            declarator: (function_declarator
+                declarator: (_) @name
+                parameters: (parameter_list
+                    "(" @context
+                    ")" @context)))
+        (pointer_declarator
+            "*" @context
+            declarator: (pointer_declarator
+                "*" @context
+                declarator: (function_declarator
+                    declarator: (_) @name
+                    parameters: (parameter_list
+                        "(" @context
+                        ")" @context))))
+    ]
+) @item
+
+(function_definition
+    (type_qualifier)? @context
+    type: (_)? @context
+    declarator: [
+        (function_declarator
+            declarator: (_) @name
+            parameters: (parameter_list
+                "(" @context
+                ")" @context))
+        (pointer_declarator
+            "*" @context
+            declarator: (function_declarator
+                declarator: (_) @name
+                parameters: (parameter_list
+                    "(" @context
+                    ")" @context)))
+        (pointer_declarator
+            "*" @context
+            declarator: (pointer_declarator
+                "*" @context
+                declarator: (function_declarator
+                    declarator: (_) @name
+                    parameters: (parameter_list
+                        "(" @context
+                        ")" @context))))
+    ]
+) @item

crates/zed2/src/languages/cpp/config.toml 🔗

@@ -0,0 +1,12 @@
+name = "C++"
+path_suffixes = ["cc", "cpp", "h", "hpp", "cxx", "hxx", "inl"]
+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 = true, newline = false, not_in = ["string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
+    { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
+]

crates/zed2/src/languages/cpp/embedding.scm 🔗

@@ -0,0 +1,61 @@
+(
+    (comment)* @context
+    .
+    (function_definition
+        (type_qualifier)? @name
+        type: (_)? @name
+        declarator: [
+            (function_declarator
+                declarator: (_) @name)
+            (pointer_declarator
+                "*" @name
+                declarator: (function_declarator
+                declarator: (_) @name))
+            (pointer_declarator
+                "*" @name
+                declarator: (pointer_declarator
+                    "*" @name
+                declarator: (function_declarator
+                    declarator: (_) @name)))
+            (reference_declarator
+                ["&" "&&"] @name
+                (function_declarator
+                declarator: (_) @name))
+        ]
+        (type_qualifier)? @name) @item
+    )
+
+(
+    (comment)* @context
+    .
+    (template_declaration
+        (class_specifier
+            "class" @name
+            name: (_) @name)
+            ) @item
+)
+
+(
+    (comment)* @context
+    .
+    (class_specifier
+        "class" @name
+        name: (_) @name) @item
+    )
+
+(
+    (comment)* @context
+    .
+    (enum_specifier
+        "enum" @name
+        name: (_) @name) @item
+    )
+
+(
+    (comment)* @context
+    .
+    (declaration
+        type: (struct_specifier
+        "struct" @name)
+        declarator: (_) @name) @item
+)

crates/zed2/src/languages/cpp/highlights.scm 🔗

@@ -0,0 +1,158 @@
+(identifier) @variable
+
+(call_expression
+  function: (qualified_identifier
+    name: (identifier) @function))
+
+(call_expression
+  function: (identifier) @function)
+
+(call_expression
+  function: (field_expression
+    field: (field_identifier) @function))
+
+(preproc_function_def
+  name: (identifier) @function.special)
+
+(template_function
+  name: (identifier) @function)
+
+(template_method
+  name: (field_identifier) @function)
+
+(function_declarator
+  declarator: (identifier) @function)
+
+(function_declarator
+  declarator: (qualified_identifier
+    name: (identifier) @function))
+
+(function_declarator
+  declarator: (field_identifier) @function)
+
+((namespace_identifier) @type
+ (#match? @type "^[A-Z]"))
+
+(auto) @type
+(type_identifier) @type
+
+((identifier) @constant
+ (#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
+
+(field_identifier) @property
+(statement_identifier) @label
+(this) @variable.special
+
+[
+  "break"
+  "case"
+  "catch"
+  "class"
+  "co_await"
+  "co_return"
+  "co_yield"
+  "const"
+  "constexpr"
+  "continue"
+  "default"
+  "delete"
+  "do"
+  "else"
+  "enum"
+  "explicit"
+  "extern"
+  "final"
+  "for"
+  "friend"
+  "if"
+  "if"
+  "inline"
+  "mutable"
+  "namespace"
+  "new"
+  "noexcept"
+  "override"
+  "private"
+  "protected"
+  "public"
+  "return"
+  "sizeof"
+  "static"
+  "struct"
+  "switch"
+  "template"
+  "throw"
+  "try"
+  "typedef"
+  "typename"
+  "union"
+  "using"
+  "virtual"
+  "volatile"
+  "while"
+  (primitive_type)
+  (type_qualifier)
+] @keyword
+
+[
+  "#define"
+  "#elif"
+  "#else"
+  "#endif"
+  "#if"
+  "#ifdef"
+  "#ifndef"
+  "#include"
+  (preproc_directive)
+] @keyword
+
+(comment) @comment
+
+[
+  (true)
+  (false)
+  (null)
+  (nullptr)
+] @constant
+
+(number_literal) @number
+
+[
+  (string_literal)
+  (system_lib_string)
+  (char_literal)
+  (raw_string_literal)
+] @string
+
+[
+  "."
+  ";"
+] @punctuation.delimiter
+
+[
+  "{"
+  "}"
+  "("
+  ")"
+  "["
+  "]"
+] @punctuation.bracket
+
+[
+  "--"
+  "-"
+  "-="
+  "->"
+  "="
+  "!="
+  "*"
+  "&"
+  "&&"
+  "+"
+  "++"
+  "+="
+  "<"
+  "=="
+  ">"
+  "||"
+] @operator

crates/zed2/src/languages/cpp/outline.scm 🔗

@@ -0,0 +1,149 @@
+(preproc_def
+    "#define" @context
+    name: (_) @name) @item
+
+(preproc_function_def
+    "#define" @context
+    name: (_) @name
+    parameters: (preproc_params
+        "(" @context
+        ")" @context)) @item
+
+(type_definition
+    "typedef" @context
+    declarator: (_) @name) @item
+
+(struct_specifier
+    "struct" @context
+    name: (_) @name) @item
+
+(class_specifier
+    "class" @context
+    name: (_) @name) @item
+
+(enum_specifier
+    "enum" @context
+    name: (_) @name) @item
+
+(enumerator
+    name: (_) @name) @item
+
+(declaration
+    (storage_class_specifier) @context
+    (type_qualifier)? @context
+    type: (_) @context
+    declarator: (init_declarator
+      declarator: (_) @name)) @item
+
+(function_definition
+    (type_qualifier)? @context
+    type: (_)? @context
+    declarator: [
+        (function_declarator
+            declarator: (_) @name
+            parameters: (parameter_list
+                "(" @context
+                ")" @context))
+        (pointer_declarator
+            "*" @context
+            declarator: (function_declarator
+                declarator: (_) @name
+                parameters: (parameter_list
+                    "(" @context
+                    ")" @context)))
+        (pointer_declarator
+            "*" @context
+            declarator: (pointer_declarator
+                "*" @context
+                declarator: (function_declarator
+                    declarator: (_) @name
+                    parameters: (parameter_list
+                        "(" @context
+                        ")" @context))))
+        (reference_declarator
+            ["&" "&&"] @context
+            (function_declarator
+                declarator: (_) @name
+                parameters: (parameter_list
+                    "(" @context
+                    ")" @context)))
+    ]
+    (type_qualifier)? @context) @item
+
+(declaration
+    (type_qualifier)? @context
+    type: (_)? @context
+    declarator: [
+        (field_identifier) @name
+        (pointer_declarator
+            "*" @context
+            declarator: (field_identifier) @name)
+        (function_declarator
+            declarator: (_) @name
+            parameters: (parameter_list
+                "(" @context
+                ")" @context))
+        (pointer_declarator
+            "*" @context
+            declarator: (function_declarator
+                declarator: (_) @name
+                parameters: (parameter_list
+                    "(" @context
+                    ")" @context)))
+        (pointer_declarator
+            "*" @context
+            declarator: (pointer_declarator
+                "*" @context
+                declarator: (function_declarator
+                    declarator: (_) @name
+                    parameters: (parameter_list
+                        "(" @context
+                        ")" @context))))
+        (reference_declarator
+            ["&" "&&"] @context
+            (function_declarator
+                declarator: (_) @name
+                parameters: (parameter_list
+                    "(" @context
+                    ")" @context)))
+    ]
+    (type_qualifier)? @context) @item
+
+(field_declaration
+    (type_qualifier)? @context
+    type: (_) @context
+    declarator: [
+        (field_identifier) @name
+        (pointer_declarator
+            "*" @context
+            declarator: (field_identifier) @name)
+        (function_declarator
+            declarator: (_) @name
+            parameters: (parameter_list
+                "(" @context
+                ")" @context))
+        (pointer_declarator
+            "*" @context
+            declarator: (function_declarator
+                declarator: (_) @name
+                parameters: (parameter_list
+                    "(" @context
+                    ")" @context)))
+        (pointer_declarator
+            "*" @context
+            declarator: (pointer_declarator
+                "*" @context
+                declarator: (function_declarator
+                    declarator: (_) @name
+                    parameters: (parameter_list
+                        "(" @context
+                        ")" @context))))
+        (reference_declarator
+            ["&" "&&"] @context
+            (function_declarator
+                declarator: (_) @name
+                parameters: (parameter_list
+                    "(" @context
+                    ")" @context)))
+    ]
+    (type_qualifier)? @context) @item

crates/zed2/src/languages/css.rs 🔗

@@ -0,0 +1,130 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use futures::StreamExt;
+use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+use serde_json::json;
+use smol::fs;
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
+
+const SERVER_PATH: &'static str =
+    "node_modules/vscode-langservers-extracted/bin/vscode-css-language-server";
+
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct CssLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl CssLspAdapter {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        CssLspAdapter { node }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for CssLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("vscode-css-language-server".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "css"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(
+            self.node
+                .npm_package_latest_version("vscode-langservers-extracted")
+                .await?,
+        ) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<String>().unwrap();
+        let server_path = container_dir.join(SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[("vscode-langservers-extracted", version.as_str())],
+                )
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        Some(json!({
+            "provideFormatter": true
+        }))
+    }
+}
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last_version_dir = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_dir() {
+                last_version_dir = Some(entry.path());
+            }
+        }
+        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+        let server_path = last_version_dir.join(SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}

crates/zed2/src/languages/css/config.toml 🔗

@@ -0,0 +1,13 @@
+name = "CSS"
+path_suffixes = ["css"]
+autoclose_before = ";:.,=}])>"
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
+]
+word_characters = ["-"]
+block_comment = ["/* ", " */"]
+prettier_parser_name = "css"

crates/zed2/src/languages/css/highlights.scm 🔗

@@ -0,0 +1,78 @@
+(comment) @comment
+
+[
+  (tag_name)
+  (nesting_selector)
+  (universal_selector)
+] @tag
+
+[
+  "~"
+  ">"
+  "+"
+  "-"
+  "*"
+  "/"
+  "="
+  "^="
+  "|="
+  "~="
+  "$="
+  "*="
+  "and"
+  "or"
+  "not"
+  "only"
+] @operator
+
+(attribute_selector (plain_value) @string)
+
+(attribute_name) @attribute
+(pseudo_element_selector (tag_name) @attribute)
+(pseudo_class_selector (class_name) @attribute)
+
+[
+  (class_name)
+  (id_name)
+  (namespace_name)
+  (property_name)
+  (feature_name)
+] @property
+
+(function_name) @function
+
+(
+  [
+    (property_name)
+    (plain_value)
+  ] @variable.special
+  (#match? @variable.special "^--")
+)
+
+[
+  "@media"
+  "@import"
+  "@charset"
+  "@namespace"
+  "@supports"
+  "@keyframes"
+  (at_keyword)
+  (to)
+  (from)
+  (important)
+]  @keyword
+
+(string_value) @string
+(color_value) @string.special
+
+[
+  (integer_value)
+  (float_value)
+] @number
+
+(unit) @type
+
+[
+  ","
+  ":"
+] @punctuation.delimiter

crates/zed2/src/languages/elixir.rs 🔗

@@ -0,0 +1,546 @@
+use anyhow::{anyhow, bail, Context, Result};
+use async_trait::async_trait;
+use futures::StreamExt;
+use gpui2::{AsyncAppContext, Task};
+pub use language2::*;
+use lsp2::{CompletionItemKind, LanguageServerBinary, SymbolKind};
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+use settings2::Settings;
+use smol::fs::{self, File};
+use std::{
+    any::Any,
+    env::consts,
+    ops::Deref,
+    path::PathBuf,
+    sync::{
+        atomic::{AtomicBool, Ordering::SeqCst},
+        Arc,
+    },
+};
+use util::{
+    async_maybe,
+    fs::remove_matching,
+    github::{latest_github_release, GitHubLspBinaryVersion},
+    ResultExt,
+};
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+pub struct ElixirSettings {
+    pub lsp: ElixirLspSetting,
+}
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ElixirLspSetting {
+    ElixirLs,
+    NextLs,
+    Local {
+        path: String,
+        arguments: Vec<String>,
+    },
+}
+
+#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)]
+pub struct ElixirSettingsContent {
+    lsp: Option<ElixirLspSetting>,
+}
+
+impl Settings for ElixirSettings {
+    const KEY: Option<&'static str> = Some("elixir");
+
+    type FileContent = ElixirSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &mut gpui2::AppContext,
+    ) -> Result<Self>
+    where
+        Self: Sized,
+    {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}
+
+pub struct ElixirLspAdapter;
+
+#[async_trait]
+impl LspAdapter for ElixirLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("elixir-ls".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "elixir-ls"
+    }
+
+    fn will_start_server(
+        &self,
+        delegate: &Arc<dyn LspAdapterDelegate>,
+        cx: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
+
+        const NOTIFICATION_MESSAGE: &str = "Could not run the elixir language server, `elixir-ls`, because `elixir` was not found.";
+
+        let delegate = delegate.clone();
+        Some(cx.spawn(|cx| async move {
+            let elixir_output = smol::process::Command::new("elixir")
+                .args(["--version"])
+                .output()
+                .await;
+            if elixir_output.is_err() {
+                if DID_SHOW_NOTIFICATION
+                    .compare_exchange(false, true, SeqCst, SeqCst)
+                    .is_ok()
+                {
+                    cx.update(|cx| {
+                        delegate.show_notification(NOTIFICATION_MESSAGE, cx);
+                    })?
+                }
+                return Err(anyhow!("cannot run elixir-ls"));
+            }
+
+            Ok(())
+        }))
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let http = delegate.http_client();
+        let release = latest_github_release("elixir-lsp/elixir-ls", false, http).await?;
+        let version_name = release
+            .name
+            .strip_prefix("Release ")
+            .context("Elixir-ls release name does not start with prefix")?
+            .to_owned();
+
+        let asset_name = format!("elixir-ls-{}.zip", &version_name);
+        let asset = release
+            .assets
+            .iter()
+            .find(|asset| asset.name == asset_name)
+            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+
+        let version = GitHubLspBinaryVersion {
+            name: version_name,
+            url: asset.browser_download_url.clone(),
+        };
+        Ok(Box::new(version) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+        let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name));
+        let version_dir = container_dir.join(format!("elixir-ls_{}", version.name));
+        let binary_path = version_dir.join("language_server.sh");
+
+        if fs::metadata(&binary_path).await.is_err() {
+            let mut response = delegate
+                .http_client()
+                .get(&version.url, Default::default(), true)
+                .await
+                .context("error downloading release")?;
+            let mut file = File::create(&zip_path)
+                .await
+                .with_context(|| format!("failed to create file {}", zip_path.display()))?;
+            if !response.status().is_success() {
+                Err(anyhow!(
+                    "download failed with status {}",
+                    response.status().to_string()
+                ))?;
+            }
+            futures::io::copy(response.body_mut(), &mut file).await?;
+
+            fs::create_dir_all(&version_dir)
+                .await
+                .with_context(|| format!("failed to create directory {}", version_dir.display()))?;
+            let unzip_status = smol::process::Command::new("unzip")
+                .arg(&zip_path)
+                .arg("-d")
+                .arg(&version_dir)
+                .output()
+                .await?
+                .status;
+            if !unzip_status.success() {
+                Err(anyhow!("failed to unzip elixir-ls archive"))?;
+            }
+
+            remove_matching(&container_dir, |entry| entry != version_dir).await;
+        }
+
+        Ok(LanguageServerBinary {
+            path: binary_path,
+            arguments: vec![],
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary_elixir_ls(container_dir).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary_elixir_ls(container_dir).await
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp2::CompletionItem,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        match completion.kind.zip(completion.detail.as_ref()) {
+            Some((_, detail)) if detail.starts_with("(function)") => {
+                let text = detail.strip_prefix("(function) ")?;
+                let filter_range = 0..text.find('(').unwrap_or(text.len());
+                let source = Rope::from(format!("def {text}").as_str());
+                let runs = language.highlight_text(&source, 4..4 + text.len());
+                return Some(CodeLabel {
+                    text: text.to_string(),
+                    runs,
+                    filter_range,
+                });
+            }
+            Some((_, detail)) if detail.starts_with("(macro)") => {
+                let text = detail.strip_prefix("(macro) ")?;
+                let filter_range = 0..text.find('(').unwrap_or(text.len());
+                let source = Rope::from(format!("defmacro {text}").as_str());
+                let runs = language.highlight_text(&source, 9..9 + text.len());
+                return Some(CodeLabel {
+                    text: text.to_string(),
+                    runs,
+                    filter_range,
+                });
+            }
+            Some((
+                CompletionItemKind::CLASS
+                | CompletionItemKind::MODULE
+                | CompletionItemKind::INTERFACE
+                | CompletionItemKind::STRUCT,
+                _,
+            )) => {
+                let filter_range = 0..completion
+                    .label
+                    .find(" (")
+                    .unwrap_or(completion.label.len());
+                let text = &completion.label[filter_range.clone()];
+                let source = Rope::from(format!("defmodule {text}").as_str());
+                let runs = language.highlight_text(&source, 10..10 + text.len());
+                return Some(CodeLabel {
+                    text: completion.label.clone(),
+                    runs,
+                    filter_range,
+                });
+            }
+            _ => {}
+        }
+
+        None
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        kind: SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        let (text, filter_range, display_range) = match kind {
+            SymbolKind::METHOD | SymbolKind::FUNCTION => {
+                let text = format!("def {}", name);
+                let filter_range = 4..4 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            SymbolKind::CLASS | SymbolKind::MODULE | SymbolKind::INTERFACE | SymbolKind::STRUCT => {
+                let text = format!("defmodule {}", name);
+                let filter_range = 10..10 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            _ => return None,
+        };
+
+        Some(CodeLabel {
+            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
+            text: text[display_range].to_string(),
+            filter_range,
+        })
+    }
+}
+
+async fn get_cached_server_binary_elixir_ls(
+    container_dir: PathBuf,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            last = Some(entry?.path());
+        }
+        last.map(|path| LanguageServerBinary {
+            path,
+            arguments: vec![],
+        })
+        .ok_or_else(|| anyhow!("no cached binary"))
+    })()
+    .await
+    .log_err()
+}
+
+pub struct NextLspAdapter;
+
+#[async_trait]
+impl LspAdapter for NextLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("next-ls".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "next-ls"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let release =
+            latest_github_release("elixir-tools/next-ls", false, delegate.http_client()).await?;
+        let version = release.name.clone();
+        let platform = match consts::ARCH {
+            "x86_64" => "darwin_amd64",
+            "aarch64" => "darwin_arm64",
+            other => bail!("Running on unsupported platform: {other}"),
+        };
+        let asset_name = format!("next_ls_{}", platform);
+        let asset = release
+            .assets
+            .iter()
+            .find(|asset| asset.name == asset_name)
+            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+        let version = GitHubLspBinaryVersion {
+            name: version,
+            url: asset.browser_download_url.clone(),
+        };
+        Ok(Box::new(version) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+
+        let binary_path = container_dir.join("next-ls");
+
+        if fs::metadata(&binary_path).await.is_err() {
+            let mut response = delegate
+                .http_client()
+                .get(&version.url, Default::default(), true)
+                .await
+                .map_err(|err| anyhow!("error downloading release: {}", err))?;
+
+            let mut file = smol::fs::File::create(&binary_path).await?;
+            if !response.status().is_success() {
+                Err(anyhow!(
+                    "download failed with status {}",
+                    response.status().to_string()
+                ))?;
+            }
+            futures::io::copy(response.body_mut(), &mut file).await?;
+
+            fs::set_permissions(
+                &binary_path,
+                <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
+            )
+            .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: binary_path,
+            arguments: vec!["--stdio".into()],
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary_next(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--stdio".into()];
+                binary
+            })
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary_next(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--help".into()];
+                binary
+            })
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp2::CompletionItem,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        label_for_completion_elixir(completion, language)
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        symbol_kind: SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        label_for_symbol_elixir(name, symbol_kind, language)
+    }
+}
+
+async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async_maybe!({
+        let mut last_binary_path = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_file()
+                && entry
+                    .file_name()
+                    .to_str()
+                    .map_or(false, |name| name == "next-ls")
+            {
+                last_binary_path = Some(entry.path());
+            }
+        }
+
+        if let Some(path) = last_binary_path {
+            Ok(LanguageServerBinary {
+                path,
+                arguments: Vec::new(),
+            })
+        } else {
+            Err(anyhow!("no cached binary"))
+        }
+    })
+    .await
+    .log_err()
+}
+
+pub struct LocalLspAdapter {
+    pub path: String,
+    pub arguments: Vec<String>,
+}
+
+#[async_trait]
+impl LspAdapter for LocalLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("local-ls".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "local-ls"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(()) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _: Box<dyn 'static + Send + Any>,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let path = shellexpand::full(&self.path)?;
+        Ok(LanguageServerBinary {
+            path: PathBuf::from(path.deref()),
+            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        let path = shellexpand::full(&self.path).ok()?;
+        Some(LanguageServerBinary {
+            path: PathBuf::from(path.deref()),
+            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
+        })
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        let path = shellexpand::full(&self.path).ok()?;
+        Some(LanguageServerBinary {
+            path: PathBuf::from(path.deref()),
+            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
+        })
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp2::CompletionItem,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        label_for_completion_elixir(completion, language)
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        symbol: SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        label_for_symbol_elixir(name, symbol, language)
+    }
+}
+
+fn label_for_completion_elixir(
+    completion: &lsp2::CompletionItem,
+    language: &Arc<Language>,
+) -> Option<CodeLabel> {
+    return Some(CodeLabel {
+        runs: language.highlight_text(&completion.label.clone().into(), 0..completion.label.len()),
+        text: completion.label.clone(),
+        filter_range: 0..completion.label.len(),
+    });
+}
+
+fn label_for_symbol_elixir(
+    name: &str,
+    _: SymbolKind,
+    language: &Arc<Language>,
+) -> Option<CodeLabel> {
+    Some(CodeLabel {
+        runs: language.highlight_text(&name.into(), 0..name.len()),
+        text: name.to_string(),
+        filter_range: 0..name.len(),
+    })
+}

crates/zed2/src/languages/elixir/config.toml 🔗

@@ -0,0 +1,16 @@
+name = "Elixir"
+path_suffixes = ["ex", "exs"]
+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 = true, newline = false, not_in = ["string", "comment"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
+]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]
+
+[overrides.string]
+word_characters = ["-"]
+opt_into_language_servers = ["tailwindcss-language-server"]

crates/zed2/src/languages/elixir/embedding.scm 🔗

@@ -0,0 +1,27 @@
+(
+    (unary_operator
+        operator: "@"
+        operand: (call
+            target: (identifier) @unary
+            (#match? @unary "^(doc)$"))
+        ) @context
+    .
+    (call
+        target: (identifier) @name
+        (arguments
+            [
+            (identifier) @name
+            (call
+                target: (identifier) @name)
+                (binary_operator
+                    left: (call
+                    target: (identifier) @name)
+                    operator: "when")
+            ])
+        (#match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item
+        )
+
+    (call
+        target: (identifier) @name
+        (arguments (alias) @name)
+        (#match? @name "^(defmodule|defprotocol)$")) @item

crates/zed2/src/languages/elixir/highlights.scm 🔗

@@ -0,0 +1,153 @@
+["when" "and" "or" "not" "in" "not in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword
+
+(unary_operator
+  operator: "&"
+  operand: (integer) @operator)
+
+(operator_identifier) @operator
+
+(unary_operator
+  operator: _ @operator)
+
+(binary_operator
+  operator: _ @operator)
+
+(dot
+  operator: _ @operator)
+
+(stab_clause
+  operator: _ @operator)
+
+[
+  (boolean)
+  (nil)
+] @constant
+
+[
+  (integer)
+  (float)
+] @number
+
+(alias) @type
+
+(call
+  target: (dot
+    left: (atom) @type))
+
+(char) @constant
+
+(escape_sequence) @string.escape
+
+[
+  (atom)
+  (quoted_atom)
+  (keyword)
+  (quoted_keyword)
+] @string.special.symbol
+
+[
+  (string)
+  (charlist)
+] @string
+
+(sigil
+  (sigil_name) @__name__
+  quoted_start: _ @string
+  quoted_end: _ @string
+  (#match? @__name__ "^[sS]$")) @string
+
+(sigil
+  (sigil_name) @__name__
+  quoted_start: _ @string.regex
+  quoted_end: _ @string.regex
+  (#match? @__name__ "^[rR]$")) @string.regex
+
+(sigil
+  (sigil_name) @__name__
+  quoted_start: _ @string.special
+  quoted_end: _ @string.special) @string.special
+
+(
+  (identifier) @comment.unused
+  (#match? @comment.unused "^_")
+)
+
+(call
+  target: [
+    (identifier) @function
+    (dot
+      right: (identifier) @function)
+  ])
+
+(call
+  target: (identifier) @keyword
+  (arguments
+    [
+      (identifier) @function
+      (binary_operator
+        left: (identifier) @function
+        operator: "when")
+      (binary_operator
+        operator: "|>"
+        right: (identifier))
+    ])
+  (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
+
+(binary_operator
+  operator: "|>"
+  right: (identifier) @function)
+
+(call
+  target: (identifier) @keyword
+  (#match? @keyword "^(def|defdelegate|defexception|defguard|defguardp|defimpl|defmacro|defmacrop|defmodule|defn|defnp|defoverridable|defp|defprotocol|defstruct)$"))
+
+(call
+  target: (identifier) @keyword
+  (#match? @keyword "^(alias|case|cond|else|for|if|import|quote|raise|receive|require|reraise|super|throw|try|unless|unquote|unquote_splicing|use|with)$"))
+
+(
+  (identifier) @constant.builtin
+  (#match? @constant.builtin "^(__MODULE__|__DIR__|__ENV__|__CALLER__|__STACKTRACE__)$")
+)
+
+(unary_operator
+  operator: "@" @comment.doc
+  operand: (call
+    target: (identifier) @__attribute__ @comment.doc
+    (arguments
+      [
+        (string)
+        (charlist)
+        (sigil)
+        (boolean)
+      ] @comment.doc))
+  (#match? @__attribute__ "^(moduledoc|typedoc|doc)$"))
+
+(comment) @comment
+
+[
+ "%"
+] @punctuation
+
+[
+ ","
+ ";"
+] @punctuation.delimiter
+
+[
+  "("
+  ")"
+  "["
+  "]"
+  "{"
+  "}"
+  "<<"
+  ">>"
+] @punctuation.bracket
+
+(interpolation "#{" @punctuation.special "}" @punctuation.special) @embedded
+
+((sigil
+  (sigil_name) @_sigil_name
+  (quoted_content) @embedded)
+ (#eq? @_sigil_name "H"))

crates/zed2/src/languages/elixir/outline.scm 🔗

@@ -0,0 +1,26 @@
+(call
+  target: (identifier) @context
+  (arguments (alias) @name)
+  (#match? @context "^(defmodule|defprotocol)$")) @item
+
+(call
+  target: (identifier) @context
+  (arguments
+    [
+      (identifier) @name
+      (call
+          target: (identifier) @name
+          (arguments
+              "(" @context.extra
+              _* @context.extra
+              ")" @context.extra))
+      (binary_operator
+        left: (call
+            target: (identifier) @name
+            (arguments
+                "(" @context.extra
+                _* @context.extra
+                ")" @context.extra))
+        operator: "when")
+    ])
+  (#match? @context "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item

crates/zed2/src/languages/elm/config.toml 🔗

@@ -0,0 +1,11 @@
+name = "Elm"
+path_suffixes = ["elm"]
+line_comment = "-- "
+block_comment = ["{- ", " -}"]
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
+]

crates/zed2/src/languages/elm/highlights.scm 🔗

@@ -0,0 +1,72 @@
+[
+    "if"
+    "then"
+    "else"
+    "let"
+    "in"
+    (case)
+    (of)
+    (backslash)
+    (as)
+    (port)
+    (exposing)
+    (alias)
+    (import)
+    (module)
+    (type)
+    (arrow)
+ ] @keyword
+
+[
+    (eq)
+    (operator_identifier)
+    (colon)
+] @operator
+
+(type_annotation(lower_case_identifier) @function)
+(port_annotation(lower_case_identifier) @function)
+(function_declaration_left(lower_case_identifier) @function.definition)
+
+(function_call_expr
+    target: (value_expr
+        name: (value_qid (lower_case_identifier) @function)))
+
+(exposed_value(lower_case_identifier) @function)
+(exposed_type(upper_case_identifier) @type)
+
+(field_access_expr(value_expr(value_qid)) @identifier)
+(lower_pattern) @variable
+(record_base_identifier) @identifier
+
+[
+    "("
+    ")"
+] @punctuation.bracket
+
+[
+    "|"
+    ","
+] @punctuation.delimiter
+
+(number_constant_expr) @constant
+
+(type_declaration(upper_case_identifier) @type)
+(type_ref) @type
+(type_alias_declaration name: (upper_case_identifier) @type)
+
+(value_expr(upper_case_qid(upper_case_identifier)) @type)
+
+[
+    (line_comment)
+    (block_comment)
+] @comment
+
+(string_escape) @string.escape
+
+[
+    (open_quote)
+    (close_quote)
+    (regular_string_part)
+    (open_char)
+    (close_char)
+] @string

crates/zed2/src/languages/elm/outline.scm 🔗

@@ -0,0 +1,22 @@
+(type_declaration
+    (type) @context
+    (upper_case_identifier) @name) @item
+
+(type_alias_declaration
+    (type) @context
+    (alias) @context
+    name: (upper_case_identifier) @name) @item
+
+(type_alias_declaration
+    typeExpression:
+        (type_expression
+            part: (record_type
+                (field_type
+                    name: (lower_case_identifier) @name) @item)))
+
+(union_variant
+    name: (upper_case_identifier) @name) @item
+
+(value_declaration
+    functionDeclarationLeft:
+        (function_declaration_left(lower_case_identifier) @name)) @item

crates/zed2/src/languages/erb/config.toml 🔗

@@ -0,0 +1,8 @@
+name = "ERB"
+path_suffixes = ["erb"]
+autoclose_before = ">})"
+brackets = [
+    { start = "<", end = ">", close = true, newline = true },
+]
+block_comment = ["<%#", "%>"]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]

crates/zed2/src/languages/glsl/config.toml 🔗

@@ -0,0 +1,9 @@
+name = "GLSL"
+path_suffixes = ["vert", "frag", "tesc", "tese", "geom", "comp"]
+line_comment = "// "
+block_comment = ["/* ", " */"]
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+]

crates/zed2/src/languages/glsl/highlights.scm 🔗

@@ -0,0 +1,118 @@
+"break" @keyword
+"case" @keyword
+"const" @keyword
+"continue" @keyword
+"default" @keyword
+"do" @keyword
+"else" @keyword
+"enum" @keyword
+"extern" @keyword
+"for" @keyword
+"if" @keyword
+"inline" @keyword
+"return" @keyword
+"sizeof" @keyword
+"static" @keyword
+"struct" @keyword
+"switch" @keyword
+"typedef" @keyword
+"union" @keyword
+"volatile" @keyword
+"while" @keyword
+
+"#define" @keyword
+"#elif" @keyword
+"#else" @keyword
+"#endif" @keyword
+"#if" @keyword
+"#ifdef" @keyword
+"#ifndef" @keyword
+"#include" @keyword
+(preproc_directive) @keyword
+
+"--" @operator
+"-" @operator
+"-=" @operator
+"->" @operator
+"=" @operator
+"!=" @operator
+"*" @operator
+"&" @operator
+"&&" @operator
+"+" @operator
+"++" @operator
+"+=" @operator
+"<" @operator
+"==" @operator
+">" @operator
+"||" @operator
+
+"." @delimiter
+";" @delimiter
+
+(string_literal) @string
+(system_lib_string) @string
+
+(null) @constant
+(number_literal) @number
+(char_literal) @number
+
+(call_expression
+  function: (identifier) @function)
+(call_expression
+  function: (field_expression
+    field: (field_identifier) @function))
+(function_declarator
+  declarator: (identifier) @function)
+(preproc_function_def
+  name: (identifier) @function.special)
+
+(field_identifier) @property
+(statement_identifier) @label
+(type_identifier) @type
+(primitive_type) @type
+(sized_type_specifier) @type
+
+((identifier) @constant
+ (#match? @constant "^[A-Z][A-Z\\d_]*$"))
+
+(identifier) @variable
+
+(comment) @comment
+; inherits: c
+
+[
+  "in"
+  "out"
+  "inout"
+  "uniform"
+  "shared"
+  "layout"
+  "attribute"
+  "varying"
+  "buffer"
+  "coherent"
+  "readonly"
+  "writeonly"
+  "precision"
+  "highp"
+  "mediump"
+  "lowp"
+  "centroid"
+  "sample"
+  "patch"
+  "smooth"
+  "flat"
+  "noperspective"
+  "invariant"
+  "precise"
+] @type.qualifier
+
+"subroutine" @keyword.function
+
+(extension_storage_class) @storageclass
+
+(
+  (identifier) @variable.builtin
+  (#match? @variable.builtin "^gl_")
+)

crates/zed2/src/languages/go.rs 🔗

@@ -0,0 +1,464 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use futures::StreamExt;
+use gpui2::{AsyncAppContext, Task};
+pub use language2::*;
+use lazy_static::lazy_static;
+use lsp2::LanguageServerBinary;
+use regex::Regex;
+use smol::{fs, process};
+use std::{
+    any::Any,
+    ffi::{OsStr, OsString},
+    ops::Range,
+    path::PathBuf,
+    str,
+    sync::{
+        atomic::{AtomicBool, Ordering::SeqCst},
+        Arc,
+    },
+};
+use util::{fs::remove_matching, github::latest_github_release, ResultExt};
+
+fn server_binary_arguments() -> Vec<OsString> {
+    vec!["-mode=stdio".into()]
+}
+
+#[derive(Copy, Clone)]
+pub struct GoLspAdapter;
+
+lazy_static! {
+    static ref GOPLS_VERSION_REGEX: Regex = Regex::new(r"\d+\.\d+\.\d+").unwrap();
+}
+
+#[async_trait]
+impl super::LspAdapter for GoLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("gopls".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "gopls"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let release = latest_github_release("golang/tools", false, delegate.http_client()).await?;
+        let version: Option<String> = release.name.strip_prefix("gopls/v").map(str::to_string);
+        if version.is_none() {
+            log::warn!(
+                "couldn't infer gopls version from github release name '{}'",
+                release.name
+            );
+        }
+        Ok(Box::new(version) as Box<_>)
+    }
+
+    fn will_fetch_server(
+        &self,
+        delegate: &Arc<dyn LspAdapterDelegate>,
+        cx: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
+
+        const NOTIFICATION_MESSAGE: &str =
+            "Could not install the Go language server `gopls`, because `go` was not found.";
+
+        let delegate = delegate.clone();
+        Some(cx.spawn(|cx| async move {
+            let install_output = process::Command::new("go").args(["version"]).output().await;
+            if install_output.is_err() {
+                if DID_SHOW_NOTIFICATION
+                    .compare_exchange(false, true, SeqCst, SeqCst)
+                    .is_ok()
+                {
+                    cx.update(|cx| {
+                        delegate.show_notification(NOTIFICATION_MESSAGE, cx);
+                    })?
+                }
+                return Err(anyhow!("cannot install gopls"));
+            }
+            Ok(())
+        }))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<Option<String>>().unwrap();
+        let this = *self;
+
+        if let Some(version) = *version {
+            let binary_path = container_dir.join(&format!("gopls_{version}"));
+            if let Ok(metadata) = fs::metadata(&binary_path).await {
+                if metadata.is_file() {
+                    remove_matching(&container_dir, |entry| {
+                        entry != binary_path && entry.file_name() != Some(OsStr::new("gobin"))
+                    })
+                    .await;
+
+                    return Ok(LanguageServerBinary {
+                        path: binary_path.to_path_buf(),
+                        arguments: server_binary_arguments(),
+                    });
+                }
+            }
+        } else if let Some(path) = this
+            .cached_server_binary(container_dir.clone(), delegate)
+            .await
+        {
+            return Ok(path);
+        }
+
+        let gobin_dir = container_dir.join("gobin");
+        fs::create_dir_all(&gobin_dir).await?;
+        let install_output = process::Command::new("go")
+            .env("GO111MODULE", "on")
+            .env("GOBIN", &gobin_dir)
+            .args(["install", "golang.org/x/tools/gopls@latest"])
+            .output()
+            .await?;
+        if !install_output.status.success() {
+            Err(anyhow!("failed to install gopls. Is go installed?"))?;
+        }
+
+        let installed_binary_path = gobin_dir.join("gopls");
+        let version_output = process::Command::new(&installed_binary_path)
+            .arg("version")
+            .output()
+            .await
+            .map_err(|e| anyhow!("failed to run installed gopls binary {:?}", e))?;
+        let version_stdout = str::from_utf8(&version_output.stdout)
+            .map_err(|_| anyhow!("gopls version produced invalid utf8"))?;
+        let version = GOPLS_VERSION_REGEX
+            .find(version_stdout)
+            .ok_or_else(|| anyhow!("failed to parse gopls version output"))?
+            .as_str();
+        let binary_path = container_dir.join(&format!("gopls_{version}"));
+        fs::rename(&installed_binary_path, &binary_path).await?;
+
+        Ok(LanguageServerBinary {
+            path: binary_path.to_path_buf(),
+            arguments: server_binary_arguments(),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--help".into()];
+                binary
+            })
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp2::CompletionItem,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        let label = &completion.label;
+
+        // Gopls returns nested fields and methods as completions.
+        // To syntax highlight these, combine their final component
+        // with their detail.
+        let name_offset = label.rfind('.').unwrap_or(0);
+
+        match completion.kind.zip(completion.detail.as_ref()) {
+            Some((lsp2::CompletionItemKind::MODULE, detail)) => {
+                let text = format!("{label} {detail}");
+                let source = Rope::from(format!("import {text}").as_str());
+                let runs = language.highlight_text(&source, 7..7 + text.len());
+                return Some(CodeLabel {
+                    text,
+                    runs,
+                    filter_range: 0..label.len(),
+                });
+            }
+            Some((
+                lsp2::CompletionItemKind::CONSTANT | lsp2::CompletionItemKind::VARIABLE,
+                detail,
+            )) => {
+                let text = format!("{label} {detail}");
+                let source =
+                    Rope::from(format!("var {} {}", &text[name_offset..], detail).as_str());
+                let runs = adjust_runs(
+                    name_offset,
+                    language.highlight_text(&source, 4..4 + text.len()),
+                );
+                return Some(CodeLabel {
+                    text,
+                    runs,
+                    filter_range: 0..label.len(),
+                });
+            }
+            Some((lsp2::CompletionItemKind::STRUCT, _)) => {
+                let text = format!("{label} struct {{}}");
+                let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
+                let runs = adjust_runs(
+                    name_offset,
+                    language.highlight_text(&source, 5..5 + text.len()),
+                );
+                return Some(CodeLabel {
+                    text,
+                    runs,
+                    filter_range: 0..label.len(),
+                });
+            }
+            Some((lsp2::CompletionItemKind::INTERFACE, _)) => {
+                let text = format!("{label} interface {{}}");
+                let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
+                let runs = adjust_runs(
+                    name_offset,
+                    language.highlight_text(&source, 5..5 + text.len()),
+                );
+                return Some(CodeLabel {
+                    text,
+                    runs,
+                    filter_range: 0..label.len(),
+                });
+            }
+            Some((lsp2::CompletionItemKind::FIELD, detail)) => {
+                let text = format!("{label} {detail}");
+                let source =
+                    Rope::from(format!("type T struct {{ {} }}", &text[name_offset..]).as_str());
+                let runs = adjust_runs(
+                    name_offset,
+                    language.highlight_text(&source, 16..16 + text.len()),
+                );
+                return Some(CodeLabel {
+                    text,
+                    runs,
+                    filter_range: 0..label.len(),
+                });
+            }
+            Some((
+                lsp2::CompletionItemKind::FUNCTION | lsp2::CompletionItemKind::METHOD,
+                detail,
+            )) => {
+                if let Some(signature) = detail.strip_prefix("func") {
+                    let text = format!("{label}{signature}");
+                    let source = Rope::from(format!("func {} {{}}", &text[name_offset..]).as_str());
+                    let runs = adjust_runs(
+                        name_offset,
+                        language.highlight_text(&source, 5..5 + text.len()),
+                    );
+                    return Some(CodeLabel {
+                        filter_range: 0..label.len(),
+                        text,
+                        runs,
+                    });
+                }
+            }
+            _ => {}
+        }
+        None
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        kind: lsp2::SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        let (text, filter_range, display_range) = match kind {
+            lsp2::SymbolKind::METHOD | lsp2::SymbolKind::FUNCTION => {
+                let text = format!("func {} () {{}}", name);
+                let filter_range = 5..5 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::STRUCT => {
+                let text = format!("type {} struct {{}}", name);
+                let filter_range = 5..5 + name.len();
+                let display_range = 0..text.len();
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::INTERFACE => {
+                let text = format!("type {} interface {{}}", name);
+                let filter_range = 5..5 + name.len();
+                let display_range = 0..text.len();
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::CLASS => {
+                let text = format!("type {} T", name);
+                let filter_range = 5..5 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::CONSTANT => {
+                let text = format!("const {} = nil", name);
+                let filter_range = 6..6 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::VARIABLE => {
+                let text = format!("var {} = nil", name);
+                let filter_range = 4..4 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::MODULE => {
+                let text = format!("package {}", name);
+                let filter_range = 8..8 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            _ => return None,
+        };
+
+        Some(CodeLabel {
+            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
+            text: text[display_range].to_string(),
+            filter_range,
+        })
+    }
+}
+
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last_binary_path = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_file()
+                && entry
+                    .file_name()
+                    .to_str()
+                    .map_or(false, |name| name.starts_with("gopls_"))
+            {
+                last_binary_path = Some(entry.path());
+            }
+        }
+
+        if let Some(path) = last_binary_path {
+            Ok(LanguageServerBinary {
+                path,
+                arguments: server_binary_arguments(),
+            })
+        } else {
+            Err(anyhow!("no cached binary"))
+        }
+    })()
+    .await
+    .log_err()
+}
+
+fn adjust_runs(
+    delta: usize,
+    mut runs: Vec<(Range<usize>, HighlightId)>,
+) -> Vec<(Range<usize>, HighlightId)> {
+    for (range, _) in &mut runs {
+        range.start += delta;
+        range.end += delta;
+    }
+    runs
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::languages::language;
+    use gpui2::Hsla;
+    use theme2::SyntaxTheme;
+
+    #[gpui2::test]
+    async fn test_go_label_for_completion() {
+        let language = language(
+            "go",
+            tree_sitter_go::language(),
+            Some(Arc::new(GoLspAdapter)),
+        )
+        .await;
+
+        let theme = SyntaxTheme::new_test([
+            ("type", Hsla::default()),
+            ("keyword", Hsla::default()),
+            ("function", Hsla::default()),
+            ("number", Hsla::default()),
+            ("property", Hsla::default()),
+        ]);
+        language.set_theme(&theme);
+
+        let grammar = language.grammar().unwrap();
+        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
+        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
+        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
+        let highlight_number = grammar.highlight_id_for_name("number").unwrap();
+        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
+
+        assert_eq!(
+            language
+                .label_for_completion(&lsp2::CompletionItem {
+                    kind: Some(lsp2::CompletionItemKind::FUNCTION),
+                    label: "Hello".to_string(),
+                    detail: Some("func(a B) c.D".to_string()),
+                    ..Default::default()
+                })
+                .await,
+            Some(CodeLabel {
+                text: "Hello(a B) c.D".to_string(),
+                filter_range: 0..5,
+                runs: vec![
+                    (0..5, highlight_function),
+                    (8..9, highlight_type),
+                    (13..14, highlight_type),
+                ],
+            })
+        );
+
+        // Nested methods
+        assert_eq!(
+            language
+                .label_for_completion(&lsp2::CompletionItem {
+                    kind: Some(lsp2::CompletionItemKind::METHOD),
+                    label: "one.two.Three".to_string(),
+                    detail: Some("func() [3]interface{}".to_string()),
+                    ..Default::default()
+                })
+                .await,
+            Some(CodeLabel {
+                text: "one.two.Three() [3]interface{}".to_string(),
+                filter_range: 0..13,
+                runs: vec![
+                    (8..13, highlight_function),
+                    (17..18, highlight_number),
+                    (19..28, highlight_keyword),
+                ],
+            })
+        );
+
+        // Nested fields
+        assert_eq!(
+            language
+                .label_for_completion(&lsp2::CompletionItem {
+                    kind: Some(lsp2::CompletionItemKind::FIELD),
+                    label: "two.Three".to_string(),
+                    detail: Some("a.Bcd".to_string()),
+                    ..Default::default()
+                })
+                .await,
+            Some(CodeLabel {
+                text: "two.Three a.Bcd".to_string(),
+                filter_range: 0..9,
+                runs: vec![(4..9, highlight_field), (12..15, highlight_type)],
+            })
+        );
+    }
+}

crates/zed2/src/languages/go/config.toml 🔗

@@ -0,0 +1,12 @@
+name = "Go"
+path_suffixes = ["go"]
+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 = true, newline = false, not_in = ["comment", "string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] },
+    { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
+]

crates/zed2/src/languages/go/embedding.scm 🔗

@@ -0,0 +1,24 @@
+(
+    (comment)* @context
+    .
+    (type_declaration
+        (type_spec
+            name: (_) @name)
+    ) @item
+)
+
+(
+    (comment)* @context
+    .
+    (function_declaration
+        name: (_) @name
+    ) @item
+)
+
+(
+    (comment)* @context
+    .
+    (method_declaration
+        name: (_) @name
+    ) @item
+)

crates/zed2/src/languages/go/highlights.scm 🔗

@@ -0,0 +1,107 @@
+(identifier) @variable
+(type_identifier) @type
+(field_identifier) @property
+
+(call_expression
+  function: (identifier) @function)
+
+(call_expression
+  function: (selector_expression
+    field: (field_identifier) @function.method))
+
+(function_declaration
+  name: (identifier) @function)
+
+(method_declaration
+  name: (field_identifier) @function.method)
+
+[
+  "--"
+  "-"
+  "-="
+  ":="
+  "!"
+  "!="
+  "..."
+  "*"
+  "*"
+  "*="
+  "/"
+  "/="
+  "&"
+  "&&"
+  "&="
+  "%"
+  "%="
+  "^"
+  "^="
+  "+"
+  "++"
+  "+="
+  "<-"
+  "<"
+  "<<"
+  "<<="
+  "<="
+  "="
+  "=="
+  ">"
+  ">="
+  ">>"
+  ">>="
+  "|"
+  "|="
+  "||"
+  "~"
+] @operator
+
+[
+  "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
+
+[
+  (interpreted_string_literal)
+  (raw_string_literal)
+  (rune_literal)
+] @string
+
+(escape_sequence) @escape
+
+[
+  (int_literal)
+  (float_literal)
+  (imaginary_literal)
+] @number
+
+[
+  (true)
+  (false)
+  (nil)
+  (iota)
+] @constant.builtin
+
+(comment) @comment

crates/zed2/src/languages/go/indents.scm 🔗

@@ -0,0 +1,9 @@
+[
+    (assignment_statement)
+    (call_expression)
+    (selector_expression)
+] @indent
+
+(_ "[" "]" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent

crates/zed2/src/languages/go/outline.scm 🔗

@@ -0,0 +1,43 @@
+(type_declaration
+    "type" @context
+    (type_spec
+        name: (_) @name)) @item
+
+(function_declaration
+    "func" @context
+    name: (identifier) @name
+    parameters: (parameter_list
+      "(" @context
+      ")" @context)) @item
+
+(method_declaration
+    "func" @context
+    receiver: (parameter_list
+        "(" @context
+        (parameter_declaration
+            type: (_) @context)
+        ")" @context)
+    name: (field_identifier) @name
+    parameters: (parameter_list
+      "(" @context
+      ")" @context)) @item
+
+(const_declaration
+    "const" @context
+    (const_spec
+        name: (identifier) @name) @item)
+
+(source_file
+    (var_declaration
+        "var" @context
+        (var_spec
+            name: (identifier) @name) @item))
+
+(method_spec
+    name: (_) @name
+    parameters: (parameter_list
+      "(" @context
+      ")" @context)) @item
+
+(field_declaration
+    name: (_) @name) @item

crates/zed2/src/languages/heex/config.toml 🔗

@@ -0,0 +1,12 @@
+name = "HEEX"
+path_suffixes = ["heex"]
+autoclose_before = ">})"
+brackets = [
+    { start = "<", end = ">", close = true, newline = true },
+]
+block_comment = ["<%!-- ", " --%>"]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]
+
+[overrides.string]
+word_characters = ["-"]
+opt_into_language_servers = ["tailwindcss-language-server"]

crates/zed2/src/languages/heex/highlights.scm 🔗

@@ -0,0 +1,57 @@
+; HEEx delimiters
+[
+  "/>"
+  "<!"
+  "<"
+  "</"
+  "</:"
+  "<:"
+  ">"
+  "{"
+  "}"
+] @punctuation.bracket
+
+[
+  "<%!--"
+  "<%"
+  "<%#"
+  "<%%="
+  "<%="
+  "%>"
+  "--%>"
+  "-->"
+  "<!--"
+] @keyword
+
+; HEEx operators are highlighted as such
+"=" @operator
+
+; HEEx inherits the DOCTYPE tag from HTML
+(doctype) @constant
+
+(comment) @comment
+
+; HEEx tags and slots are highlighted as HTML
+[
+ (tag_name)
+ (slot_name)
+] @tag
+
+; HEEx attributes are highlighted as HTML attributes
+(attribute_name) @attribute
+
+; HEEx special attributes are highlighted as keywords
+(special_attribute_name) @keyword
+
+[
+  (attribute_value)
+  (quoted_attribute_value)
+] @string
+
+; HEEx components are highlighted as Elixir modules and functions
+(component_name
+  [
+    (module) @module
+    (function) @function
+    "." @punctuation.delimiter
+  ])

crates/zed2/src/languages/heex/injections.scm 🔗

@@ -0,0 +1,13 @@
+(
+  (directive
+    [
+      (partial_expression_value)
+      (expression_value)
+      (ending_expression_value)
+    ] @content)
+  (#set! language "elixir")
+  (#set! combined)
+)
+
+((expression (expression_value) @content)
+ (#set! language "elixir"))

crates/zed2/src/languages/html.rs 🔗

@@ -0,0 +1,130 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use futures::StreamExt;
+use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+use serde_json::json;
+use smol::fs;
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
+
+const SERVER_PATH: &'static str =
+    "node_modules/vscode-langservers-extracted/bin/vscode-html-language-server";
+
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct HtmlLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl HtmlLspAdapter {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        HtmlLspAdapter { node }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for HtmlLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("vscode-html-language-server".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "html"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(
+            self.node
+                .npm_package_latest_version("vscode-langservers-extracted")
+                .await?,
+        ) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<String>().unwrap();
+        let server_path = container_dir.join(SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[("vscode-langservers-extracted", version.as_str())],
+                )
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        Some(json!({
+            "provideFormatter": true
+        }))
+    }
+}
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last_version_dir = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_dir() {
+                last_version_dir = Some(entry.path());
+            }
+        }
+        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+        let server_path = last_version_dir.join(SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}

crates/zed2/src/languages/html/config.toml 🔗

@@ -0,0 +1,14 @@
+name = "HTML"
+path_suffixes = ["html"]
+autoclose_before = ">})"
+block_comment = ["<!-- ", " -->"]
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] },
+    { start = "<", end = ">", close = true, newline = true, not_in = ["comment", "string"] },
+    { start = "!--", end = " --", close = true, newline = false, not_in = ["comment", "string"] },
+]
+word_characters = ["-"]
+prettier_parser_name = "html"

crates/zed2/src/languages/html/highlights.scm 🔗

@@ -0,0 +1,15 @@
+(tag_name) @keyword
+(erroneous_end_tag_name) @keyword
+(doctype) @constant
+(attribute_name) @property
+(attribute_value) @string
+(comment) @comment
+
+"=" @operator
+
+[
+  "<"
+  ">"
+  "</"
+  "/>"
+] @punctuation.bracket

crates/zed2/src/languages/javascript/config.toml 🔗

@@ -0,0 +1,26 @@
+name = "JavaScript"
+path_suffixes = ["js", "jsx", "mjs", "cjs"]
+first_line_pattern = '^#!.*\bnode\b'
+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, not_in = ["comment", "string"] },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] },
+    { start = "`", end = "`", close = true, newline = false, not_in = ["comment", "string"] },
+    { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
+]
+word_characters = ["$", "#"]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]
+prettier_parser_name = "babel"
+
+[overrides.element]
+line_comment = { remove = true }
+block_comment = ["{/* ", " */}"]
+
+[overrides.string]
+word_characters = ["-"]
+opt_into_language_servers = ["tailwindcss-language-server"]

crates/zed2/src/languages/javascript/embedding.scm 🔗

@@ -0,0 +1,71 @@
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (function_declaration
+                "async"? @name
+                "function" @name
+                name: (_) @name))
+        (function_declaration
+            "async"? @name
+            "function" @name
+            name: (_) @name)
+    ] @item
+)
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (class_declaration
+                "class" @name
+                name: (_) @name))
+        (class_declaration
+            "class" @name
+            name: (_) @name)
+    ] @item
+)
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (interface_declaration
+                "interface" @name
+                name: (_) @name))
+        (interface_declaration
+            "interface" @name
+            name: (_) @name)
+    ] @item
+)
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (enum_declaration
+                "enum" @name
+                name: (_) @name))
+        (enum_declaration
+            "enum" @name
+            name: (_) @name)
+    ] @item
+)
+
+(
+    (comment)* @context
+    .
+    (method_definition
+        [
+            "get"
+            "set"
+            "async"
+            "*"
+            "static"
+            ]* @name
+        name: (_) @name) @item
+)

crates/zed2/src/languages/javascript/highlights.scm 🔗

@@ -0,0 +1,217 @@
+; 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) @type
+ (#match? @type "^[A-Z]"))
+(type_identifier) @type
+(predefined_type) @type.builtin
+
+([
+  (identifier)
+  (shorthand_property_identifier)
+  (shorthand_property_identifier_pattern)
+ ] @constant
+ (#match? @constant "^_*[A-Z_][A-Z\\d_]*$"))
+
+; Literals
+
+(this) @variable.special
+(super) @variable.special
+
+[
+  (null)
+  (undefined)
+] @constant.builtin
+
+[
+  (true)
+  (false)
+] @boolean
+
+(comment) @comment
+
+[
+  (string)
+  (template_string)
+] @string
+
+(regex) @string.regex
+(number) @number
+
+; Tokens
+
+[
+  ";"
+  "?."
+  "."
+  ","
+  ":"
+] @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
+
+(template_substitution
+  "${" @punctuation.special
+  "}" @punctuation.special) @embedded
+
+(type_arguments
+  "<" @punctuation.bracket
+  ">" @punctuation.bracket)
+
+; Keywords
+
+[ "abstract"
+  "declare"
+  "enum"
+  "export"
+  "implements"
+  "interface"
+  "keyof"
+  "namespace"
+  "private"
+  "protected"
+  "public"
+  "type"
+  "readonly"
+  "override"
+] @keyword

crates/zed2/src/languages/javascript/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/zed2/src/languages/javascript/outline.scm 🔗

@@ -0,0 +1,62 @@
+(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
+    (export_statement
+        (lexical_declaration
+            ["let" "const"] @context
+            (variable_declarator
+                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/zed2/src/languages/json.rs 🔗

@@ -0,0 +1,184 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use collections::HashMap;
+use feature_flags2::FeatureFlagAppExt;
+use futures::{future::BoxFuture, FutureExt, StreamExt};
+use gpui2::AppContext;
+use language2::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+use serde_json::json;
+use settings2::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
+use smol::fs;
+use std::{
+    any::Any,
+    ffi::OsString,
+    future,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::{paths, ResultExt};
+
+const SERVER_PATH: &'static str =
+    "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
+
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct JsonLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+    languages: Arc<LanguageRegistry>,
+}
+
+impl JsonLspAdapter {
+    pub fn new(node: Arc<dyn NodeRuntime>, languages: Arc<LanguageRegistry>) -> Self {
+        JsonLspAdapter { node, languages }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for JsonLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("json-language-server".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "json"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(
+            self.node
+                .npm_package_latest_version("vscode-json-languageserver")
+                .await?,
+        ) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<String>().unwrap();
+        let server_path = container_dir.join(SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[("vscode-json-languageserver", version.as_str())],
+                )
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        Some(json!({
+            "provideFormatter": true
+        }))
+    }
+
+    fn workspace_configuration(
+        &self,
+        cx: &mut AppContext,
+    ) -> BoxFuture<'static, serde_json::Value> {
+        let action_names = cx.all_action_names().collect::<Vec<_>>();
+        let staff_mode = cx.is_staff();
+        let language_names = &self.languages.language_names();
+        let settings_schema = cx.global::<SettingsStore>().json_schema(
+            &SettingsJsonSchemaParams {
+                language_names,
+                staff_mode,
+            },
+            cx,
+        );
+
+        future::ready(serde_json::json!({
+            "json": {
+                "format": {
+                    "enable": true,
+                },
+                "schemas": [
+                    {
+                        "fileMatch": [
+                            schema_file_match(&paths::SETTINGS),
+                            &*paths::LOCAL_SETTINGS_RELATIVE_PATH,
+                        ],
+                        "schema": settings_schema,
+                    },
+                    {
+                        "fileMatch": [schema_file_match(&paths::KEYMAP)],
+                        "schema": KeymapFile::generate_json_schema(&action_names),
+                    }
+                ]
+            }
+        }))
+        .boxed()
+    }
+
+    async fn language_ids(&self) -> HashMap<String, String> {
+        [("JSON".into(), "jsonc".into())].into_iter().collect()
+    }
+}
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last_version_dir = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_dir() {
+                last_version_dir = Some(entry.path());
+            }
+        }
+
+        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+        let server_path = last_version_dir.join(SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}
+
+fn schema_file_match(path: &Path) -> &Path {
+    path.strip_prefix(path.parent().unwrap().parent().unwrap())
+        .unwrap()
+}

crates/zed2/src/languages/json/config.toml 🔗

@@ -0,0 +1,10 @@
+name = "JSON"
+path_suffixes = ["json"]
+line_comment = "// "
+autoclose_before = ",]}"
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+]
+prettier_parser_name = "json"

crates/zed2/src/languages/json/embedding.scm 🔗

@@ -0,0 +1,14 @@
+; Only produce one embedding for the entire file.
+(document) @item
+
+; Collapse arrays, except for the first object.
+(array
+  "[" @keep
+  .
+  (object)? @keep
+  "]" @keep) @collapse
+
+; Collapse string values (but not keys).
+(pair value: (string
+  "\"" @keep
+  "\"" @keep) @collapse)

crates/zed2/src/languages/json/highlights.scm 🔗

@@ -0,0 +1,21 @@
+(comment) @comment
+
+(string) @string
+
+(pair
+  key: (string) @property)
+
+(number) @number
+
+[
+  (true)
+  (false)
+  (null)
+] @constant
+
+[
+  "{"
+  "}"
+  "["
+  "]"
+] @punctuation.bracket

crates/zed2/src/languages/language_plugin.rs 🔗

@@ -0,0 +1,168 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use collections::HashMap;
+use futures::lock::Mutex;
+use gpui2::executor::Background;
+use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, WasiFn};
+use std::{any::Any, path::PathBuf, sync::Arc};
+use util::ResultExt;
+
+#[allow(dead_code)]
+pub async fn new_json(executor: Arc<Background>) -> Result<PluginLspAdapter> {
+    let plugin = PluginBuilder::new_default()?
+        .host_function_async("command", |command: String| async move {
+            let mut args = command.split(' ');
+            let command = args.next().unwrap();
+            smol::process::Command::new(command)
+                .args(args)
+                .output()
+                .await
+                .log_err()
+                .map(|output| output.stdout)
+        })?
+        .init(PluginBinary::Precompiled(include_bytes!(
+            "../../../../plugins/bin/json_language.wasm.pre",
+        )))
+        .await?;
+
+    PluginLspAdapter::new(plugin, executor).await
+}
+
+pub struct PluginLspAdapter {
+    name: WasiFn<(), String>,
+    fetch_latest_server_version: WasiFn<(), Option<String>>,
+    fetch_server_binary: WasiFn<(PathBuf, String), Result<LanguageServerBinary, String>>,
+    cached_server_binary: WasiFn<PathBuf, Option<LanguageServerBinary>>,
+    initialization_options: WasiFn<(), String>,
+    language_ids: WasiFn<(), Vec<(String, String)>>,
+    executor: Arc<Background>,
+    runtime: Arc<Mutex<Plugin>>,
+}
+
+impl PluginLspAdapter {
+    #[allow(unused)]
+    pub async fn new(mut plugin: Plugin, executor: Arc<Background>) -> Result<Self> {
+        Ok(Self {
+            name: plugin.function("name")?,
+            fetch_latest_server_version: plugin.function("fetch_latest_server_version")?,
+            fetch_server_binary: plugin.function("fetch_server_binary")?,
+            cached_server_binary: plugin.function("cached_server_binary")?,
+            initialization_options: plugin.function("initialization_options")?,
+            language_ids: plugin.function("language_ids")?,
+            executor,
+            runtime: Arc::new(Mutex::new(plugin)),
+        })
+    }
+}
+
+#[async_trait]
+impl LspAdapter for PluginLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        let name: String = self
+            .runtime
+            .lock()
+            .await
+            .call(&self.name, ())
+            .await
+            .unwrap();
+        LanguageServerName(name.into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "PluginLspAdapter"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let runtime = self.runtime.clone();
+        let function = self.fetch_latest_server_version;
+        self.executor
+            .spawn(async move {
+                let mut runtime = runtime.lock().await;
+                let versions: Result<Option<String>> =
+                    runtime.call::<_, Option<String>>(&function, ()).await;
+                versions
+                    .map_err(|e| anyhow!("{}", e))?
+                    .ok_or_else(|| anyhow!("Could not fetch latest server version"))
+                    .map(|v| Box::new(v) as Box<_>)
+            })
+            .await
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = *version.downcast::<String>().unwrap();
+        let runtime = self.runtime.clone();
+        let function = self.fetch_server_binary;
+        self.executor
+            .spawn(async move {
+                let mut runtime = runtime.lock().await;
+                let handle = runtime.attach_path(&container_dir)?;
+                let result: Result<LanguageServerBinary, String> =
+                    runtime.call(&function, (container_dir, version)).await?;
+                runtime.remove_resource(handle)?;
+                result.map_err(|e| anyhow!("{}", e))
+            })
+            .await
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        let runtime = self.runtime.clone();
+        let function = self.cached_server_binary;
+
+        self.executor
+            .spawn(async move {
+                let mut runtime = runtime.lock().await;
+                let handle = runtime.attach_path(&container_dir).ok()?;
+                let result: Option<LanguageServerBinary> =
+                    runtime.call(&function, container_dir).await.ok()?;
+                runtime.remove_resource(handle).ok()?;
+                result
+            })
+            .await
+    }
+
+    fn can_be_reinstalled(&self) -> bool {
+        false
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        None
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        let string: String = self
+            .runtime
+            .lock()
+            .await
+            .call(&self.initialization_options, ())
+            .await
+            .log_err()?;
+
+        serde_json::from_str(&string).ok()
+    }
+
+    async fn language_ids(&self) -> HashMap<String, String> {
+        self.runtime
+            .lock()
+            .await
+            .call(&self.language_ids, ())
+            .await
+            .log_err()
+            .unwrap_or_default()
+            .into_iter()
+            .collect()
+    }
+}

crates/zed2/src/languages/lua.rs 🔗

@@ -0,0 +1,135 @@
+use anyhow::{anyhow, bail, Result};
+use async_compression::futures::bufread::GzipDecoder;
+use async_tar::Archive;
+use async_trait::async_trait;
+use futures::{io::BufReader, StreamExt};
+use language2::{LanguageServerName, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use smol::fs;
+use std::{any::Any, env::consts, path::PathBuf};
+use util::{
+    async_maybe,
+    github::{latest_github_release, GitHubLspBinaryVersion},
+    ResultExt,
+};
+
+#[derive(Copy, Clone)]
+pub struct LuaLspAdapter;
+
+#[async_trait]
+impl super::LspAdapter for LuaLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("lua-language-server".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "lua"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let release =
+            latest_github_release("LuaLS/lua-language-server", false, delegate.http_client())
+                .await?;
+        let version = release.name.clone();
+        let platform = match consts::ARCH {
+            "x86_64" => "x64",
+            "aarch64" => "arm64",
+            other => bail!("Running on unsupported platform: {other}"),
+        };
+        let asset_name = format!("lua-language-server-{version}-darwin-{platform}.tar.gz");
+        let asset = release
+            .assets
+            .iter()
+            .find(|asset| asset.name == asset_name)
+            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+        let version = GitHubLspBinaryVersion {
+            name: release.name.clone(),
+            url: asset.browser_download_url.clone(),
+        };
+        Ok(Box::new(version) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+
+        let binary_path = container_dir.join("bin/lua-language-server");
+
+        if fs::metadata(&binary_path).await.is_err() {
+            let mut response = delegate
+                .http_client()
+                .get(&version.url, Default::default(), true)
+                .await
+                .map_err(|err| anyhow!("error downloading release: {}", err))?;
+            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
+            let archive = Archive::new(decompressed_bytes);
+            archive.unpack(container_dir).await?;
+        }
+
+        fs::set_permissions(
+            &binary_path,
+            <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
+        )
+        .await?;
+        Ok(LanguageServerBinary {
+            path: binary_path,
+            arguments: Vec::new(),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--version".into()];
+                binary
+            })
+    }
+}
+
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async_maybe!({
+        let mut last_binary_path = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_file()
+                && entry
+                    .file_name()
+                    .to_str()
+                    .map_or(false, |name| name == "lua-language-server")
+            {
+                last_binary_path = Some(entry.path());
+            }
+        }
+
+        if let Some(path) = last_binary_path {
+            Ok(LanguageServerBinary {
+                path,
+                arguments: Vec::new(),
+            })
+        } else {
+            Err(anyhow!("no cached binary"))
+        }
+    })
+    .await
+    .log_err()
+}

crates/zed2/src/languages/lua/config.toml 🔗

@@ -0,0 +1,10 @@
+name = "Lua"
+path_suffixes = ["lua"]
+line_comment = "-- "
+autoclose_before = ",]}"
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+]
+collapsed_placeholder = "--[ ... ]--"

crates/zed2/src/languages/lua/highlights.scm 🔗

@@ -0,0 +1,198 @@
+;; Keywords
+
+"return" @keyword
+
+[
+ "goto"
+ "in"
+ "local"
+] @keyword
+
+(break_statement) @keyword
+
+(do_statement
+[
+  "do"
+  "end"
+] @keyword)
+
+(while_statement
+[
+  "while"
+  "do"
+  "end"
+] @keyword)
+
+(repeat_statement
+[
+  "repeat"
+  "until"
+] @keyword)
+
+(if_statement
+[
+  "if"
+  "elseif"
+  "else"
+  "then"
+  "end"
+] @keyword)
+
+(elseif_statement
+[
+  "elseif"
+  "then"
+  "end"
+] @keyword)
+
+(else_statement
+[
+  "else"
+  "end"
+] @keyword)
+
+(for_statement
+[
+  "for"
+  "do"
+  "end"
+] @keyword)
+
+(function_declaration
+[
+  "function"
+  "end"
+] @keyword)
+
+(function_definition
+[
+  "function"
+  "end"
+] @keyword)
+
+;; Operators
+
+[
+ "and"
+ "not"
+ "or"
+] @operator
+
+[
+  "+"
+  "-"
+  "*"
+  "/"
+  "%"
+  "^"
+  "#"
+  "=="
+  "~="
+  "<="
+  ">="
+  "<"
+  ">"
+  "="
+  "&"
+  "~"
+  "|"
+  "<<"
+  ">>"
+  "//"
+  ".."
+] @operator
+
+;; Punctuations
+
+[
+  ";"
+  ":"
+  ","
+  "."
+] @punctuation.delimiter
+
+;; Brackets
+
+[
+ "("
+ ")"
+ "["
+ "]"
+ "{"
+ "}"
+] @punctuation.bracket
+
+;; Variables
+
+(identifier) @variable
+
+((identifier) @variable.special
+ (#eq? @variable.special "self"))
+
+(variable_list
+   attribute: (attribute
+     (["<" ">"] @punctuation.bracket
+      (identifier) @attribute)))
+
+;; Constants
+
+((identifier) @constant
+ (#match? @constant "^[A-Z][A-Z_0-9]*$"))
+
+(vararg_expression) @constant
+
+(nil) @constant.builtin
+
+[
+  (false)
+  (true)
+] @boolean
+
+;; Tables
+
+(field name: (identifier) @field)
+
+(dot_index_expression field: (identifier) @field)
+
+(table_constructor
+[
+  "{"
+  "}"
+] @constructor)
+
+;; Functions
+
+(parameters (identifier) @parameter)
+
+(function_call
+  name: [
+    (identifier) @function
+    (dot_index_expression field: (identifier) @function)
+  ])
+
+(function_declaration
+  name: [
+    (identifier) @function.definition
+    (dot_index_expression field: (identifier) @function.definition)
+  ])
+
+(method_index_expression method: (identifier) @method)
+
+(function_call
+  (identifier) @function.builtin
+  (#any-of? @function.builtin
+    ;; built-in functions in Lua 5.1
+    "assert" "collectgarbage" "dofile" "error" "getfenv" "getmetatable" "ipairs"
+    "load" "loadfile" "loadstring" "module" "next" "pairs" "pcall" "print"
+    "rawequal" "rawget" "rawset" "require" "select" "setfenv" "setmetatable"
+    "tonumber" "tostring" "type" "unpack" "xpcall"))
+
+;; Others
+
+(comment) @comment
+
+(hash_bang_line) @preproc
+
+(number) @number
+
+(string) @string

crates/zed2/src/languages/lua/indents.scm 🔗

@@ -0,0 +1,10 @@
+(if_statement "end" @end) @indent
+(do_statement "end" @end) @indent
+(while_statement "end" @end) @indent
+(for_statement "end" @end) @indent
+(repeat_statement "until" @end) @indent
+(function_declaration "end" @end) @indent
+
+(_ "[" "]" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent

crates/zed2/src/languages/markdown/config.toml 🔗

@@ -0,0 +1,11 @@
+name = "Markdown"
+path_suffixes = ["md", "mdx"]
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "<", end = ">", close = true, newline = true },
+    { start = "\"", end = "\"", close = false, newline = false },
+    { start = "'", end = "'", close = false, newline = false },
+    { start = "`", end = "`", close = false, newline = false },
+]

crates/zed2/src/languages/markdown/highlights.scm 🔗

@@ -0,0 +1,24 @@
+(emphasis) @emphasis
+(strong_emphasis) @emphasis.strong
+
+[
+  (atx_heading)
+  (setext_heading)
+] @title
+
+[
+  (list_marker_plus)
+  (list_marker_minus)
+  (list_marker_star)
+  (list_marker_dot)
+  (list_marker_parenthesis)
+] @punctuation.list_marker
+
+(code_span) @text.literal
+
+(fenced_code_block
+  (info_string
+    (language) @text.literal))
+
+(link_destination) @link_uri
+(link_text) @link_text

crates/zed2/src/languages/nix/config.toml 🔗

@@ -0,0 +1,11 @@
+name = "Nix"
+path_suffixes = ["nix"]
+line_comment = "# "
+block_comment = ["/* ", " */"]
+autoclose_before = ";:.,=}])>` \n\t\""
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "<", end = ">", close = true, newline = true },
+]

crates/zed2/src/languages/nix/highlights.scm 🔗

@@ -0,0 +1,95 @@
+(comment) @comment
+
+[
+  "if"
+  "then"
+  "else"
+  "let"
+  "inherit"
+  "in"
+  "rec"
+  "with"
+  "assert"
+  "or"
+] @keyword
+
+[
+ (string_expression)
+ (indented_string_expression)
+] @string
+
+[
+  (path_expression)
+  (hpath_expression)
+  (spath_expression)
+] @string.special.path
+
+(uri_expression) @link_uri
+
+[
+  (integer_expression)
+  (float_expression)
+] @number
+
+(interpolation
+  "${" @punctuation.special
+  "}" @punctuation.special) @embedded
+
+(escape_sequence) @escape
+(dollar_escape) @escape
+
+(function_expression
+  universal: (identifier) @parameter
+)
+
+(formal
+  name: (identifier) @parameter
+  "?"? @punctuation.delimiter)
+
+(select_expression
+  attrpath: (attrpath (identifier)) @property)
+
+(apply_expression
+  function: [
+    (variable_expression (identifier)) @function
+    (select_expression
+      attrpath: (attrpath
+        attr: (identifier) @function .))])
+
+(unary_expression
+  operator: _ @operator)
+
+(binary_expression
+  operator: _ @operator)
+
+(variable_expression (identifier) @variable)
+
+(binding
+  attrpath: (attrpath (identifier)) @property)
+
+"=" @operator
+
+[
+  ";"
+  "."
+  ","
+] @punctuation.delimiter
+
+[
+  "("
+  ")"
+  "["
+  "]"
+  "{"
+  "}"
+] @punctuation.bracket
+
+(identifier) @variable
+
+((identifier) @function.builtin

crates/zed2/src/languages/nu/config.toml 🔗

@@ -0,0 +1,9 @@
+name = "Nu"
+path_suffixes = ["nu"]
+line_comment = "# "
+autoclose_before = ";:.,=}])>` \n\t\""
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+]

crates/zed2/src/languages/nu/highlights.scm 🔗

@@ -0,0 +1,302 @@
+;;; ---
+;;; keywords
+[
+    "def"
+    "def-env"
+    "alias"
+    "export-env"
+    "export"
+    "extern"
+    "module"
+
+    "let"
+    "let-env"
+    "mut"
+    "const"
+
+    "hide-env"
+
+    "source"
+    "source-env"
+
+    "overlay"
+    "register"
+
+    "loop"
+    "while"
+    "error"
+
+    "do"
+    "if"
+    "else"
+    "try"
+    "catch"
+    "match"
+
+    "break"
+    "continue"
+    "return"
+
+] @keyword
+
+(hide_mod "hide" @keyword)
+(decl_use "use" @keyword)
+
+(ctrl_for
+    "for" @keyword
+    "in" @keyword
+)
+(overlay_list "list" @keyword)
+(overlay_hide "hide" @keyword)
+(overlay_new "new" @keyword)
+(overlay_use
+    "use" @keyword
+    "as" @keyword
+)
+(ctrl_error "make" @keyword)
+
+;;; ---
+;;; literals
+(val_number) @constant
+(val_duration
+    unit: [
+        "ns" "µs" "us" "ms" "sec" "min" "hr" "day" "wk"
+    ] @variable
+)
+(val_filesize
+    unit: [
+        "b" "B"
+
+        "kb" "kB" "Kb" "KB"
+        "mb" "mB" "Mb" "MB"
+        "gb" "gB" "Gb" "GB"
+        "tb" "tB" "Tb" "TB"
+        "pb" "pB" "Pb" "PB"
+        "eb" "eB" "Eb" "EB"
+        "zb" "zB" "Zb" "ZB"
+
+        "kib" "kiB" "kIB" "kIb" "Kib" "KIb" "KIB"
+        "mib" "miB" "mIB" "mIb" "Mib" "MIb" "MIB"
+        "gib" "giB" "gIB" "gIb" "Gib" "GIb" "GIB"
+        "tib" "tiB" "tIB" "tIb" "Tib" "TIb" "TIB"
+        "pib" "piB" "pIB" "pIb" "Pib" "PIb" "PIB"
+        "eib" "eiB" "eIB" "eIb" "Eib" "EIb" "EIB"
+        "zib" "ziB" "zIB" "zIb" "Zib" "ZIb" "ZIB"
+    ] @variable
+)
+(val_binary
+    [
+       "0b"
+       "0o"
+       "0x"
+    ] @constant
+    "[" @punctuation.bracket
+    digit: [
+        "," @punctuation.delimiter
+        (hex_digit) @constant
+    ]
+    "]" @punctuation.bracket
+) @constant
+(val_bool) @constant.builtin
+(val_nothing) @constant.builtin
+(val_string) @string
+(val_date) @constant
+(inter_escape_sequence) @constant
+(escape_sequence) @constant
+(val_interpolated [
+    "$\""
+    "$\'"
+    "\""
+    "\'"
+] @string)
+(unescaped_interpolated_content) @string
+(escaped_interpolated_content) @string
+(expr_interpolated ["(" ")"] @variable)
+
+;;; ---
+;;; operators
+(expr_binary [
+    "+"
+    "-"
+    "*"
+    "/"
+    "mod"
+    "//"
+    "++"
+    "**"
+    "=="
+    "!="
+    "<"
+    "<="
+    ">"
+    ">="
+    "=~"
+    "!~"
+    "and"
+    "or"
+    "xor"
+    "bit-or"
+    "bit-xor"
+    "bit-and"
+    "bit-shl"
+    "bit-shr"
+    "in"
+    "not-in"
+    "starts-with"
+    "ends-with"
+] @operator)
+
+(expr_binary opr: ([
+    "and"
+    "or"
+    "xor"
+    "bit-or"
+    "bit-xor"
+    "bit-and"
+    "bit-shl"
+    "bit-shr"
+    "in"
+    "not-in"
+    "starts-with"
+    "ends-with"
+]) @keyword)
+
+(where_command [
+    "+"
+    "-"
+    "*"
+    "/"
+    "mod"
+    "//"
+    "++"
+    "**"
+    "=="
+    "!="
+    "<"
+    "<="
+    ">"
+    ">="
+    "=~"
+    "!~"
+    "and"
+    "or"
+    "xor"
+    "bit-or"
+    "bit-xor"
+    "bit-and"
+    "bit-shl"
+    "bit-shr"
+    "in"
+    "not-in"
+    "starts-with"
+    "ends-with"
+] @operator)
+
+(assignment [
+    "="
+    "+="
+    "-="
+    "*="
+    "/="
+    "++="
+] @operator)
+
+(expr_unary ["not" "-"] @operator)
+
+(val_range [
+    ".."
+    "..="
+    "..<"
+] @operator)
+
+["=>" "=" "|"] @operator
+
+[
+    "o>"   "out>"
+    "e>"   "err>"
+    "e+o>" "err+out>"
+    "o+e>" "out+err>"
+] @special
+
+;;; ---
+;;; punctuation
+[
+    ","
+    ";"
+] @punctuation.delimiter
+
+(param_short_flag "-" @punctuation.delimiter)
+(param_long_flag ["--"] @punctuation.delimiter)
+(long_flag ["--"] @punctuation.delimiter)
+(param_rest "..." @punctuation.delimiter)
+(param_type [":"] @punctuation.special)
+(param_value ["="] @punctuation.special)
+(param_cmd ["@"] @punctuation.special)
+(param_opt ["?"] @punctuation.special)
+
+[
+    "(" ")"
+    "{" "}"
+    "[" "]"
+] @punctuation.bracket
+
+(val_record
+  (record_entry ":" @punctuation.delimiter))
+;;; ---
+;;; identifiers
+(param_rest
+    name: (_) @variable)
+(param_opt
+    name: (_) @variable)
+(parameter
+    param_name: (_) @variable)
+(param_cmd
+    (cmd_identifier) @string)
+(param_long_flag) @variable
+(param_short_flag) @variable
+
+(short_flag) @variable
+(long_flag) @variable
+
+(scope_pattern [(wild_card) @function])
+
+(cmd_identifier) @function
+
+(command
+    "^" @punctuation.delimiter
+    head: (_) @function
+)
+
+"where" @function
+
+(path
+  ["." "?"] @punctuation.delimiter
+) @variable
+
+(val_variable
+  "$" @operator
+  [
+   (identifier) @variable
+   "in" @type.builtin
+   "nu" @type.builtin
+   "env" @type.builtin
+   "nothing" @type.builtin
+   ]  ; If we have a special styling, use it here
+)
+;;; ---
+;;; types
+(flat_type) @type.builtin
+(list_type
+    "list" @type
+    ["<" ">"] @punctuation.bracket
+)
+(collection_type
+    ["record" "table"] @type
+    "<" @punctuation.bracket
+    key: (_) @variable
+    ["," ":"] @punctuation.delimiter
+    ">" @punctuation.bracket
+)
+
+(shebang) @comment
+(comment) @comment

crates/zed2/src/languages/php.rs 🔗

@@ -0,0 +1,137 @@
+use anyhow::{anyhow, Result};
+
+use async_trait::async_trait;
+use collections::HashMap;
+
+use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+
+use smol::{fs, stream::StreamExt};
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
+
+fn intelephense_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct IntelephenseVersion(String);
+
+pub struct IntelephenseLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl IntelephenseLspAdapter {
+    const SERVER_PATH: &'static str = "node_modules/intelephense/lib/intelephense.js";
+
+    #[allow(unused)]
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        Self { node }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for IntelephenseLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("intelephense".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "php"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(IntelephenseVersion(
+            self.node.npm_package_latest_version("intelephense").await?,
+        )) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<IntelephenseVersion>().unwrap();
+        let server_path = container_dir.join(Self::SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(&container_dir, &[("intelephense", version.0.as_str())])
+                .await?;
+        }
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: intelephense_server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn label_for_completion(
+        &self,
+        _item: &lsp2::CompletionItem,
+        _language: &Arc<language2::Language>,
+    ) -> Option<language2::CodeLabel> {
+        None
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        None
+    }
+    async fn language_ids(&self) -> HashMap<String, String> {
+        HashMap::from_iter([("PHP".into(), "php".into())])
+    }
+}
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last_version_dir = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_dir() {
+                last_version_dir = Some(entry.path());
+            }
+        }
+        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+        let server_path = last_version_dir.join(IntelephenseLspAdapter::SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: intelephense_server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}

crates/zed2/src/languages/php/config.toml 🔗

@@ -0,0 +1,14 @@
+name = "PHP"
+path_suffixes = ["php"]
+first_line_pattern = '^#!.*php'
+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 = true, newline = false, not_in = ["string"] },
+]
+collapsed_placeholder = "/* ... */"
+word_characters = ["$"]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]

crates/zed2/src/languages/php/embedding.scm 🔗

@@ -0,0 +1,36 @@
+(
+    (comment)* @context
+    .
+    [
+        (function_definition
+            "function" @name
+            name: (_) @name
+            body: (_
+                "{" @keep
+                "}" @keep) @collapse
+            )
+
+        (trait_declaration
+            "trait" @name
+            name: (_) @name)
+
+        (method_declaration
+            "function" @name
+            name: (_) @name
+            body: (_
+                "{" @keep
+                "}" @keep) @collapse
+            )
+
+        (interface_declaration
+            "interface" @name
+            name: (_) @name
+            )
+
+        (enum_declaration
+            "enum" @name
+            name: (_) @name
+            )
+
+        ] @item
+    )

crates/zed2/src/languages/php/highlights.scm 🔗

@@ -0,0 +1,123 @@
+(php_tag) @tag
+"?>" @tag
+
+; Types
+
+(primitive_type) @type.builtin
+(cast_type) @type.builtin
+(named_type (name) @type) @type
+(named_type (qualified_name) @type) @type
+
+; Functions
+
+(array_creation_expression "array" @function.builtin)
+(list_literal "list" @function.builtin)
+
+(method_declaration
+  name: (name) @function.method)
+
+(function_call_expression
+  function: [(qualified_name (name)) (name)] @function)
+
+(scoped_call_expression
+  name: (name) @function)
+
+(member_call_expression
+  name: (name) @function.method)
+
+(function_definition
+  name: (name) @function)
+
+; Member
+
+(property_element
+  (variable_name) @property)
+
+(member_access_expression
+  name: (variable_name (name)) @property)
+(member_access_expression
+  name: (name) @property)
+
+; Variables
+
+(relative_scope) @variable.builtin
+
+((name) @constant
+ (#match? @constant "^_?[A-Z][A-Z\\d_]+$"))
+((name) @constant.builtin
+ (#match? @constant.builtin "^__[A-Z][A-Z\d_]+__$"))
+
+((name) @constructor
+ (#match? @constructor "^[A-Z]"))
+
+((name) @variable.builtin
+ (#eq? @variable.builtin "this"))
+
+(variable_name) @variable
+
+; Basic tokens
+[
+  (string)
+  (string_value)
+  (encapsed_string)
+  (heredoc)
+  (heredoc_body)
+  (nowdoc_body)
+] @string
+(boolean) @constant.builtin
+(null) @constant.builtin
+(integer) @number
+(float) @number
+(comment) @comment
+
+"$" @operator
+
+; Keywords
+
+"abstract" @keyword
+"as" @keyword
+"break" @keyword
+"case" @keyword
+"catch" @keyword
+"class" @keyword
+"const" @keyword
+"continue" @keyword
+"declare" @keyword
+"default" @keyword
+"do" @keyword
+"echo" @keyword
+"else" @keyword
+"elseif" @keyword
+"enum" @keyword
+"enddeclare" @keyword
+"endforeach" @keyword
+"endif" @keyword
+"endswitch" @keyword
+"endwhile" @keyword
+"extends" @keyword
+"final" @keyword
+"finally" @keyword
+"foreach" @keyword
+"function" @keyword
+"global" @keyword
+"if" @keyword
+"implements" @keyword
+"include_once" @keyword
+"include" @keyword
+"insteadof" @keyword
+"interface" @keyword
+"namespace" @keyword
+"new" @keyword
+"private" @keyword
+"protected" @keyword
+"public" @keyword
+"require_once" @keyword
+"require" @keyword
+"return" @keyword
+"static" @keyword
+"switch" @keyword
+"throw" @keyword
+"trait" @keyword
+"try" @keyword
+"use" @keyword
+"while" @keyword

crates/zed2/src/languages/php/outline.scm 🔗

@@ -0,0 +1,29 @@
+(class_declaration
+    "class" @context
+    name: (name) @name
+    ) @item
+
+(function_definition
+    "function" @context
+    name: (_) @name
+    ) @item
+
+(method_declaration
+    "function" @context
+    name: (_) @name
+    ) @item
+
+(interface_declaration
+    "interface" @context
+    name: (_) @name
+    ) @item
+
+(enum_declaration
+    "enum" @context
+    name: (_) @name
+    ) @item
+
+(trait_declaration
+    "trait" @context
+    name: (_) @name
+    ) @item

crates/zed2/src/languages/php/tags.scm 🔗

@@ -0,0 +1,40 @@
+(namespace_definition
+  name: (namespace_name) @name) @module
+
+(interface_declaration
+  name: (name) @name) @definition.interface
+
+(trait_declaration
+  name: (name) @name) @definition.interface
+
+(class_declaration
+  name: (name) @name) @definition.class
+
+(class_interface_clause [(name) (qualified_name)] @name) @impl
+
+(property_declaration
+  (property_element (variable_name (name) @name))) @definition.field
+
+(function_definition
+  name: (name) @name) @definition.function
+
+(method_declaration
+  name: (name) @name) @definition.function
+
+(object_creation_expression
+  [
+    (qualified_name (name) @name)
+    (variable_name (name) @name)
+  ]) @reference.class
+
+(function_call_expression
+  function: [
+    (qualified_name (name) @name)
+    (variable_name (name)) @name
+  ]) @reference.call
+
+(scoped_call_expression
+  name: (name) @name) @reference.call
+
+(member_call_expression
+  name: (name) @name) @reference.call

crates/zed2/src/languages/python.rs 🔗

@@ -0,0 +1,296 @@
+use anyhow::Result;
+use async_trait::async_trait;
+use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+use smol::fs;
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
+
+const SERVER_PATH: &'static str = "node_modules/pyright/langserver.index.js";
+
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct PythonLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl PythonLspAdapter {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        PythonLspAdapter { node }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for PythonLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("pyright".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "pyright"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(self.node.npm_package_latest_version("pyright").await?) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<String>().unwrap();
+        let server_path = container_dir.join(SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(&container_dir, &[("pyright", version.as_str())])
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn process_completion(&self, item: &mut lsp2::CompletionItem) {
+        // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.
+        // Where `XX` is the sorting category, `YYYY` is based on most recent usage,
+        // and `name` is the symbol name itself.
+        //
+        // Because the the symbol name is included, there generally are not ties when
+        // sorting by the `sortText`, so the symbol's fuzzy match score is not taken
+        // into account. Here, we remove the symbol name from the sortText in order
+        // to allow our own fuzzy score to be used to break ties.
+        //
+        // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873
+        let Some(sort_text) = &mut item.sort_text else {
+            return;
+        };
+        let mut parts = sort_text.split('.');
+        let Some(first) = parts.next() else { return };
+        let Some(second) = parts.next() else { return };
+        let Some(_) = parts.next() else { return };
+        sort_text.replace_range(first.len() + second.len() + 1.., "");
+    }
+
+    async fn label_for_completion(
+        &self,
+        item: &lsp2::CompletionItem,
+        language: &Arc<language2::Language>,
+    ) -> Option<language2::CodeLabel> {
+        let label = &item.label;
+        let grammar = language.grammar()?;
+        let highlight_id = match item.kind? {
+            lsp2::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
+            lsp2::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
+            lsp2::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
+            lsp2::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
+            _ => return None,
+        };
+        Some(language2::CodeLabel {
+            text: label.clone(),
+            runs: vec![(0..label.len(), highlight_id)],
+            filter_range: 0..label.len(),
+        })
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        kind: lsp2::SymbolKind,
+        language: &Arc<language2::Language>,
+    ) -> Option<language2::CodeLabel> {
+        let (text, filter_range, display_range) = match kind {
+            lsp2::SymbolKind::METHOD | lsp2::SymbolKind::FUNCTION => {
+                let text = format!("def {}():\n", name);
+                let filter_range = 4..4 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::CLASS => {
+                let text = format!("class {}:", name);
+                let filter_range = 6..6 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::CONSTANT => {
+                let text = format!("{} = 0", name);
+                let filter_range = 0..name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            _ => return None,
+        };
+
+        Some(language2::CodeLabel {
+            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
+            text: text[display_range].to_string(),
+            filter_range,
+        })
+    }
+}
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    let server_path = container_dir.join(SERVER_PATH);
+    if server_path.exists() {
+        Some(LanguageServerBinary {
+            path: node.binary_path().await.log_err()?,
+            arguments: server_binary_arguments(&server_path),
+        })
+    } else {
+        log::error!("missing executable in directory {:?}", server_path);
+        None
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use gpui2::{Context, ModelContext, TestAppContext};
+    use language2::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
+    use settings2::SettingsStore;
+    use std::num::NonZeroU32;
+
+    #[gpui2::test]
+    async fn test_python_autoindent(cx: &mut TestAppContext) {
+        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
+        let language =
+            crate::languages::language("python", tree_sitter_python::language(), None).await;
+        cx.update(|cx| {
+            let test_settings = SettingsStore::test(cx);
+            cx.set_global(test_settings);
+            language2::init(cx);
+            cx.update_global::<SettingsStore, _>(|store, cx| {
+                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
+                    s.defaults.tab_size = NonZeroU32::new(2);
+                });
+            });
+        });
+
+        cx.build_model(|cx| {
+            let mut buffer =
+                Buffer::new(0, cx.entity_id().as_u64(), "").with_language(language, cx);
+            let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext<Buffer>| {
+                let ix = buffer.len();
+                buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);
+            };
+
+            // indent after "def():"
+            append(&mut buffer, "def a():\n", cx);
+            assert_eq!(buffer.text(), "def a():\n  ");
+
+            // preserve indent after blank line
+            append(&mut buffer, "\n  ", cx);
+            assert_eq!(buffer.text(), "def a():\n  \n  ");
+
+            // indent after "if"
+            append(&mut buffer, "if a:\n  ", cx);
+            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    ");
+
+            // preserve indent after statement
+            append(&mut buffer, "b()\n", cx);
+            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n    ");
+
+            // preserve indent after statement
+            append(&mut buffer, "else", cx);
+            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n    else");
+
+            // dedent "else""
+            append(&mut buffer, ":", cx);
+            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n  else:");
+
+            // indent lines after else
+            append(&mut buffer, "\n", cx);
+            assert_eq!(
+                buffer.text(),
+                "def a():\n  \n  if a:\n    b()\n  else:\n    "
+            );
+
+            // indent after an open paren. the closing  paren is not indented
+            // because there is another token before it on the same line.
+            append(&mut buffer, "foo(\n1)", cx);
+            assert_eq!(
+                buffer.text(),
+                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n      1)"
+            );
+
+            // dedent the closing paren if it is shifted to the beginning of the line
+            let argument_ix = buffer.text().find('1').unwrap();
+            buffer.edit(
+                [(argument_ix..argument_ix + 1, "")],
+                Some(AutoindentMode::EachLine),
+                cx,
+            );
+            assert_eq!(
+                buffer.text(),
+                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )"
+            );
+
+            // preserve indent after the close paren
+            append(&mut buffer, "\n", cx);
+            assert_eq!(
+                buffer.text(),
+                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n    "
+            );
+
+            // manually outdent the last line
+            let end_whitespace_ix = buffer.len() - 4;
+            buffer.edit(
+                [(end_whitespace_ix..buffer.len(), "")],
+                Some(AutoindentMode::EachLine),
+                cx,
+            );
+            assert_eq!(
+                buffer.text(),
+                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n"
+            );
+
+            // preserve the newly reduced indentation on the next newline
+            append(&mut buffer, "\n", cx);
+            assert_eq!(
+                buffer.text(),
+                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n\n"
+            );
+
+            // reset to a simple if statement
+            buffer.edit([(0..buffer.len(), "if a:\n  b(\n  )")], None, cx);
+
+            // dedent "else" on the line after a closing paren
+            append(&mut buffer, "\n  else:\n", cx);
+            assert_eq!(buffer.text(), "if a:\n  b(\n  )\nelse:\n  ");
+
+            buffer
+        });
+    }
+}

crates/zed2/src/languages/python/config.toml 🔗

@@ -0,0 +1,16 @@
+name = "Python"
+path_suffixes = ["py", "pyi", "mpy"]
+first_line_pattern = '^#!.*\bpython[0-9.]*\b'
+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 = true, newline = false, not_in = ["string"] },
+    { start = "'", end = "'", close = false, newline = false, not_in = ["string"] },
+]
+
+auto_indent_using_last_non_empty_line = false
+increase_indent_pattern = ":\\s*$"
+decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:"

crates/zed2/src/languages/python/highlights.scm 🔗

@@ -0,0 +1,125 @@
+(attribute attribute: (identifier) @property)
+(type (identifier) @type)
+
+; Function calls
+
+(decorator) @function
+
+(call
+  function: (attribute attribute: (identifier) @function.method))
+(call
+  function: (identifier) @function)
+
+; Function definitions
+
+(function_definition
+  name: (identifier) @function)
+
+; Identifier naming conventions
+
+((identifier) @type
+ (#match? @type "^[A-Z]"))
+
+((identifier) @constant
+ (#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
+
+; Builtin functions
+
+((call
+  function: (identifier) @function.builtin)
+ (#match?
+   @function.builtin
+   "^(abs|all|any|ascii|bin|bool|breakpoint|bytearray|bytes|callable|chr|classmethod|compile|complex|delattr|dict|dir|divmod|enumerate|eval|exec|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|isinstance|issubclass|iter|len|list|locals|map|max|memoryview|min|next|object|oct|open|ord|pow|print|property|range|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|vars|zip|__import__)$"))
+
+; Literals
+
+[
+  (none)
+  (true)
+  (false)
+] @constant.builtin
+
+[
+  (integer)
+  (float)
+] @number
+
+(comment) @comment
+(string) @string
+(escape_sequence) @escape
+
+(interpolation
+  "{" @punctuation.special
+  "}" @punctuation.special) @embedded
+
+[
+  "-"
+  "-="
+  "!="
+  "*"
+  "**"
+  "**="
+  "*="
+  "/"
+  "//"
+  "//="
+  "/="
+  "&"
+  "%"
+  "%="
+  "^"
+  "+"
+  "->"
+  "+="
+  "<"
+  "<<"
+  "<="
+  "<>"
+  "="
+  ":="
+  "=="
+  ">"
+  ">="
+  ">>"
+  "|"
+  "~"
+  "and"
+  "in"
+  "is"
+  "not"
+  "or"
+] @operator
+
+[
+  "as"
+  "assert"
+  "async"
+  "await"
+  "break"
+  "class"
+  "continue"
+  "def"
+  "del"
+  "elif"
+  "else"
+  "except"
+  "exec"
+  "finally"
+  "for"
+  "from"
+  "global"
+  "if"
+  "import"
+  "lambda"
+  "nonlocal"
+  "pass"
+  "print"
+  "raise"
+  "return"
+  "try"
+  "while"
+  "with"
+  "yield"
+  "match"
+  "case"
+] @keyword

crates/zed2/src/languages/racket/config.toml 🔗

@@ -0,0 +1,9 @@
+name = "Racket"
+path_suffixes = ["rkt"]
+line_comment = "; "
+autoclose_before = "])"
+brackets = [
+    { start = "[", end = "]", close = true, newline = false },
+    { start = "(", end = ")", close = true, newline = false },
+    { start = "\"", end = "\"", close = true, newline = false },
+]

crates/zed2/src/languages/racket/highlights.scm 🔗

@@ -0,0 +1,40 @@
+["(" ")" "[" "]" "{" "}"] @punctuation.bracket
+
+[(string)
+ (here_string)
+ (byte_string)] @string
+(regex) @string.regex
+(escape_sequence) @escape
+
+[(comment)
+ (block_comment)
+ (sexp_comment)] @comment
+
+(symbol) @variable
+
+(number) @number
+(character) @constant.builtin
+(boolean) @constant.builtin
+(keyword) @constant
+(quote . (symbol)) @constant
+
+(extension) @keyword
+(lang_name) @variable.special
+
+((symbol) @operator
+ (#match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$"))
+
+(list
+  .
+  (symbol) @function)
+
+(list
+  .
+  (symbol) @keyword
+  (#match? @keyword

crates/zed2/src/languages/ruby.rs 🔗

@@ -0,0 +1,160 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use std::{any::Any, path::PathBuf, sync::Arc};
+
+pub struct RubyLanguageServer;
+
+#[async_trait]
+impl LspAdapter for RubyLanguageServer {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("solargraph".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "solargraph"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(()))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _version: Box<dyn 'static + Send + Any>,
+        _container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        Err(anyhow!("solargraph must be installed manually"))
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        Some(LanguageServerBinary {
+            path: "solargraph".into(),
+            arguments: vec!["stdio".into()],
+        })
+    }
+
+    fn can_be_reinstalled(&self) -> bool {
+        false
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        None
+    }
+
+    async fn label_for_completion(
+        &self,
+        item: &lsp2::CompletionItem,
+        language: &Arc<language2::Language>,
+    ) -> Option<language2::CodeLabel> {
+        let label = &item.label;
+        let grammar = language.grammar()?;
+        let highlight_id = match item.kind? {
+            lsp2::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
+            lsp2::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
+            lsp2::CompletionItemKind::CLASS | lsp2::CompletionItemKind::MODULE => {
+                grammar.highlight_id_for_name("type")?
+            }
+            lsp2::CompletionItemKind::KEYWORD => {
+                if label.starts_with(':') {
+                    grammar.highlight_id_for_name("string.special.symbol")?
+                } else {
+                    grammar.highlight_id_for_name("keyword")?
+                }
+            }
+            lsp2::CompletionItemKind::VARIABLE => {
+                if label.starts_with('@') {
+                    grammar.highlight_id_for_name("property")?
+                } else {
+                    return None;
+                }
+            }
+            _ => return None,
+        };
+        Some(language2::CodeLabel {
+            text: label.clone(),
+            runs: vec![(0..label.len(), highlight_id)],
+            filter_range: 0..label.len(),
+        })
+    }
+
+    async fn label_for_symbol(
+        &self,
+        label: &str,
+        kind: lsp2::SymbolKind,
+        language: &Arc<language2::Language>,
+    ) -> Option<language2::CodeLabel> {
+        let grammar = language.grammar()?;
+        match kind {
+            lsp2::SymbolKind::METHOD => {
+                let mut parts = label.split('#');
+                let classes = parts.next()?;
+                let method = parts.next()?;
+                if parts.next().is_some() {
+                    return None;
+                }
+
+                let class_id = grammar.highlight_id_for_name("type")?;
+                let method_id = grammar.highlight_id_for_name("function.method")?;
+
+                let mut ix = 0;
+                let mut runs = Vec::new();
+                for (i, class) in classes.split("::").enumerate() {
+                    if i > 0 {
+                        ix += 2;
+                    }
+                    let end_ix = ix + class.len();
+                    runs.push((ix..end_ix, class_id));
+                    ix = end_ix;
+                }
+
+                ix += 1;
+                let end_ix = ix + method.len();
+                runs.push((ix..end_ix, method_id));
+                Some(language2::CodeLabel {
+                    text: label.to_string(),
+                    runs,
+                    filter_range: 0..label.len(),
+                })
+            }
+            lsp2::SymbolKind::CONSTANT => {
+                let constant_id = grammar.highlight_id_for_name("constant")?;
+                Some(language2::CodeLabel {
+                    text: label.to_string(),
+                    runs: vec![(0..label.len(), constant_id)],
+                    filter_range: 0..label.len(),
+                })
+            }
+            lsp2::SymbolKind::CLASS | lsp2::SymbolKind::MODULE => {
+                let class_id = grammar.highlight_id_for_name("type")?;
+
+                let mut ix = 0;
+                let mut runs = Vec::new();
+                for (i, class) in label.split("::").enumerate() {
+                    if i > 0 {
+                        ix += "::".len();
+                    }
+                    let end_ix = ix + class.len();
+                    runs.push((ix..end_ix, class_id));
+                    ix = end_ix;
+                }
+
+                Some(language2::CodeLabel {
+                    text: label.to_string(),
+                    runs,
+                    filter_range: 0..label.len(),
+                })
+            }
+            _ => return None,
+        }
+    }
+}

crates/zed2/src/languages/ruby/brackets.scm 🔗

@@ -0,0 +1,14 @@
+("[" @open "]" @close)
+("{" @open "}" @close)
+("\"" @open "\"" @close)
+("do" @open "end" @close)
+
+(block_parameters "|" @open "|" @close)
+(interpolation "#{" @open "}" @close)
+
+(if "if" @open "end" @close)
+(unless "unless" @open "end" @close)
+(begin "begin" @open "end" @close)
+(module "module" @open "end" @close)
+(_ . "def" @open "end" @close)
+(_ . "class" @open "end" @close)

crates/zed2/src/languages/ruby/config.toml 🔗

@@ -0,0 +1,13 @@
+name = "Ruby"
+path_suffixes = ["rb", "Gemfile"]
+first_line_pattern = '^#!.*\bruby\b'
+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 = true, newline = false, not_in = ["comment", "string"] },
+  { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] },
+]
+collapsed_placeholder = "# ..."

crates/zed2/src/languages/ruby/embedding.scm 🔗

@@ -0,0 +1,22 @@
+(
+    (comment)* @context
+    .
+    [
+        (module
+            "module" @name
+            name: (_) @name)
+        (method
+            "def" @name
+            name: (_) @name
+            body: (body_statement) @collapse)
+        (class
+            "class" @name
+            name: (_) @name)
+        (singleton_method
+            "def" @name
+            object: (_) @name
+            "." @name
+            name: (_) @name
+            body: (body_statement) @collapse)
+        ] @item
+    )

crates/zed2/src/languages/ruby/highlights.scm 🔗

@@ -0,0 +1,181 @@
+; Keywords
+
+[
+  "alias"
+  "and"
+  "begin"
+  "break"
+  "case"
+  "class"
+  "def"
+  "do"
+  "else"
+  "elsif"
+  "end"
+  "ensure"
+  "for"
+  "if"
+  "in"
+  "module"
+  "next"
+  "or"
+  "rescue"
+  "retry"
+  "return"
+  "then"
+  "unless"
+  "until"
+  "when"
+  "while"
+  "yield"
+] @keyword
+
+(identifier) @variable
+
+((identifier) @keyword
+ (#match? @keyword "^(private|protected|public)$"))
+
+; Function calls
+
+((identifier) @function.method.builtin
+ (#eq? @function.method.builtin "require"))
+
+"defined?" @function.method.builtin
+
+(call
+  method: [(identifier) (constant)] @function.method)
+
+; Function definitions
+
+(alias (identifier) @function.method)
+(setter (identifier) @function.method)
+(method name: [(identifier) (constant)] @function.method)
+(singleton_method name: [(identifier) (constant)] @function.method)
+
+; Identifiers
+
+[
+  (class_variable)
+  (instance_variable)
+] @property
+
+((identifier) @constant.builtin
+ (#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$"))
+
+(file) @constant.builtin
+(line) @constant.builtin
+(encoding) @constant.builtin
+
+(hash_splat_nil
+  "**" @operator
+) @constant.builtin
+
+((constant) @constant
+ (#match? @constant "^[A-Z\\d_]+$"))
+
+(constant) @type
+
+(self) @variable.special
+(super) @variable.special
+
+; Literals
+
+[
+  (string)
+  (bare_string)
+  (subshell)
+  (heredoc_body)
+  (heredoc_beginning)
+] @string
+
+[
+  (simple_symbol)
+  (delimited_symbol)
+  (hash_key_symbol)
+  (bare_symbol)
+] @string.special.symbol
+
+(regex) @string.regex
+(escape_sequence) @escape
+
+[
+  (integer)
+  (float)
+] @number
+
+[
+  (nil)
+  (true)
+  (false)
+] @constant.builtin
+
+(comment) @comment
+
+; Operators
+
+[
+  "!"
+  "~"
+  "+"
+  "-"
+  "**"
+  "*"
+  "/"
+  "%"
+  "<<"
+  ">>"
+  "&"
+  "|"
+  "^"
+  ">"
+  "<"
+  "<="
+  ">="
+  "=="
+  "!="
+  "=~"
+  "!~"
+  "<=>"
+  "||"
+  "&&"
+  ".."
+  "..."
+  "="
+  "**="
+  "*="
+  "/="
+  "%="
+  "+="
+  "-="
+  "<<="
+  ">>="
+  "&&="
+  "&="
+  "||="
+  "|="
+  "^="
+  "=>"
+  "->"
+  (operator)
+] @operator
+
+[
+  ","
+  ";"
+  "."
+] @punctuation.delimiter
+
+[
+  "("
+  ")"
+  "["
+  "]"
+  "{"
+  "}"
+  "%w("
+  "%i("
+] @punctuation.bracket
+
+(interpolation
+  "#{" @punctuation.special
+  "}" @punctuation.special) @embedded

crates/zed2/src/languages/ruby/indents.scm 🔗

@@ -0,0 +1,17 @@
+(method "end" @end) @indent
+(class "end" @end) @indent
+(module "end" @end) @indent
+(begin "end" @end) @indent
+(do_block "end" @end) @indent
+
+(then) @indent
+(call) @indent
+
+(ensure) @outdent
+(rescue) @outdent
+(else) @outdent
+
+
+(_ "[" "]" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent

crates/zed2/src/languages/ruby/outline.scm 🔗

@@ -0,0 +1,17 @@
+(class
+    "class" @context
+    name: (_) @name) @item
+
+(method
+    "def" @context
+    name: (_) @name) @item
+
+(singleton_method
+    "def" @context
+    object: (_) @context
+    "." @context
+    name: (_) @name) @item
+
+(module
+    "module" @context
+    name: (_) @name) @item

crates/zed2/src/languages/rust.rs 🔗

@@ -0,0 +1,568 @@
+use anyhow::{anyhow, Result};
+use async_compression::futures::bufread::GzipDecoder;
+use async_trait::async_trait;
+use futures::{io::BufReader, StreamExt};
+pub use language2::*;
+use lazy_static::lazy_static;
+use lsp2::LanguageServerBinary;
+use regex::Regex;
+use smol::fs::{self, File};
+use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc};
+use util::{
+    fs::remove_matching,
+    github::{latest_github_release, GitHubLspBinaryVersion},
+    ResultExt,
+};
+
+pub struct RustLspAdapter;
+
+#[async_trait]
+impl LspAdapter for RustLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("rust-analyzer".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "rust"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let release =
+            latest_github_release("rust-analyzer/rust-analyzer", false, delegate.http_client())
+                .await?;
+        let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH);
+        let asset = release
+            .assets
+            .iter()
+            .find(|asset| asset.name == asset_name)
+            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+        Ok(Box::new(GitHubLspBinaryVersion {
+            name: release.name,
+            url: asset.browser_download_url.clone(),
+        }))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+        let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
+
+        if fs::metadata(&destination_path).await.is_err() {
+            let mut response = delegate
+                .http_client()
+                .get(&version.url, Default::default(), true)
+                .await
+                .map_err(|err| anyhow!("error downloading release: {}", err))?;
+            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
+            let mut file = File::create(&destination_path).await?;
+            futures::io::copy(decompressed_bytes, &mut file).await?;
+            fs::set_permissions(
+                &destination_path,
+                <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
+            )
+            .await?;
+
+            remove_matching(&container_dir, |entry| entry != destination_path).await;
+        }
+
+        Ok(LanguageServerBinary {
+            path: destination_path,
+            arguments: Default::default(),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--help".into()];
+                binary
+            })
+    }
+
+    async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
+        vec!["rustc".into()]
+    }
+
+    async fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
+        Some("rust-analyzer/flycheck".into())
+    }
+
+    fn process_diagnostics(&self, params: &mut lsp2::PublishDiagnosticsParams) {
+        lazy_static! {
+            static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap();
+        }
+
+        for diagnostic in &mut params.diagnostics {
+            for message in diagnostic
+                .related_information
+                .iter_mut()
+                .flatten()
+                .map(|info| &mut info.message)
+                .chain([&mut diagnostic.message])
+            {
+                if let Cow::Owned(sanitized) = REGEX.replace_all(message, "`$1`") {
+                    *message = sanitized;
+                }
+            }
+        }
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp2::CompletionItem,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        match completion.kind {
+            Some(lsp2::CompletionItemKind::FIELD) if completion.detail.is_some() => {
+                let detail = completion.detail.as_ref().unwrap();
+                let name = &completion.label;
+                let text = format!("{}: {}", name, detail);
+                let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
+                let runs = language.highlight_text(&source, 11..11 + text.len());
+                return Some(CodeLabel {
+                    text,
+                    runs,
+                    filter_range: 0..name.len(),
+                });
+            }
+            Some(lsp2::CompletionItemKind::CONSTANT | lsp2::CompletionItemKind::VARIABLE)
+                if completion.detail.is_some()
+                    && completion.insert_text_format != Some(lsp2::InsertTextFormat::SNIPPET) =>
+            {
+                let detail = completion.detail.as_ref().unwrap();
+                let name = &completion.label;
+                let text = format!("{}: {}", name, detail);
+                let source = Rope::from(format!("let {} = ();", text).as_str());
+                let runs = language.highlight_text(&source, 4..4 + text.len());
+                return Some(CodeLabel {
+                    text,
+                    runs,
+                    filter_range: 0..name.len(),
+                });
+            }
+            Some(lsp2::CompletionItemKind::FUNCTION | lsp2::CompletionItemKind::METHOD)
+                if completion.detail.is_some() =>
+            {
+                lazy_static! {
+                    static ref REGEX: Regex = Regex::new("\\(…?\\)").unwrap();
+                }
+                let detail = completion.detail.as_ref().unwrap();
+                const FUNCTION_PREFIXES: [&'static str; 2] = ["async fn", "fn"];
+                let prefix = FUNCTION_PREFIXES
+                    .iter()
+                    .find_map(|prefix| detail.strip_prefix(*prefix).map(|suffix| (prefix, suffix)));
+                // fn keyword should be followed by opening parenthesis.
+                if let Some((prefix, suffix)) = prefix {
+                    if suffix.starts_with('(') {
+                        let text = REGEX.replace(&completion.label, suffix).to_string();
+                        let source = Rope::from(format!("{prefix} {} {{}}", text).as_str());
+                        let run_start = prefix.len() + 1;
+                        let runs =
+                            language.highlight_text(&source, run_start..run_start + text.len());
+                        return Some(CodeLabel {
+                            filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
+                            text,
+                            runs,
+                        });
+                    }
+                }
+            }
+            Some(kind) => {
+                let highlight_name = match kind {
+                    lsp2::CompletionItemKind::STRUCT
+                    | lsp2::CompletionItemKind::INTERFACE
+                    | lsp2::CompletionItemKind::ENUM => Some("type"),
+                    lsp2::CompletionItemKind::ENUM_MEMBER => Some("variant"),
+                    lsp2::CompletionItemKind::KEYWORD => Some("keyword"),
+                    lsp2::CompletionItemKind::VALUE | lsp2::CompletionItemKind::CONSTANT => {
+                        Some("constant")
+                    }
+                    _ => None,
+                };
+                let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name?)?;
+                let mut label = CodeLabel::plain(completion.label.clone(), None);
+                label.runs.push((
+                    0..label.text.rfind('(').unwrap_or(label.text.len()),
+                    highlight_id,
+                ));
+                return Some(label);
+            }
+            _ => {}
+        }
+        None
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        kind: lsp2::SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        let (text, filter_range, display_range) = match kind {
+            lsp2::SymbolKind::METHOD | lsp2::SymbolKind::FUNCTION => {
+                let text = format!("fn {} () {{}}", name);
+                let filter_range = 3..3 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::STRUCT => {
+                let text = format!("struct {} {{}}", name);
+                let filter_range = 7..7 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::ENUM => {
+                let text = format!("enum {} {{}}", name);
+                let filter_range = 5..5 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::INTERFACE => {
+                let text = format!("trait {} {{}}", name);
+                let filter_range = 6..6 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::CONSTANT => {
+                let text = format!("const {}: () = ();", name);
+                let filter_range = 6..6 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::MODULE => {
+                let text = format!("mod {} {{}}", name);
+                let filter_range = 4..4 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::TYPE_PARAMETER => {
+                let text = format!("type {} {{}}", name);
+                let filter_range = 5..5 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            _ => return None,
+        };
+
+        Some(CodeLabel {
+            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
+            text: text[display_range].to_string(),
+            filter_range,
+        })
+    }
+}
+
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            last = Some(entry?.path());
+        }
+
+        anyhow::Ok(LanguageServerBinary {
+            path: last.ok_or_else(|| anyhow!("no cached binary"))?,
+            arguments: Default::default(),
+        })
+    })()
+    .await
+    .log_err()
+}
+
+#[cfg(test)]
+mod tests {
+    use std::num::NonZeroU32;
+
+    use super::*;
+    use crate::languages::language;
+    use gpui2::{Context, Hsla, TestAppContext};
+    use language2::language_settings::AllLanguageSettings;
+    use settings2::SettingsStore;
+    use theme2::SyntaxTheme;
+
+    #[gpui2::test]
+    async fn test_process_rust_diagnostics() {
+        let mut params = lsp2::PublishDiagnosticsParams {
+            uri: lsp2::Url::from_file_path("/a").unwrap(),
+            version: None,
+            diagnostics: vec![
+                // no newlines
+                lsp2::Diagnostic {
+                    message: "use of moved value `a`".to_string(),
+                    ..Default::default()
+                },
+                // newline at the end of a code span
+                lsp2::Diagnostic {
+                    message: "consider importing this struct: `use b::c;\n`".to_string(),
+                    ..Default::default()
+                },
+                // code span starting right after a newline
+                lsp2::Diagnostic {
+                    message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
+                        .to_string(),
+                    ..Default::default()
+                },
+            ],
+        };
+        RustLspAdapter.process_diagnostics(&mut params);
+
+        assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
+
+        // remove trailing newline from code span
+        assert_eq!(
+            params.diagnostics[1].message,
+            "consider importing this struct: `use b::c;`"
+        );
+
+        // do not remove newline before the start of code span
+        assert_eq!(
+            params.diagnostics[2].message,
+            "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
+        );
+    }
+
+    #[gpui2::test]
+    async fn test_rust_label_for_completion() {
+        let language = language(
+            "rust",
+            tree_sitter_rust::language(),
+            Some(Arc::new(RustLspAdapter)),
+        )
+        .await;
+        let grammar = language.grammar().unwrap();
+        let theme = SyntaxTheme::new_test([
+            ("type", Hsla::default()),
+            ("keyword", Hsla::default()),
+            ("function", Hsla::default()),
+            ("property", Hsla::default()),
+        ]);
+
+        language.set_theme(&theme);
+
+        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
+        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
+        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
+        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
+
+        assert_eq!(
+            language
+                .label_for_completion(&lsp2::CompletionItem {
+                    kind: Some(lsp2::CompletionItemKind::FUNCTION),
+                    label: "hello(…)".to_string(),
+                    detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
+                    ..Default::default()
+                })
+                .await,
+            Some(CodeLabel {
+                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
+                filter_range: 0..5,
+                runs: vec![
+                    (0..5, highlight_function),
+                    (7..10, highlight_keyword),
+                    (11..17, highlight_type),
+                    (18..19, highlight_type),
+                    (25..28, highlight_type),
+                    (29..30, highlight_type),
+                ],
+            })
+        );
+        assert_eq!(
+            language
+                .label_for_completion(&lsp2::CompletionItem {
+                    kind: Some(lsp2::CompletionItemKind::FUNCTION),
+                    label: "hello(…)".to_string(),
+                    detail: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
+                    ..Default::default()
+                })
+                .await,
+            Some(CodeLabel {
+                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
+                filter_range: 0..5,
+                runs: vec![
+                    (0..5, highlight_function),
+                    (7..10, highlight_keyword),
+                    (11..17, highlight_type),
+                    (18..19, highlight_type),
+                    (25..28, highlight_type),
+                    (29..30, highlight_type),
+                ],
+            })
+        );
+        assert_eq!(
+            language
+                .label_for_completion(&lsp2::CompletionItem {
+                    kind: Some(lsp2::CompletionItemKind::FIELD),
+                    label: "len".to_string(),
+                    detail: Some("usize".to_string()),
+                    ..Default::default()
+                })
+                .await,
+            Some(CodeLabel {
+                text: "len: usize".to_string(),
+                filter_range: 0..3,
+                runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
+            })
+        );
+
+        assert_eq!(
+            language
+                .label_for_completion(&lsp2::CompletionItem {
+                    kind: Some(lsp2::CompletionItemKind::FUNCTION),
+                    label: "hello(…)".to_string(),
+                    detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
+                    ..Default::default()
+                })
+                .await,
+            Some(CodeLabel {
+                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
+                filter_range: 0..5,
+                runs: vec![
+                    (0..5, highlight_function),
+                    (7..10, highlight_keyword),
+                    (11..17, highlight_type),
+                    (18..19, highlight_type),
+                    (25..28, highlight_type),
+                    (29..30, highlight_type),
+                ],
+            })
+        );
+    }
+
+    #[gpui2::test]
+    async fn test_rust_label_for_symbol() {
+        let language = language(
+            "rust",
+            tree_sitter_rust::language(),
+            Some(Arc::new(RustLspAdapter)),
+        )
+        .await;
+        let grammar = language.grammar().unwrap();
+        let theme = SyntaxTheme::new_test([
+            ("type", Hsla::default()),
+            ("keyword", Hsla::default()),
+            ("function", Hsla::default()),
+            ("property", Hsla::default()),
+        ]);
+
+        language.set_theme(&theme);
+
+        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
+        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
+        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
+
+        assert_eq!(
+            language
+                .label_for_symbol("hello", lsp2::SymbolKind::FUNCTION)
+                .await,
+            Some(CodeLabel {
+                text: "fn hello".to_string(),
+                filter_range: 3..8,
+                runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
+            })
+        );
+
+        assert_eq!(
+            language
+                .label_for_symbol("World", lsp2::SymbolKind::TYPE_PARAMETER)
+                .await,
+            Some(CodeLabel {
+                text: "type World".to_string(),
+                filter_range: 5..10,
+                runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
+            })
+        );
+    }
+
+    #[gpui2::test]
+    async fn test_rust_autoindent(cx: &mut TestAppContext) {
+        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
+        cx.update(|cx| {
+            let test_settings = SettingsStore::test(cx);
+            cx.set_global(test_settings);
+            language2::init(cx);
+            cx.update_global::<SettingsStore, _>(|store, cx| {
+                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
+                    s.defaults.tab_size = NonZeroU32::new(2);
+                });
+            });
+        });
+
+        let language = crate::languages::language("rust", tree_sitter_rust::language(), None).await;
+
+        cx.build_model(|cx| {
+            let mut buffer =
+                Buffer::new(0, cx.entity_id().as_u64(), "").with_language(language, cx);
+
+            // indent between braces
+            buffer.set_text("fn a() {}", cx);
+            let ix = buffer.len() - 1;
+            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(buffer.text(), "fn a() {\n  \n}");
+
+            // indent between braces, even after empty lines
+            buffer.set_text("fn a() {\n\n\n}", cx);
+            let ix = buffer.len() - 2;
+            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(buffer.text(), "fn a() {\n\n\n  \n}");
+
+            // indent a line that continues a field expression
+            buffer.set_text("fn a() {\n  \n}", cx);
+            let ix = buffer.len() - 2;
+            buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n}");
+
+            // indent further lines that continue the field expression, even after empty lines
+            let ix = buffer.len() - 2;
+            buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n    \n    .d\n}");
+
+            // dedent the line after the field expression
+            let ix = buffer.len() - 2;
+            buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(
+                buffer.text(),
+                "fn a() {\n  b\n    .c\n    \n    .d;\n  e\n}"
+            );
+
+            // indent inside a struct within a call
+            buffer.set_text("const a: B = c(D {});", cx);
+            let ix = buffer.len() - 3;
+            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(buffer.text(), "const a: B = c(D {\n  \n});");
+
+            // indent further inside a nested call
+            let ix = buffer.len() - 4;
+            buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(buffer.text(), "const a: B = c(D {\n  e: f(\n    \n  )\n});");
+
+            // keep that indent after an empty line
+            let ix = buffer.len() - 8;
+            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(
+                buffer.text(),
+                "const a: B = c(D {\n  e: f(\n    \n    \n  )\n});"
+            );
+
+            buffer
+        });
+    }
+}

crates/zed2/src/languages/rust/config.toml 🔗

@@ -0,0 +1,13 @@
+name = "Rust"
+path_suffixes = ["rs"]
+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, not_in = ["string", "comment"] },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+    { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
+]
+collapsed_placeholder = " /* ... */ "

crates/zed2/src/languages/rust/embedding.scm 🔗

@@ -0,0 +1,32 @@
+(
+    [(line_comment) (attribute_item)]* @context
+    .
+    [
+
+        (struct_item
+            name: (_) @name)
+
+        (enum_item
+            name: (_) @name)
+
+        (impl_item
+            trait: (_)? @name
+            "for"? @name
+            type: (_) @name)
+
+        (trait_item
+            name: (_) @name)
+
+        (function_item
+            name: (_) @name
+            body: (block
+                "{" @keep
+                "}" @keep) @collapse)
+
+        (macro_definition
+            name: (_) @name)
+        ] @item
+    )
+
+(attribute_item) @collapse
+(use_declaration) @collapse

crates/zed2/src/languages/rust/highlights.scm 🔗

@@ -0,0 +1,116 @@
+(type_identifier) @type
+(primitive_type) @type.builtin
+(self) @variable.special
+(field_identifier) @property
+
+(call_expression
+  function: [
+    (identifier) @function
+    (scoped_identifier
+      name: (identifier) @function)
+    (field_expression
+      field: (field_identifier) @function.method)
+  ])
+
+(generic_function
+  function: [
+    (identifier) @function
+    (scoped_identifier
+      name: (identifier) @function)
+    (field_expression
+      field: (field_identifier) @function.method)
+  ])
+
+(function_item name: (identifier) @function.definition)
+(function_signature_item name: (identifier) @function.definition)
+
+(macro_invocation
+  macro: [
+    (identifier) @function.special
+    (scoped_identifier
+      name: (identifier) @function.special)
+  ])
+
+(macro_definition
+  name: (identifier) @function.special.definition)
+
+; Identifier conventions
+
+; Assume uppercase names are types/enum-constructors
+((identifier) @type
+ (#match? @type "^[A-Z]"))
+
+; Assume all-caps names are constants
+((identifier) @constant
+ (#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
+
+[
+  "("
+  ")"
+  "{"
+  "}"
+  "["
+  "]"
+] @punctuation.bracket
+
+(_
+  .
+  "<" @punctuation.bracket
+  ">" @punctuation.bracket)
+
+[
+  "as"
+  "async"
+  "await"
+  "break"
+  "const"
+  "continue"
+  "default"
+  "dyn"
+  "else"
+  "enum"
+  "extern"
+  "for"
+  "fn"
+  "if"
+  "in"
+  "impl"
+  "let"
+  "loop"
+  "macro_rules!"
+  "match"
+  "mod"
+  "move"
+  "pub"
+  "ref"
+  "return"
+  "static"
+  "struct"
+  "trait"
+  "type"
+  "use"
+  "where"
+  "while"
+  "union"
+  "unsafe"
+  (mutable_specifier)
+  (super)
+] @keyword
+
+[
+  (string_literal)
+  (raw_string_literal)
+  (char_literal)
+] @string
+
+[
+  (integer_literal)
+  (float_literal)
+] @number
+
+(boolean_literal) @constant
+
+[
+  (line_comment)
+  (block_comment)
+] @comment

crates/zed2/src/languages/rust/indents.scm 🔗

@@ -0,0 +1,14 @@
+[
+    ((where_clause) _ @end)
+    (field_expression)
+    (call_expression)
+    (assignment_expression)
+    (let_declaration)
+    (let_chain)
+    (await_expression)
+] @indent
+
+(_ "[" "]" @end) @indent
+(_ "<" ">" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent

crates/zed2/src/languages/rust/outline.scm 🔗

@@ -0,0 +1,63 @@
+(struct_item
+    (visibility_modifier)? @context
+    "struct" @context
+    name: (_) @name) @item
+
+(enum_item
+    (visibility_modifier)? @context
+    "enum" @context
+    name: (_) @name) @item
+
+(enum_variant
+    (visibility_modifier)? @context
+    name: (_) @name) @item
+
+(impl_item
+    "impl" @context
+    trait: (_)? @name
+    "for"? @context
+    type: (_) @name) @item
+
+(trait_item
+    (visibility_modifier)? @context
+    "trait" @context
+    name: (_) @name) @item
+
+(function_item
+    (visibility_modifier)? @context
+    (function_modifiers)? @context
+    "fn" @context
+    name: (_) @name) @item
+
+(function_signature_item
+    (visibility_modifier)? @context
+    (function_modifiers)? @context
+    "fn" @context
+    name: (_) @name) @item
+
+(macro_definition
+    . "macro_rules!" @context
+    name: (_) @name) @item
+
+(mod_item
+    (visibility_modifier)? @context
+    "mod" @context
+    name: (_) @name) @item
+
+(type_item
+    (visibility_modifier)? @context
+    "type" @context
+    name: (_) @name) @item
+
+(associated_type
+    "type" @context
+    name: (_) @name) @item
+
+(const_item
+    (visibility_modifier)? @context
+    "const" @context
+    name: (_) @name) @item
+
+(field_declaration
+    (visibility_modifier)? @context
+    name: (_) @name) @item

crates/zed2/src/languages/scheme/config.toml 🔗

@@ -0,0 +1,9 @@
+name = "Scheme"
+path_suffixes = ["scm", "ss"]
+line_comment = "; "
+autoclose_before = "])"
+brackets = [
+    { start = "[", end = "]", close = true, newline = false },
+    { start = "(", end = ")", close = true, newline = false },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] },
+]

crates/zed2/src/languages/scheme/highlights.scm 🔗

@@ -0,0 +1,28 @@
+["(" ")" "[" "]" "{" "}"] @punctuation.bracket
+
+(number) @number
+(character) @constant.builtin
+(boolean) @constant.builtin
+
+(symbol) @variable
+(string) @string
+
+(escape_sequence) @escape
+
+[(comment)
+ (block_comment)
+ (directive)] @comment
+
+((symbol) @operator
+ (#match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$"))
+
+(list
+  .
+  (symbol) @function)
+
+(list
+  .
+  (symbol) @keyword
+  (#match? @keyword
+   "^(define-syntax|let\\*|lambda|λ|case|=>|quote-splicing|unquote-splicing|set!|let|letrec|letrec-syntax|let-values|let\\*-values|do|else|define|cond|syntax-rules|unquote|begin|quote|let-syntax|and|if|quasiquote|letrec|delay|or|when|unless|identifier-syntax|assert|library|export|import|rename|only|except|prefix)$"
+   ))

crates/zed2/src/languages/svelte.rs 🔗

@@ -0,0 +1,133 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use futures::StreamExt;
+use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+use serde_json::json;
+use smol::fs;
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
+
+const SERVER_PATH: &'static str = "node_modules/svelte-language-server/bin/server.js";
+
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct SvelteLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl SvelteLspAdapter {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        SvelteLspAdapter { node }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for SvelteLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("svelte-language-server".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "svelte"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(
+            self.node
+                .npm_package_latest_version("svelte-language-server")
+                .await?,
+        ) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<String>().unwrap();
+        let server_path = container_dir.join(SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[("svelte-language-server", version.as_str())],
+                )
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        Some(json!({
+            "provideFormatter": true
+        }))
+    }
+
+    fn prettier_plugins(&self) -> &[&'static str] {
+        &["prettier-plugin-svelte"]
+    }
+}
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last_version_dir = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_dir() {
+                last_version_dir = Some(entry.path());
+            }
+        }
+        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+        let server_path = last_version_dir.join(SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}

crates/zed2/src/languages/svelte/config.toml 🔗

@@ -0,0 +1,20 @@
+name = "Svelte"
+path_suffixes = ["svelte"]
+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, not_in = ["string", "comment"] },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
+    { start = "`", end = "`", close = true, newline = false, not_in = ["string"] },
+    { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
+]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]
+prettier_parser_name = "svelte"
+
+[overrides.string]
+word_characters = ["-"]
+opt_into_language_servers = ["tailwindcss-language-server"]

crates/zed2/src/languages/svelte/highlights.scm 🔗

@@ -0,0 +1,42 @@
+; Special identifiers
+;--------------------
+
+; TODO:
+(tag_name) @tag
+(attribute_name) @property
+(erroneous_end_tag_name) @keyword
+(comment) @comment
+
+[
+  (attribute_value)
+  (quoted_attribute_value)
+] @string
+
+[
+  (text)
+  (raw_text_expr)
+] @none
+
+[
+  (special_block_keyword)
+  (then)
+  (as)
+] @keyword
+
+[
+  "{"
+  "}"
+] @punctuation.bracket
+
+"=" @operator
+
+[
+  "<"
+  ">"
+  "</"
+  "/>"
+  "#"
+  ":"
+  "/"
+  "@"
+] @tag.delimiter

crates/zed2/src/languages/svelte/injections.scm 🔗

@@ -0,0 +1,28 @@
+; injections.scm
+; --------------
+(script_element
+  (raw_text) @content
+  (#set! "language" "javascript"))
+
+ ((script_element
+     (start_tag
+       (attribute
+         (quoted_attribute_value (attribute_value) @_language)))
+      (raw_text) @content)
+    (#eq? @_language "ts")
+    (#set! "language" "typescript"))
+
+((script_element
+    (start_tag
+        (attribute
+        (quoted_attribute_value (attribute_value) @_language)))
+    (raw_text) @content)
+  (#eq? @_language "typescript")
+  (#set! "language" "typescript"))
+
+(style_element
+  (raw_text) @content
+  (#set! "language" "css"))
+
+((raw_text_expr) @content
+  (#set! "language" "javascript"))

crates/zed2/src/languages/tailwind.rs 🔗

@@ -0,0 +1,167 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use collections::HashMap;
+use futures::{
+    future::{self, BoxFuture},
+    FutureExt, StreamExt,
+};
+use gpui2::AppContext;
+use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+use serde_json::{json, Value};
+use smol::fs;
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
+
+const SERVER_PATH: &'static str = "node_modules/.bin/tailwindcss-language-server";
+
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct TailwindLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl TailwindLspAdapter {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        TailwindLspAdapter { node }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for TailwindLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("tailwindcss-language-server".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "tailwind"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(
+            self.node
+                .npm_package_latest_version("@tailwindcss/language-server")
+                .await?,
+        ) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<String>().unwrap();
+        let server_path = container_dir.join(SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[("@tailwindcss/language-server", version.as_str())],
+                )
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        Some(json!({
+            "provideFormatter": true,
+            "userLanguages": {
+                "html": "html",
+                "css": "css",
+                "javascript": "javascript",
+                "typescriptreact": "typescriptreact",
+            },
+        }))
+    }
+
+    fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+        future::ready(json!({
+            "tailwindCSS": {
+                "emmetCompletions": true,
+            }
+        }))
+        .boxed()
+    }
+
+    async fn language_ids(&self) -> HashMap<String, String> {
+        HashMap::from_iter([
+            ("HTML".to_string(), "html".to_string()),
+            ("CSS".to_string(), "css".to_string()),
+            ("JavaScript".to_string(), "javascript".to_string()),
+            ("TSX".to_string(), "typescriptreact".to_string()),
+            ("Svelte".to_string(), "svelte".to_string()),
+            ("Elixir".to_string(), "phoenix-heex".to_string()),
+            ("HEEX".to_string(), "phoenix-heex".to_string()),
+            ("ERB".to_string(), "erb".to_string()),
+            ("PHP".to_string(), "php".to_string()),
+        ])
+    }
+
+    fn prettier_plugins(&self) -> &[&'static str] {
+        &["prettier-plugin-tailwindcss"]
+    }
+}
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last_version_dir = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_dir() {
+                last_version_dir = Some(entry.path());
+            }
+        }
+        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+        let server_path = last_version_dir.join(SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}

crates/zed2/src/languages/toml/config.toml 🔗

@@ -0,0 +1,10 @@
+name = "TOML"
+path_suffixes = ["Cargo.lock", "toml"]
+line_comment = "# "
+autoclose_before = ",]}"
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] },
+]

crates/zed2/src/languages/toml/highlights.scm 🔗

@@ -0,0 +1,37 @@
+; Properties
+;-----------
+
+(bare_key) @property
+(quoted_key) @property
+
+; Literals
+;---------
+
+(boolean) @constant
+(comment) @comment
+(string) @string
+(integer) @number
+(float) @number
+(offset_date_time) @string.special
+(local_date_time) @string.special
+(local_date) @string.special
+(local_time) @string.special
+
+; Punctuation
+;------------
+
+[
+  "."
+  ","
+] @punctuation.delimiter
+
+"=" @operator
+
+[
+  "["
+  "]"
+  "[["
+  "]]"
+  "{"
+  "}"
+]  @punctuation.bracket

crates/zed2/src/languages/tsx/config.toml 🔗

@@ -0,0 +1,25 @@
+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, not_in = ["string", "comment"] },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
+    { start = "`", end = "`", close = true, newline = false, not_in = ["string"] },
+    { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
+]
+word_characters = ["#", "$"]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]
+prettier_parser_name = "typescript"
+
+[overrides.element]
+line_comment = { remove = true }
+block_comment = ["{/* ", " */}"]
+
+[overrides.string]
+word_characters = ["-"]
+opt_into_language_servers = ["tailwindcss-language-server"]

crates/zed2/src/languages/tsx/embedding.scm 🔗

@@ -0,0 +1,85 @@
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (function_declaration
+                "async"? @name
+                "function" @name
+                name: (_) @name))
+        (function_declaration
+            "async"? @name
+            "function" @name
+            name: (_) @name)
+        ] @item
+    )
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (class_declaration
+                "class" @name
+                name: (_) @name))
+        (class_declaration
+            "class" @name
+            name: (_) @name)
+        ] @item
+    )
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (interface_declaration
+                "interface" @name
+                name: (_) @name))
+        (interface_declaration
+            "interface" @name
+            name: (_) @name)
+        ] @item
+    )
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (enum_declaration
+                "enum" @name
+                name: (_) @name))
+        (enum_declaration
+            "enum" @name
+            name: (_) @name)
+        ] @item
+    )
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (type_alias_declaration
+                "type" @name
+                name: (_) @name))
+        (type_alias_declaration
+            "type" @name
+            name: (_) @name)
+        ] @item
+    )
+
+(
+    (comment)* @context
+    .
+    (method_definition
+        [
+            "get"
+            "set"
+            "async"
+            "*"
+            "static"
+            ]* @name
+        name: (_) @name) @item
+    )

crates/zed2/src/languages/typescript.rs 🔗

@@ -0,0 +1,384 @@
+use anyhow::{anyhow, Result};
+use async_compression::futures::bufread::GzipDecoder;
+use async_tar::Archive;
+use async_trait::async_trait;
+use futures::{future::BoxFuture, FutureExt};
+use gpui2::AppContext;
+use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::{CodeActionKind, LanguageServerBinary};
+use node_runtime::NodeRuntime;
+use serde_json::{json, Value};
+use smol::{fs, io::BufReader, stream::StreamExt};
+use std::{
+    any::Any,
+    ffi::OsString,
+    future,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::{fs::remove_matching, github::latest_github_release};
+use util::{github::GitHubLspBinaryVersion, ResultExt};
+
+fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![
+        server_path.into(),
+        "--stdio".into(),
+        "--tsserver-path".into(),
+        "node_modules/typescript/lib".into(),
+    ]
+}
+
+fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct TypeScriptLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl TypeScriptLspAdapter {
+    const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
+    const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";
+
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        TypeScriptLspAdapter { node }
+    }
+}
+
+struct TypeScriptVersions {
+    typescript_version: String,
+    server_version: String,
+}
+
+#[async_trait]
+impl LspAdapter for TypeScriptLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("typescript-language-server".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "tsserver"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(TypeScriptVersions {
+            typescript_version: self.node.npm_package_latest_version("typescript").await?,
+            server_version: self
+                .node
+                .npm_package_latest_version("typescript-language-server")
+                .await?,
+        }) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<TypeScriptVersions>().unwrap();
+        let server_path = container_dir.join(Self::NEW_SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[
+                        ("typescript", version.typescript_version.as_str()),
+                        (
+                            "typescript-language-server",
+                            version.server_version.as_str(),
+                        ),
+                    ],
+                )
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: typescript_server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_ts_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_ts_server_binary(container_dir, &*self.node).await
+    }
+
+    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
+        Some(vec![
+            CodeActionKind::QUICKFIX,
+            CodeActionKind::REFACTOR,
+            CodeActionKind::REFACTOR_EXTRACT,
+            CodeActionKind::SOURCE,
+        ])
+    }
+
+    async fn label_for_completion(
+        &self,
+        item: &lsp2::CompletionItem,
+        language: &Arc<language2::Language>,
+    ) -> Option<language2::CodeLabel> {
+        use lsp2::CompletionItemKind as Kind;
+        let len = item.label.len();
+        let grammar = language.grammar()?;
+        let highlight_id = match item.kind? {
+            Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
+            Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
+            Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
+            Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
+            Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
+            _ => None,
+        }?;
+
+        let text = match &item.detail {
+            Some(detail) => format!("{} {}", item.label, detail),
+            None => item.label.clone(),
+        };
+
+        Some(language2::CodeLabel {
+            text,
+            runs: vec![(0..len, highlight_id)],
+            filter_range: 0..len,
+        })
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        Some(json!({
+            "provideFormatter": true
+        }))
+    }
+}
+
+async fn get_cached_ts_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
+        let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH);
+        if new_server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: typescript_server_binary_arguments(&new_server_path),
+            })
+        } else if old_server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: typescript_server_binary_arguments(&old_server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                container_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}
+
+pub struct EsLintLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl EsLintLspAdapter {
+    const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
+
+    #[allow(unused)]
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        EsLintLspAdapter { node }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for EsLintLspAdapter {
+    fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+        future::ready(json!({
+            "": {
+                "validate": "on",
+                "rulesCustomizations": [],
+                "run": "onType",
+                "nodePath": null,
+            }
+        }))
+        .boxed()
+    }
+
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("eslint".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "eslint"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        // At the time of writing the latest vscode-eslint release was released in 2020 and requires
+        // special custom LSP protocol extensions be handled to fully initialize. Download the latest
+        // prerelease instead to sidestep this issue
+        let release =
+            latest_github_release("microsoft/vscode-eslint", true, delegate.http_client()).await?;
+        Ok(Box::new(GitHubLspBinaryVersion {
+            name: release.name,
+            url: release.tarball_url,
+        }))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+        let destination_path = container_dir.join(format!("vscode-eslint-{}", version.name));
+        let server_path = destination_path.join(Self::SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            remove_matching(&container_dir, |entry| entry != destination_path).await;
+
+            let mut response = delegate
+                .http_client()
+                .get(&version.url, Default::default(), true)
+                .await
+                .map_err(|err| anyhow!("error downloading release: {}", err))?;
+            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
+            let archive = Archive::new(decompressed_bytes);
+            archive.unpack(&destination_path).await?;
+
+            let mut dir = fs::read_dir(&destination_path).await?;
+            let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
+            let repo_root = destination_path.join("vscode-eslint");
+            fs::rename(first.path(), &repo_root).await?;
+
+            self.node
+                .run_npm_subcommand(Some(&repo_root), "install", &[])
+                .await?;
+
+            self.node
+                .run_npm_subcommand(Some(&repo_root), "run-script", &["compile"])
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: eslint_server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_eslint_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_eslint_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn label_for_completion(
+        &self,
+        _item: &lsp2::CompletionItem,
+        _language: &Arc<language2::Language>,
+    ) -> Option<language2::CodeLabel> {
+        None
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        None
+    }
+}
+
+async fn get_cached_eslint_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        // This is unfortunate but we don't know what the version is to build a path directly
+        let mut dir = fs::read_dir(&container_dir).await?;
+        let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
+        if !first.file_type().await?.is_dir() {
+            return Err(anyhow!("First entry is not a directory"));
+        }
+        let server_path = first.path().join(EsLintLspAdapter::SERVER_PATH);
+
+        Ok(LanguageServerBinary {
+            path: node.binary_path().await?,
+            arguments: eslint_server_binary_arguments(&server_path),
+        })
+    })()
+    .await
+    .log_err()
+}
+
+#[cfg(test)]
+mod tests {
+    use gpui2::{Context, TestAppContext};
+    use unindent::Unindent;
+
+    #[gpui2::test]
+    async fn test_outline(cx: &mut TestAppContext) {
+        let language = crate::languages::language(
+            "typescript",
+            tree_sitter_typescript::language_typescript(),
+            None,
+        )
+        .await;
+
+        let text = r#"
+            function a() {
+              // local variables are omitted
+              let a1 = 1;
+              // all functions are included
+              async function a2() {}
+            }
+            // top-level variables are included
+            let b: C
+            function getB() {}
+            // exported variables are included
+            export const d = e;
+        "#
+        .unindent();
+
+        let buffer = cx.build_model(|cx| {
+            language2::Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
+        });
+        let outline = buffer.update(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
+        assert_eq!(
+            outline
+                .items
+                .iter()
+                .map(|item| (item.text.as_str(), item.depth))
+                .collect::<Vec<_>>(),
+            &[
+                ("function a()", 0),
+                ("async function a2()", 1),
+                ("let b", 0),
+                ("function getB()", 0),
+                ("const d", 0),
+            ]
+        );
+    }
+}

crates/zed2/src/languages/typescript/config.toml 🔗

@@ -0,0 +1,16 @@
+name = "TypeScript"
+path_suffixes = ["ts", "cts", "d.cts", "d.mts", "mts"]
+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, not_in = ["string", "comment"] },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
+    { start = "`", end = "`", close = true, newline = false, not_in = ["string"] },
+    { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
+]
+word_characters = ["#", "$"]
+prettier_parser_name = "typescript"

crates/zed2/src/languages/typescript/embedding.scm 🔗

@@ -0,0 +1,85 @@
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (function_declaration
+                "async"? @name
+                "function" @name
+                name: (_) @name))
+        (function_declaration
+            "async"? @name
+            "function" @name
+            name: (_) @name)
+    ] @item
+)
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (class_declaration
+                "class" @name
+                name: (_) @name))
+        (class_declaration
+            "class" @name
+            name: (_) @name)
+    ] @item
+)
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (interface_declaration
+                "interface" @name
+                name: (_) @name))
+        (interface_declaration
+            "interface" @name
+            name: (_) @name)
+    ] @item
+)
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (enum_declaration
+                "enum" @name
+                name: (_) @name))
+        (enum_declaration
+            "enum" @name
+            name: (_) @name)
+    ] @item
+)
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (type_alias_declaration
+                "type" @name
+                name: (_) @name))
+        (type_alias_declaration
+            "type" @name
+            name: (_) @name)
+    ] @item
+)
+
+(
+    (comment)* @context
+    .
+    (method_definition
+        [
+            "get"
+            "set"
+            "async"
+            "*"
+            "static"
+            ]* @name
+        name: (_) @name) @item
+)

crates/zed2/src/languages/typescript/highlights.scm 🔗

@@ -0,0 +1,221 @@
+; 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) @type
+ (#match? @type "^[A-Z]"))
+(type_identifier) @type
+(predefined_type) @type.builtin
+
+([
+  (identifier)
+  (shorthand_property_identifier)
+  (shorthand_property_identifier_pattern)
+ ] @constant
+ (#match? @constant "^_*[A-Z_][A-Z\\d_]*$"))
+
+; Literals
+
+(this) @variable.special
+(super) @variable.special
+
+[
+  (null)
+  (undefined)
+] @constant.builtin
+
+[
+  (true)
+  (false)
+] @boolean
+
+(comment) @comment
+
+[
+  (string)
+  (template_string)
+] @string
+
+(regex) @string.regex
+(number) @number
+
+; Tokens
+
+[
+  ";"
+  "?."
+  "."
+  ","
+  ":"
+] @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"
+  "satisfies"
+  "set"
+  "static"
+  "switch"
+  "target"
+  "throw"
+  "try"
+  "typeof"
+  "var"
+  "void"
+  "while"
+  "with"
+  "yield"
+] @keyword
+
+(template_substitution
+  "${" @punctuation.special
+  "}" @punctuation.special) @embedded
+
+(type_arguments
+  "<" @punctuation.bracket
+  ">" @punctuation.bracket)
+
+; Keywords
+
+[ "abstract"
+  "declare"
+  "enum"
+  "export"
+  "implements"
+  "interface"
+  "keyof"
+  "namespace"
+  "private"
+  "protected"
+  "public"
+  "type"
+  "readonly"
+  "override"
+] @keyword

crates/zed2/src/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/zed2/src/languages/typescript/outline.scm 🔗

@@ -0,0 +1,65 @@
+(internal_module
+    "namespace" @context
+    name: (_) @name) @item
+
+(enum_declaration
+    "enum" @context
+    name: (_) @name) @item
+
+(type_alias_declaration
+    "type" @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
+
+(export_statement
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            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/zed2/src/languages/vue.rs 🔗

@@ -0,0 +1,220 @@
+use anyhow::{anyhow, ensure, Result};
+use async_trait::async_trait;
+use futures::StreamExt;
+pub use language2::*;
+use lsp2::{CodeActionKind, LanguageServerBinary};
+use node_runtime::NodeRuntime;
+use parking_lot::Mutex;
+use serde_json::Value;
+use smol::fs::{self};
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
+
+pub struct VueLspVersion {
+    vue_version: String,
+    ts_version: String,
+}
+
+pub struct VueLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+    typescript_install_path: Mutex<Option<PathBuf>>,
+}
+
+impl VueLspAdapter {
+    const SERVER_PATH: &'static str =
+        "node_modules/@vue/language-server/bin/vue-language-server.js";
+    // TODO: this can't be hardcoded, yet we have to figure out how to pass it in initialization_options.
+    const TYPESCRIPT_PATH: &'static str = "node_modules/typescript/lib";
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        let typescript_install_path = Mutex::new(None);
+        Self {
+            node,
+            typescript_install_path,
+        }
+    }
+}
+#[async_trait]
+impl super::LspAdapter for VueLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("vue-language-server".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "vue-language-server"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(VueLspVersion {
+            vue_version: self
+                .node
+                .npm_package_latest_version("@vue/language-server")
+                .await?,
+            ts_version: self.node.npm_package_latest_version("typescript").await?,
+        }) as Box<_>)
+    }
+    async fn initialization_options(&self) -> Option<Value> {
+        let typescript_sdk_path = self.typescript_install_path.lock();
+        let typescript_sdk_path = typescript_sdk_path
+            .as_ref()
+            .expect("initialization_options called without a container_dir for typescript");
+
+        Some(serde_json::json!({
+            "typescript": {
+                "tsdk": typescript_sdk_path
+            }
+        }))
+    }
+    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
+        // REFACTOR is explicitly disabled, as vue-lsp does not adhere to LSP protocol for code actions with these - it
+        // sends back a CodeAction with neither `command` nor `edits` fields set, which is against the spec.
+        Some(vec![
+            CodeActionKind::EMPTY,
+            CodeActionKind::QUICKFIX,
+            CodeActionKind::REFACTOR_REWRITE,
+        ])
+    }
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<VueLspVersion>().unwrap();
+        let server_path = container_dir.join(Self::SERVER_PATH);
+        let ts_path = container_dir.join(Self::TYPESCRIPT_PATH);
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[("@vue/language-server", version.vue_version.as_str())],
+                )
+                .await?;
+        }
+        ensure!(
+            fs::metadata(&server_path).await.is_ok(),
+            "@vue/language-server package installation failed"
+        );
+        if fs::metadata(&ts_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[("typescript", version.ts_version.as_str())],
+                )
+                .await?;
+        }
+
+        ensure!(
+            fs::metadata(&ts_path).await.is_ok(),
+            "typescript for Vue package installation failed"
+        );
+        *self.typescript_install_path.lock() = Some(ts_path);
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: vue_server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone()).await?;
+        *self.typescript_install_path.lock() = Some(ts_path);
+        Some(server)
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone())
+            .await
+            .map(|(mut binary, ts_path)| {
+                binary.arguments = vec!["--help".into()];
+                (binary, ts_path)
+            })?;
+        *self.typescript_install_path.lock() = Some(ts_path);
+        Some(server)
+    }
+
+    async fn label_for_completion(
+        &self,
+        item: &lsp2::CompletionItem,
+        language: &Arc<language2::Language>,
+    ) -> Option<language2::CodeLabel> {
+        use lsp2::CompletionItemKind as Kind;
+        let len = item.label.len();
+        let grammar = language.grammar()?;
+        let highlight_id = match item.kind? {
+            Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
+            Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
+            Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
+            Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
+            Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("tag"),
+            Kind::VARIABLE => grammar.highlight_id_for_name("type"),
+            Kind::KEYWORD => grammar.highlight_id_for_name("keyword"),
+            Kind::VALUE => grammar.highlight_id_for_name("tag"),
+            _ => None,
+        }?;
+
+        let text = match &item.detail {
+            Some(detail) => format!("{} {}", item.label, detail),
+            None => item.label.clone(),
+        };
+
+        Some(language2::CodeLabel {
+            text,
+            runs: vec![(0..len, highlight_id)],
+            filter_range: 0..len,
+        })
+    }
+}
+
+fn vue_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+type TypescriptPath = PathBuf;
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: Arc<dyn NodeRuntime>,
+) -> Option<(LanguageServerBinary, TypescriptPath)> {
+    (|| async move {
+        let mut last_version_dir = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_dir() {
+                last_version_dir = Some(entry.path());
+            }
+        }
+        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+        let server_path = last_version_dir.join(VueLspAdapter::SERVER_PATH);
+        let typescript_path = last_version_dir.join(VueLspAdapter::TYPESCRIPT_PATH);
+        if server_path.exists() && typescript_path.exists() {
+            Ok((
+                LanguageServerBinary {
+                    path: node.binary_path().await?,
+                    arguments: vue_server_binary_arguments(&server_path),
+                },
+                typescript_path,
+            ))
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}

crates/zed2/src/languages/vue/config.toml 🔗

@@ -0,0 +1,14 @@
+name = "Vue.js"
+path_suffixes = ["vue"]
+block_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 = true, newline = true, not_in = ["string", "comment"] },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
+    { start = "`", end = "`", close = true, newline = false, not_in = ["string"] },
+]
+word_characters = ["-"]

crates/zed2/src/languages/vue/highlights.scm 🔗

@@ -0,0 +1,15 @@
+(attribute) @property
+(directive_attribute) @property
+(quoted_attribute_value) @string
+(interpolation) @punctuation.special
+(raw_text) @embedded
+
+((tag_name) @type
+ (#match? @type "^[A-Z]"))
+
+((directive_name) @keyword
+ (#match? @keyword "^v-"))
+
+(start_tag) @tag
+(end_tag) @tag
+(self_closing_tag) @tag

crates/zed2/src/languages/yaml.rs 🔗

@@ -0,0 +1,142 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use futures::{future::BoxFuture, FutureExt, StreamExt};
+use gpui2::AppContext;
+use language2::{
+    language_settings::all_language_settings, LanguageServerName, LspAdapter, LspAdapterDelegate,
+};
+use lsp2::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+use serde_json::Value;
+use smol::fs;
+use std::{
+    any::Any,
+    ffi::OsString,
+    future,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
+
+const SERVER_PATH: &'static str = "node_modules/yaml-language-server/bin/yaml-language-server";
+
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct YamlLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl YamlLspAdapter {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        YamlLspAdapter { node }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for YamlLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("yaml-language-server".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "yaml"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(
+            self.node
+                .npm_package_latest_version("yaml-language-server")
+                .await?,
+        ) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<String>().unwrap();
+        let server_path = container_dir.join(SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[("yaml-language-server", version.as_str())],
+                )
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+    fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> {
+        let tab_size = all_language_settings(None, cx)
+            .language(Some("YAML"))
+            .tab_size;
+
+        future::ready(serde_json::json!({
+            "yaml": {
+                "keyOrdering": false
+            },
+            "[yaml]": {
+                "editor.tabSize": tab_size,
+            }
+        }))
+        .boxed()
+    }
+}
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last_version_dir = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_dir() {
+                last_version_dir = Some(entry.path());
+            }
+        }
+        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+        let server_path = last_version_dir.join(SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}

crates/zed2/src/languages/yaml/config.toml 🔗

@@ -0,0 +1,12 @@
+name = "YAML"
+path_suffixes = ["yml", "yaml"]
+line_comment = "# "
+autoclose_before = ",]}"
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+]
+
+increase_indent_pattern = ":\\s*[|>]?\\s*$"
+prettier_parser_name = "yaml"

crates/zed2/src/languages/yaml/highlights.scm 🔗

@@ -0,0 +1,49 @@
+(boolean_scalar) @boolean
+(null_scalar) @constant.builtin
+
+[
+  (double_quote_scalar)
+  (single_quote_scalar)
+  (block_scalar)
+  (string_scalar)
+] @string
+
+(escape_sequence) @string.escape
+
+[
+  (integer_scalar)
+  (float_scalar)
+] @number
+
+(comment) @comment
+
+[
+  (anchor_name)
+  (alias_name)
+  (tag) 
+] @type
+
+key: (flow_node (plain_scalar (string_scalar) @property)) 
+
+[
+ ","
+ "-"
+ ":"
+ ">"
+ "?"
+ "|"
+] @punctuation.delimiter
+
+[
+ "["
+ "]"
+ "{"
+ "}"
+] @punctuation.bracket
+
+[
+ "*"
+ "&"
+ "---"
+ "..."
+] @punctuation.special

crates/zed2/src/main.rs 🔗

@@ -45,12 +45,8 @@ use util::{
     paths, ResultExt,
 };
 use uuid::Uuid;
+use zed2::languages;
 use zed2::{ensure_only_instance, AppState, Assets, IsOnlyInstance};
-// use zed2::{
-//     assets::Assets,
-//     build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
-//     only_instance::{ensure_only_instance, IsOnlyInstance},
-// };
 
 mod open_listener;
 
@@ -117,9 +113,11 @@ fn main() {
         let copilot_language_server_id = languages.next_language_server_id();
         languages.set_executor(cx.executor().clone());
         languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
+        let languages = Arc::new(languages);
         let node_runtime = RealNodeRuntime::new(http.clone());
 
         language2::init(cx);
+        languages::init(languages.clone(), node_runtime.clone(), cx);
         let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
         // let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));