Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,86 @@

A [Lua](https://www.lua.org/) extension for [Zed](https://zed.dev).

## Language servers

The extension supports multiple language servers.

| Name | GitHub | Settings ID | Binary |
| --------- | ------------------------------------------------------------------------------------- | --------------------- | --------------------- |
| LuaLS | [LuaLS/lua-language-server](https://github.com/LuaLS/lua-language-server) | `lua-language-server` | `lua-language-server` |
| EmmyLuaLS | [EmmyLuaLs/emmylua-analyzer-rust](https://github.com/EmmyLuaLs/emmylua-analyzer-rust) | `emmylua_ls` | `emmylua_ls` |

If Zed finds the binary for an LSP in your PATH (e.g. `which emmylua_ls` or `which lua-language-server`) it will use that version, if not it will automatically download managed copies of the newest version via GitHub releases.

### Language server selection

You should only enable one language server at a time. For backwards compatability, Zed defaults to `LuaLS`.

If you would like to use EmmyLuaLS instead, you can specify your preferred language server via Zed Settings:

```json
{
"languages": {
"Lua": {
"language_servers": ["emmylua_ls", "!lua-language-server", "..."]
}
}
}
```

Explicitly use LuaLS:

```json
{
"languages": {
"Lua": {
"language_servers": ["lua-language-server", "!emmylua_ls", "..."]
}
}
}
```

Or to use no language server:

```json
{
"languages": {
"Lua": {
"enable_language_server": false
}
}
}
```


### Advanced CLI Settings

You can also specify optional `binary` configuration per language-server if required:

```json
{
"lsp": {
"emmylua_ls": {
"binary": {
"path": "/home/somebody/.cargo/bin/emmylua_ls",
"arguments": ["--log-level", "debug"],
"env": {
"RUST_LOG": "debug"
}
}
}
}
}
```

## Links

- Official [Zed Lua Language Docs](https://zed.dev/docs/languages/lua)
- [`.luarc.json` Documentation](https://luals.github.io/wiki/configuration/#luarcjson-file) and [`.luarc.json` Schema](https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json)
- [`.emmyrc.json` Docucmentation](https://github.com/EmmyLuaLs/emmylua-analyzer-rust/blob/main/docs/config/emmyrc_json_EN.md) and [`.emmyrc.json` Schema](https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json)
- Luau Extension: [4teapo/zed-luau ](https://github.com/4teapo/zed-luau)
- Fennel Extension: [notpeter/fennel](https://github.com/notpeter/fennel-zed/)

## Development

To develop this extension, see the [Developing Extensions](https://zed.dev/docs/extensions/developing-extensions) section of the Zed docs.
4 changes: 4 additions & 0 deletions extension.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ repository = "https://github.com/zed-extensions/lua"
name = "LuaLS"
languages = ["Lua", "EmmyLuadoc"]

[language_servers.emmylua_ls]
name = "EmmyLuaLS"
languages = ["Lua", "EmmyLuadoc"]

[grammars.lua]
repository = "https://github.com/tree-sitter-grammars/tree-sitter-lua"
commit = "d76023017f7485eae629cb60d406c7a1ca0f40c9"
Expand Down
152 changes: 152 additions & 0 deletions src/language_servers/emmylua_ls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
use super::remove_old_server_versions;
use std::fs;
use zed_extension_api::{self as zed, settings::LspSettings, LanguageServerId, Result};

pub struct EmmyLuaLs {
cached_binary_path: Option<String>,
}

impl EmmyLuaLs {
pub const SERVER_ID: &str = "emmylua_ls";
const BINARY_NAME: &str = "emmylua_ls";

pub fn new() -> Self {
Self {
cached_binary_path: None,
}
}

pub fn language_server_command(
&mut self,
language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> Result<zed::Command> {
let binary = LspSettings::for_worktree(language_server_id.as_ref(), worktree)?.binary;
let args = binary
.as_ref()
.and_then(|binary| binary.arguments.clone())
.unwrap_or_default();
let env = binary
.as_ref()
.and_then(|binary| binary.env.clone())
.map(|env| env.into_iter().collect())
.unwrap_or_default();
let configured_or_system_path = binary
.and_then(|binary| binary.path)
.or_else(|| worktree.which(Self::BINARY_NAME));
let command = match configured_or_system_path {
Some(path) => path,
None => self.zed_managed_binary_path(language_server_id)?,
};

Ok(zed::Command { command, args, env })
}

fn zed_managed_binary_path(&mut self, language_server_id: &LanguageServerId) -> Result<String> {
if let Some(path) = &self.cached_binary_path {
if fs::metadata(path).is_ok_and(|stat| stat.is_file()) {
return Ok(path.clone());
}
}

zed::set_language_server_installation_status(
language_server_id,
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
);
let release = zed::latest_github_release(
"EmmyLuaLs/emmylua-analyzer-rust",
zed::GithubReleaseOptions {
require_assets: true,
pre_release: false,
},
)?;

let (platform, arch) = zed::current_platform();
let asset_name = Self::asset_name(platform, arch)?;
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!("emmylua_ls-{}", release.version);
let binary_path = format!(
"{version_dir}/emmylua_ls{extension}",
extension = match platform {
zed::Os::Mac | zed::Os::Linux => "",
zed::Os::Windows => ".exe",
},
);

if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) {
zed::set_language_server_installation_status(
language_server_id,
&zed::LanguageServerInstallationStatus::Downloading,
);

zed::download_file(
&asset.download_url,
&version_dir,
match platform {
zed::Os::Mac | zed::Os::Linux => zed::DownloadedFileType::GzipTar,
zed::Os::Windows => zed::DownloadedFileType::Zip,
},
)
.map_err(|e| format!("failed to download file: {e}"))?;

remove_old_server_versions("emmylua_ls-", &version_dir)?;
}

self.cached_binary_path = Some(binary_path.clone());
Ok(binary_path)
}

fn asset_name(platform: zed::Os, arch: zed::Architecture) -> Result<String> {
let platform_name = match (platform, arch) {
(zed::Os::Mac, zed::Architecture::Aarch64) => "darwin-arm64",
(zed::Os::Mac, zed::Architecture::X8664) => "darwin-x64",
(zed::Os::Mac, zed::Architecture::X86) => {
return Err("unsupported macOS architecture x86".into());
}
(zed::Os::Linux, zed::Architecture::Aarch64) => "linux-aarch64-glibc.2.17",
(zed::Os::Linux, zed::Architecture::X8664) => "linux-x64-glibc.2.17",
(zed::Os::Linux, zed::Architecture::X86) => {
return Err("unsupported Linux architecture x86".into());
}
(zed::Os::Windows, zed::Architecture::Aarch64) => "win32-arm64",
(zed::Os::Windows, zed::Architecture::X8664) => "win32-x64",
(zed::Os::Windows, zed::Architecture::X86) => "win32-ia32",
};
let extension = match platform {
zed::Os::Mac | zed::Os::Linux => "tar.gz",
zed::Os::Windows => "zip",
};
Ok(format!("emmylua_ls-{platform_name}.{extension}"))
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn uses_its_own_server_id() {
assert_eq!(EmmyLuaLs::SERVER_ID, "emmylua_ls");
}

#[test]
fn maps_supported_release_assets() {
assert_eq!(
EmmyLuaLs::asset_name(zed::Os::Mac, zed::Architecture::Aarch64).unwrap(),
"emmylua_ls-darwin-arm64.tar.gz"
);
assert_eq!(
EmmyLuaLs::asset_name(zed::Os::Linux, zed::Architecture::X8664).unwrap(),
"emmylua_ls-linux-x64-glibc.2.17.tar.gz"
);
assert_eq!(
EmmyLuaLs::asset_name(zed::Os::Windows, zed::Architecture::X86).unwrap(),
"emmylua_ls-win32-ia32.zip"
);
}
}
131 changes: 131 additions & 0 deletions src/language_servers/lua_ls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
use super::remove_old_server_versions;
use std::fs;
use zed_extension_api::{self as zed, settings::LspSettings, LanguageServerId, Result};

pub struct LuaLs {
cached_binary_path: Option<String>,
}

impl LuaLs {
pub const SERVER_ID: &str = "lua-language-server";
const BINARY_NAME: &str = "lua-language-server";

pub fn new() -> Self {
Self {
cached_binary_path: None,
}
}

pub fn language_server_command(
&mut self,
language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> Result<zed::Command> {
let binary = LspSettings::for_worktree(language_server_id.as_ref(), worktree)?.binary;
let args = binary
.as_ref()
.and_then(|binary| binary.arguments.clone())
.unwrap_or_default();
let env = binary
.as_ref()
.and_then(|binary| binary.env.clone())
.map(|env| env.into_iter().collect())
.unwrap_or_default();
let configured_or_system_path = binary
.and_then(|binary| binary.path)
.or_else(|| worktree.which(Self::BINARY_NAME));
let command = match configured_or_system_path {
Some(path) => path,
None => self.zed_managed_binary_path(language_server_id)?,
};

Ok(zed::Command { command, args, env })
}

fn zed_managed_binary_path(&mut self, language_server_id: &LanguageServerId) -> Result<String> {
if let Some(path) = &self.cached_binary_path {
if fs::metadata(path).is_ok_and(|stat| stat.is_file()) {
return Ok(path.clone());
}
}

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}.{extension}",
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 => "x64",
zed::Architecture::X86 => return Err("unsupported platform x86".into()),
},
extension = match platform {
zed::Os::Mac | zed::Os::Linux => "tar.gz",
zed::Os::Windows => "zip",
},
);

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{extension}",
extension = match platform {
zed::Os::Mac | zed::Os::Linux => "",
zed::Os::Windows => ".exe",
},
);

if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) {
zed::set_language_server_installation_status(
language_server_id,
&zed::LanguageServerInstallationStatus::Downloading,
);

zed::download_file(
&asset.download_url,
&version_dir,
match platform {
zed::Os::Mac | zed::Os::Linux => zed::DownloadedFileType::GzipTar,
zed::Os::Windows => zed::DownloadedFileType::Zip,
},
)
.map_err(|e| format!("failed to download file: {e}"))?;

remove_old_server_versions("lua-language-server-", &version_dir)?;
}

self.cached_binary_path = Some(binary_path.clone());
Ok(binary_path)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn uses_its_own_server_id() {
assert_eq!(LuaLs::SERVER_ID, "lua-language-server");
}
}
Loading
Loading