Extract lua language support into an extension (#10437)

Release Notes:

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

---------

Co-authored-by: Marshall <marshall@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-04-11 11:32:10 -07:00 committed by GitHub
parent c6028f6651
commit 176f440158
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 265 additions and 225 deletions

18
Cargo.lock generated
View file

@ -5438,7 +5438,6 @@ dependencies = [
"tree-sitter-heex", "tree-sitter-heex",
"tree-sitter-jsdoc", "tree-sitter-jsdoc",
"tree-sitter-json 0.20.0", "tree-sitter-json 0.20.0",
"tree-sitter-lua",
"tree-sitter-markdown", "tree-sitter-markdown",
"tree-sitter-nix", "tree-sitter-nix",
"tree-sitter-nu", "tree-sitter-nu",
@ -10435,16 +10434,6 @@ dependencies = [
"tree-sitter", "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]] [[package]]
name = "tree-sitter-markdown" name = "tree-sitter-markdown"
version = "0.0.1" version = "0.0.1"
@ -12648,6 +12637,13 @@ dependencies = [
"zed_extension_api 0.0.4", "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]] [[package]]
name = "zed_php" name = "zed_php"
version = "0.0.1" version = "0.0.1"

View file

@ -110,6 +110,7 @@ members = [
"extensions/gleam", "extensions/gleam",
"extensions/haskell", "extensions/haskell",
"extensions/html", "extensions/html",
"extensions/lua",
"extensions/php", "extensions/php",
"extensions/prisma", "extensions/prisma",
"extensions/purescript", "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-html = "0.19.0"
tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", ref = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" } 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-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-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-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" } tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "7dd29f9616822e5fc259f5b4ae6c4ded9a71a132" }

View file

@ -8,66 +8,70 @@ use extension::ExtensionStore;
use gpui::{Model, VisualContext}; use gpui::{Model, VisualContext};
use language::Buffer; use language::Buffer;
use ui::{SharedString, ViewContext}; use ui::{SharedString, ViewContext};
use workspace::notifications::NotificationId; use workspace::{
use workspace::{notifications::simple_message_notification, 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>> { fn suggested_extensions() -> &'static HashMap<&'static str, Arc<str>> {
static SUGGESTED: OnceLock<HashMap<&str, Arc<str>>> = OnceLock::new(); static SUGGESTIONS_BY_PATH_SUFFIX: OnceLock<HashMap<&str, Arc<str>>> = OnceLock::new();
SUGGESTED.get_or_init(|| { SUGGESTIONS_BY_PATH_SUFFIX.get_or_init(|| {
[ SUGGESTIONS_BY_EXTENSION_ID
("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() .into_iter()
.map(|(name, file)| (file, name.into())) .flat_map(|(name, path_suffixes)| {
let name = Arc::<str>::from(*name);
path_suffixes
.into_iter()
.map(move |suffix| (*suffix, name.clone()))
})
.collect() .collect()
}) })
} }

View file

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

View file

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

View file

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

16
extensions/lua/Cargo.toml Normal file
View file

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

View file

@ -0,0 +1 @@
../../LICENSE-APACHE

View file

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

View file

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

158
extensions/lua/src/lua.rs Normal file
View file

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