Extract lua language support into an extension (#10437)

Max Brunsfeld and Marshall created

Release Notes:

- Extracted lua language support into an extension, and improved Lua
highlighting and completion label styling.

---------

Co-authored-by: Marshall <marshall@zed.dev>

Change summary

Cargo.lock                                    |  18 -
Cargo.toml                                    |   2 
crates/extensions_ui/src/extension_suggest.rs | 122 ++++++++-------
crates/languages/Cargo.toml                   |   1 
crates/languages/src/lib.rs                   |   3 
crates/languages/src/lua.rs                   | 146 -------------------
extensions/lua/Cargo.toml                     |  16 ++
extensions/lua/LICENSE-APACHE                 |   1 
extensions/lua/extension.toml                 |  15 +
extensions/lua/languages/lua/brackets.scm     |   0 
extensions/lua/languages/lua/config.toml      |   0 
extensions/lua/languages/lua/embedding.scm    |   0 
extensions/lua/languages/lua/highlights.scm   |   8 
extensions/lua/languages/lua/indents.scm      |   0 
extensions/lua/languages/lua/outline.scm      |   0 
extensions/lua/src/lua.rs                     | 158 +++++++++++++++++++++
16 files changed, 265 insertions(+), 225 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5438,7 +5438,6 @@ dependencies = [
  "tree-sitter-heex",
  "tree-sitter-jsdoc",
  "tree-sitter-json 0.20.0",
- "tree-sitter-lua",
  "tree-sitter-markdown",
  "tree-sitter-nix",
  "tree-sitter-nu",
@@ -10435,16 +10434,6 @@ dependencies = [
  "tree-sitter",
 ]
 
-[[package]]
-name = "tree-sitter-lua"
-version = "0.0.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d489873fd1a2fa6d5f04930bfc5c081c96f0c038c1437104518b5b842c69b282"
-dependencies = [
- "cc",
- "tree-sitter",
-]
-
 [[package]]
 name = "tree-sitter-markdown"
 version = "0.0.1"
@@ -12648,6 +12637,13 @@ dependencies = [
  "zed_extension_api 0.0.4",
 ]
 
+[[package]]
+name = "zed_lua"
+version = "0.0.1"
+dependencies = [
+ "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "zed_php"
 version = "0.0.1"

Cargo.toml 🔗

@@ -110,6 +110,7 @@ members = [
     "extensions/gleam",
     "extensions/haskell",
     "extensions/html",
+    "extensions/lua",
     "extensions/php",
     "extensions/prisma",
     "extensions/purescript",
@@ -323,7 +324,6 @@ tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex
 tree-sitter-html = "0.19.0"
 tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", ref = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" }
 tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
-tree-sitter-lua = "0.0.14"
 tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
 tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
 tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "7dd29f9616822e5fc259f5b4ae6c4ded9a71a132" }

crates/extensions_ui/src/extension_suggest.rs 🔗

@@ -8,67 +8,71 @@ use extension::ExtensionStore;
 use gpui::{Model, VisualContext};
 use language::Buffer;
 use ui::{SharedString, ViewContext};
-use workspace::notifications::NotificationId;
-use workspace::{notifications::simple_message_notification, Workspace};
+use workspace::{
+    notifications::{simple_message_notification, NotificationId},
+    Workspace,
+};
+
+const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
+    ("astro", &["astro"]),
+    ("beancount", &["beancount"]),
+    ("clojure", &["bb", "clj", "cljc", "cljs", "edn"]),
+    ("csharp", &["cs"]),
+    ("dart", &["dart"]),
+    ("dockerfile", &["Dockerfile"]),
+    ("elisp", &["el"]),
+    ("elm", &["elm"]),
+    ("erlang", &["erl", "hrl"]),
+    ("fish", &["fish"]),
+    (
+        "git-firefly",
+        &[
+            ".gitconfig",
+            ".gitignore",
+            "COMMIT_EDITMSG",
+            "EDIT_DESCRIPTION",
+            "MERGE_MSG",
+            "NOTES_EDITMSG",
+            "TAG_EDITMSG",
+            "git-rebase-todo",
+        ],
+    ),
+    ("gleam", &["gleam"]),
+    ("glsl", &["vert", "frag"]),
+    ("graphql", &["gql", "graphql"]),
+    ("haskell", &["hs"]),
+    ("html", &["htm", "html", "shtml"]),
+    ("java", &["java"]),
+    ("kotlin", &["kt"]),
+    ("latex", &["tex"]),
+    ("lua", &["lua"]),
+    ("make", &["Makefile"]),
+    ("nix", &["nix"]),
+    ("php", &["php"]),
+    ("prisma", &["prisma"]),
+    ("purescript", &["purs"]),
+    ("r", &["r", "R"]),
+    ("sql", &["sql"]),
+    ("svelte", &["svelte"]),
+    ("swift", &["swift"]),
+    ("templ", &["templ"]),
+    ("toml", &["Cargo.lock", "toml"]),
+    ("wgsl", &["wgsl"]),
+    ("zig", &["zig"]),
+];
 
 fn suggested_extensions() -> &'static HashMap<&'static str, Arc<str>> {
-    static SUGGESTED: OnceLock<HashMap<&str, Arc<str>>> = OnceLock::new();
-    SUGGESTED.get_or_init(|| {
-        [
-            ("astro", "astro"),
-            ("beancount", "beancount"),
-            ("clojure", "bb"),
-            ("clojure", "clj"),
-            ("clojure", "cljc"),
-            ("clojure", "cljs"),
-            ("clojure", "edn"),
-            ("csharp", "cs"),
-            ("dart", "dart"),
-            ("dockerfile", "Dockerfile"),
-            ("elisp", "el"),
-            ("elm", "elm"),
-            ("erlang", "erl"),
-            ("erlang", "hrl"),
-            ("fish", "fish"),
-            ("git-firefly", ".gitconfig"),
-            ("git-firefly", ".gitignore"),
-            ("git-firefly", "COMMIT_EDITMSG"),
-            ("git-firefly", "EDIT_DESCRIPTION"),
-            ("git-firefly", "MERGE_MSG"),
-            ("git-firefly", "NOTES_EDITMSG"),
-            ("git-firefly", "TAG_EDITMSG"),
-            ("git-firefly", "git-rebase-todo"),
-            ("gleam", "gleam"),
-            ("glsl", "vert"),
-            ("glsl", "frag"),
-            ("graphql", "gql"),
-            ("graphql", "graphql"),
-            ("haskell", "hs"),
-            ("html", "htm"),
-            ("html", "html"),
-            ("html", "shtml"),
-            ("java", "java"),
-            ("kotlin", "kt"),
-            ("latex", "tex"),
-            ("make", "Makefile"),
-            ("nix", "nix"),
-            ("php", "php"),
-            ("prisma", "prisma"),
-            ("purescript", "purs"),
-            ("r", "r"),
-            ("r", "R"),
-            ("sql", "sql"),
-            ("svelte", "svelte"),
-            ("swift", "swift"),
-            ("templ", "templ"),
-            ("toml", "Cargo.lock"),
-            ("toml", "toml"),
-            ("wgsl", "wgsl"),
-            ("zig", "zig"),
-        ]
-        .into_iter()
-        .map(|(name, file)| (file, name.into()))
-        .collect()
+    static SUGGESTIONS_BY_PATH_SUFFIX: OnceLock<HashMap<&str, Arc<str>>> = OnceLock::new();
+    SUGGESTIONS_BY_PATH_SUFFIX.get_or_init(|| {
+        SUGGESTIONS_BY_EXTENSION_ID
+            .into_iter()
+            .flat_map(|(name, path_suffixes)| {
+                let name = Arc::<str>::from(*name);
+                path_suffixes
+                    .into_iter()
+                    .map(move |suffix| (*suffix, name.clone()))
+            })
+            .collect()
     })
 }
 

crates/languages/Cargo.toml 🔗

@@ -49,7 +49,6 @@ tree-sitter-hcl.workspace = true
 tree-sitter-heex.workspace = true
 tree-sitter-jsdoc.workspace = true
 tree-sitter-json.workspace = true
-tree-sitter-lua.workspace = true
 tree-sitter-markdown.workspace = true
 tree-sitter-nix.workspace = true
 tree-sitter-nu.workspace = true

crates/languages/src/lib.rs 🔗

@@ -18,7 +18,6 @@ mod deno;
 mod elixir;
 mod go;
 mod json;
-mod lua;
 mod nu;
 mod ocaml;
 mod python;
@@ -69,7 +68,6 @@ pub fn init(
         ("heex", tree_sitter_heex::language()),
         ("jsdoc", tree_sitter_jsdoc::language()),
         ("json", tree_sitter_json::language()),
-        ("lua", tree_sitter_lua::language()),
         ("markdown", tree_sitter_markdown::language()),
         ("nix", tree_sitter_nix::language()),
         ("nu", tree_sitter_nu::language()),
@@ -280,7 +278,6 @@ pub fn init(
     language!("scheme");
     language!("racket");
     language!("regex");
-    language!("lua", vec![Arc::new(lua::LuaLspAdapter)]);
     language!(
         "yaml",
         vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))]

crates/languages/src/lua.rs 🔗

@@ -1,146 +0,0 @@
-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 language::{LanguageServerName, LspAdapterDelegate};
-use lsp::LanguageServerBinary;
-use smol::fs;
-use std::{any::Any, env::consts, path::PathBuf};
-use util::{
-    github::{latest_github_release, GitHubLspBinaryVersion},
-    maybe, ResultExt,
-};
-
-#[derive(Copy, Clone)]
-pub struct LuaLspAdapter;
-
-#[async_trait(?Send)]
-impl super::LspAdapter for LuaLspAdapter {
-    fn name(&self) -> LanguageServerName {
-        LanguageServerName("lua-language-server".into())
-    }
-
-    async fn fetch_latest_server_version(
-        &self,
-        delegate: &dyn LspAdapterDelegate,
-    ) -> Result<Box<dyn 'static + Send + Any>> {
-        let os = match consts::OS {
-            "macos" => "darwin",
-            "linux" => "linux",
-            "windows" => "win32",
-            other => bail!("Running on unsupported os: {other}"),
-        };
-        let platform = match consts::ARCH {
-            "x86_64" => "x64",
-            "aarch64" => "arm64",
-            other => bail!("Running on unsupported platform: {other}"),
-        };
-        let release = latest_github_release(
-            "LuaLS/lua-language-server",
-            true,
-            false,
-            delegate.http_client(),
-        )
-        .await?;
-        let version = &release.tag_name;
-        let asset_name = format!("lua-language-server-{version}-{os}-{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.tag_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 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?;
-        }
-
-        // todo("windows")
-        #[cfg(not(windows))]
-        {
-            fs::set_permissions(
-                &binary_path,
-                <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
-            )
-            .await?;
-        }
-        Ok(LanguageServerBinary {
-            path: binary_path,
-            env: None,
-            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> {
-    maybe!(async {
-        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,
-                env: None,
-                arguments: Vec::new(),
-            })
-        } else {
-            Err(anyhow!("no cached binary"))
-        }
-    })
-    .await
-    .log_err()
-}

extensions/lua/Cargo.toml 🔗

@@ -0,0 +1,16 @@
+[package]
+name = "zed_lua"
+version = "0.0.1"
+edition = "2021"
+publish = false
+license = "Apache-2.0"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/lua.rs"
+crate-type = ["cdylib"]
+
+[dependencies]
+zed_extension_api = "0.0.6"

extensions/lua/extension.toml 🔗

@@ -0,0 +1,15 @@
+id = "lua"
+name = "Lua"
+description = "Lua support."
+version = "0.1.0"
+schema_version = 1
+authors = ["Kaylee Simmons <kay@the-simmons.net>"]
+repository = "https://github.com/zed-industries/zed"
+
+[language_servers.lua-language-server]
+name = "LuaLS"
+language = "Lua"
+
+[grammars.lua]
+repository = "https://github.com/tree-sitter-grammars/tree-sitter-lua"
+commit = "a24dab177e58c9c6832f96b9a73102a0cfbced4a"

crates/languages/src/lua/highlights.scm → extensions/lua/languages/lua/highlights.scm 🔗

@@ -150,9 +150,9 @@
 
 ;; Tables
 
-(field name: (identifier) @field)
+(field name: (identifier) @property)
 
-(dot_index_expression field: (identifier) @field)
+(dot_index_expression field: (identifier) @property)
 
 (table_constructor
 [
@@ -176,7 +176,7 @@
     (dot_index_expression field: (identifier) @function.definition)
   ])
 
-(method_index_expression method: (identifier) @method)
+(method_index_expression method: (identifier) @function.method)
 
 (function_call
   (identifier) @function.builtin
@@ -195,4 +195,4 @@
 
 (number) @number
 
-(string) @string
+(string) @string

extensions/lua/src/lua.rs 🔗

@@ -0,0 +1,158 @@
+use std::fs;
+use zed::lsp::CompletionKind;
+use zed::{CodeLabel, CodeLabelSpan, LanguageServerId};
+use zed_extension_api::{self as zed, Result};
+
+struct LuaExtension {
+    cached_binary_path: Option<String>,
+}
+
+impl LuaExtension {
+    fn language_server_binary_path(
+        &mut self,
+        language_server_id: &LanguageServerId,
+        worktree: &zed::Worktree,
+    ) -> Result<String> {
+        if let Some(path) = &self.cached_binary_path {
+            if fs::metadata(path).map_or(false, |stat| stat.is_file()) {
+                return Ok(path.clone());
+            }
+        }
+
+        if let Some(path) = worktree.which("lua-language-server") {
+            self.cached_binary_path = Some(path.clone());
+            return Ok(path);
+        }
+
+        zed::set_language_server_installation_status(
+            &language_server_id,
+            &zed::LanguageServerInstallationStatus::CheckingForUpdate,
+        );
+        let release = zed::latest_github_release(
+            "LuaLS/lua-language-server",
+            zed::GithubReleaseOptions {
+                require_assets: true,
+                pre_release: false,
+            },
+        )?;
+
+        let (platform, arch) = zed::current_platform();
+        let asset_name = format!(
+            "lua-language-server-{version}-{os}-{arch}.tar.gz",
+            version = release.version,
+            os = match platform {
+                zed::Os::Mac => "darwin",
+                zed::Os::Linux => "linux",
+                zed::Os::Windows => "win32",
+            },
+            arch = match arch {
+                zed::Architecture::Aarch64 => "arm64",
+                zed::Architecture::X8664 => "x86_64",
+                zed::Architecture::X86 => return Err("unsupported platform x86".into()),
+            },
+        );
+
+        let asset = release
+            .assets
+            .iter()
+            .find(|asset| asset.name == asset_name)
+            .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?;
+
+        let version_dir = format!("lua-language-server-{}", release.version);
+        let binary_path = format!("{version_dir}/bin/lua-language-server");
+
+        if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
+            zed::set_language_server_installation_status(
+                &language_server_id,
+                &zed::LanguageServerInstallationStatus::Downloading,
+            );
+
+            zed::download_file(
+                &asset.download_url,
+                &version_dir,
+                zed::DownloadedFileType::GzipTar,
+            )
+            .map_err(|e| format!("failed to download file: {e}"))?;
+
+            let entries =
+                fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
+            for entry in entries {
+                let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
+                if entry.file_name().to_str() != Some(&version_dir) {
+                    fs::remove_dir_all(&entry.path()).ok();
+                }
+            }
+        }
+
+        self.cached_binary_path = Some(binary_path.clone());
+        Ok(binary_path)
+    }
+}
+
+impl zed::Extension for LuaExtension {
+    fn new() -> Self {
+        Self {
+            cached_binary_path: None,
+        }
+    }
+
+    fn language_server_command(
+        &mut self,
+        language_server_id: &LanguageServerId,
+        worktree: &zed::Worktree,
+    ) -> Result<zed::Command> {
+        Ok(zed::Command {
+            command: self.language_server_binary_path(language_server_id, worktree)?,
+            args: Default::default(),
+            env: Default::default(),
+        })
+    }
+
+    fn label_for_completion(
+        &self,
+        _language_server_id: &LanguageServerId,
+        completion: zed::lsp::Completion,
+    ) -> Option<CodeLabel> {
+        match completion.kind? {
+            CompletionKind::Method | CompletionKind::Function => {
+                let name_len = completion.label.find('(').unwrap_or(completion.label.len());
+                Some(CodeLabel {
+                    spans: vec![CodeLabelSpan::code_range(0..completion.label.len())],
+                    filter_range: (0..name_len).into(),
+                    code: completion.label,
+                })
+            }
+            CompletionKind::Field => Some(CodeLabel {
+                spans: vec![CodeLabelSpan::literal(
+                    completion.label.clone(),
+                    Some("property".into()),
+                )],
+                filter_range: (0..completion.label.len()).into(),
+                code: Default::default(),
+            }),
+            _ => None,
+        }
+    }
+
+    fn label_for_symbol(
+        &self,
+        _language_server_id: &LanguageServerId,
+        symbol: zed::lsp::Symbol,
+    ) -> Option<CodeLabel> {
+        let prefix = "let a = ";
+        let suffix = match symbol.kind {
+            zed::lsp::SymbolKind::Method => "()",
+            _ => "",
+        };
+        let code = format!("{prefix}{}{suffix}", symbol.name);
+        Some(CodeLabel {
+            spans: vec![CodeLabelSpan::code_range(
+                prefix.len()..code.len() - suffix.len(),
+            )],
+            filter_range: (0..symbol.name.len()).into(),
+            code,
+        })
+    }
+}
+
+zed::register_extension!(LuaExtension);