Skip to content
Merged
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
13 changes: 13 additions & 0 deletions extension.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ languages = [
"EEx" = "eex"
"HEEx" = "heex"

[language_servers.dexter]
name = "Dexter"
languages = [
"Elixir",
"EEx",
"HEEx",
]

[language_servers.dexter.language_ids]
"Elixir" = "elixir"
"EEx" = "eex"
"HEEx" = "heex"

[language_servers.next-ls]
name = "Next LS"
languages = [
Expand Down
18 changes: 17 additions & 1 deletion src/elixir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ use zed_extension_api::{
serde_json::Value,
};

use crate::language_servers::{ElixirLs, Expert, Lexical, NextLs};
use crate::language_servers::{Dexter, ElixirLs, Expert, Lexical, NextLs};

struct ElixirExtension {
expert: Option<Expert>,
elixir_ls: Option<ElixirLs>,
dexter: Option<Dexter>,
next_ls: Option<NextLs>,
lexical: Option<Lexical>,
}
Expand All @@ -20,6 +21,7 @@ impl zed::Extension for ElixirExtension {
Self {
expert: None,
elixir_ls: None,
dexter: None,
next_ls: None,
lexical: None,
}
Expand All @@ -39,6 +41,10 @@ impl zed::Extension for ElixirExtension {
.elixir_ls
.get_or_insert_with(ElixirLs::new)
.language_server_command(language_server_id, worktree),
Dexter::LANGUAGE_SERVER_ID => self
.dexter
.get_or_insert_with(Dexter::new)
.language_server_command(language_server_id, worktree),
NextLs::LANGUAGE_SERVER_ID => self
.next_ls
.get_or_insert_with(NextLs::new)
Expand All @@ -65,6 +71,10 @@ impl zed::Extension for ElixirExtension {
.elixir_ls
.get_or_insert_with(ElixirLs::new)
.language_server_initialization_options(worktree),
Dexter::LANGUAGE_SERVER_ID => self
.dexter
.get_or_insert_with(Dexter::new)
.language_server_initialization_options(worktree),
NextLs::LANGUAGE_SERVER_ID => self
.next_ls
.get_or_insert_with(NextLs::new)
Expand All @@ -91,6 +101,10 @@ impl zed::Extension for ElixirExtension {
.elixir_ls
.get_or_insert_with(ElixirLs::new)
.language_server_workspace_configuration(worktree),
Dexter::LANGUAGE_SERVER_ID => self
.dexter
.get_or_insert_with(Dexter::new)
.language_server_workspace_configuration(worktree),
NextLs::LANGUAGE_SERVER_ID => self
.next_ls
.get_or_insert_with(NextLs::new)
Expand All @@ -113,6 +127,7 @@ impl zed::Extension for ElixirExtension {
ElixirLs::LANGUAGE_SERVER_ID => {
self.elixir_ls.as_ref()?.label_for_completion(completion)
}
Dexter::LANGUAGE_SERVER_ID => self.dexter.as_ref()?.label_for_completion(completion),
NextLs::LANGUAGE_SERVER_ID => self.next_ls.as_ref()?.label_for_completion(completion),
Lexical::LANGUAGE_SERVER_ID => self.lexical.as_ref()?.label_for_completion(completion),
_ => None,
Expand All @@ -127,6 +142,7 @@ impl zed::Extension for ElixirExtension {
match language_server_id.as_ref() {
Expert::LANGUAGE_SERVER_ID => self.expert.as_ref()?.label_for_symbol(symbol),
ElixirLs::LANGUAGE_SERVER_ID => self.elixir_ls.as_ref()?.label_for_symbol(symbol),
Dexter::LANGUAGE_SERVER_ID => self.dexter.as_ref()?.label_for_symbol(symbol),
NextLs::LANGUAGE_SERVER_ID => self.next_ls.as_ref()?.label_for_symbol(symbol),
Lexical::LANGUAGE_SERVER_ID => self.lexical.as_ref()?.label_for_symbol(symbol),
_ => None,
Expand Down
2 changes: 2 additions & 0 deletions src/language_servers.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
mod config;
mod dexter;
mod elixir_ls;
mod expert;
mod lexical;
mod next_ls;
mod util;

pub use dexter::*;
pub use elixir_ls::*;
pub use expert::*;
pub use lexical::*;
Expand Down
236 changes: 236 additions & 0 deletions src/language_servers/dexter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
use std::fs;

use zed_extension_api::{
self as zed, CodeLabel, CodeLabelSpan, LanguageServerId, Result, Worktree,
lsp::{Completion, CompletionKind, Symbol, SymbolKind},
serde_json::{Value, json},
};

use crate::language_servers::{config, util};

struct DexterBinary {
path: String,
args: Vec<String>,
}

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

impl Dexter {
pub const LANGUAGE_SERVER_ID: &'static str = "dexter";

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

pub fn language_server_command(
&mut self,
language_server_id: &LanguageServerId,
worktree: &Worktree,
) -> Result<zed::Command> {
let dexter = self.language_server_binary(language_server_id, worktree)?;

Ok(zed::Command {
command: dexter.path,
args: dexter.args,
env: Default::default(),
})
}

fn language_server_binary(
&mut self,
language_server_id: &LanguageServerId,
worktree: &Worktree,
) -> Result<DexterBinary> {
let (platform, arch) = zed::current_platform();
let archive_name = format!(
"{}_{os}_{arch}",
Self::LANGUAGE_SERVER_ID,
os = match platform {
zed::Os::Mac => "Darwin",
zed::Os::Linux => "Linux",
zed::Os::Windows => return Err(format!("unsupported platform: {platform:?}")),
},
arch = match arch {
zed::Architecture::Aarch64 => "arm64",
zed::Architecture::X8664 => "x86_64",
zed::Architecture::X86 =>
return Err(format!("unsupported architecture: {arch:?}")),
},
);

let binary_name = format!("{}/{}", archive_name, Self::LANGUAGE_SERVER_ID);
let binary_settings = config::get_binary_settings(Self::LANGUAGE_SERVER_ID, worktree);
Comment thread
flowerett marked this conversation as resolved.
let binary_args =
config::get_binary_args(&binary_settings).unwrap_or_else(|| vec!["lsp".to_string()]);

if let Some(binary_path) = config::get_binary_path(&binary_settings) {
return Ok(DexterBinary {
path: binary_path,
args: binary_args,
});
}

if let Some(binary_path) = worktree.which(Self::LANGUAGE_SERVER_ID) {
return Ok(DexterBinary {
path: binary_path,
args: binary_args,
});
}

if let Some(binary_path) = &self.cached_binary_path
&& fs::metadata(binary_path).is_ok_and(|stat| stat.is_file())
{
return Ok(DexterBinary {
path: binary_path.clone(),
args: binary_args,
});
}

zed::set_language_server_installation_status(
language_server_id,
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
);

let release = match zed::latest_github_release(
"remoteoss/dexter",
zed::GithubReleaseOptions {
require_assets: true,
pre_release: false,
},
) {
Ok(release) => release,
Err(_) => {
if let Some(binary_path) =
util::find_existing_binary(Self::LANGUAGE_SERVER_ID, &binary_name)
{
self.cached_binary_path = Some(binary_path.clone());
return Ok(DexterBinary {
path: binary_path,
args: binary_args,
});
}
return Err("failed to download latest github release".to_string());
}
};

let asset_name = format!("{archive_name}.tar.gz");
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!("{}-{}", Self::LANGUAGE_SERVER_ID, release.version);
let binary_path = format!("{}/{}", version_dir, binary_name);

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,
zed::DownloadedFileType::GzipTar,
)
.map_err(|e| format!("failed to download file: {e}"))?;

zed::make_file_executable(&binary_path)?;

util::remove_outdated_versions(Self::LANGUAGE_SERVER_ID, &version_dir)?;
}

self.cached_binary_path = Some(binary_path.clone());
Ok(DexterBinary {
path: binary_path,
args: binary_args,
})
}

pub fn language_server_initialization_options(
&mut self,
worktree: &Worktree,
) -> Result<Option<Value>> {
let settings = config::get_initialization_options(Self::LANGUAGE_SERVER_ID, worktree)
.unwrap_or_else(|| {
json!({
"followDelegates": true
})
});

Ok(Some(settings))
}

pub fn language_server_workspace_configuration(
&mut self,
worktree: &Worktree,
) -> Result<Option<Value>> {
let settings = config::get_workspace_configuration(Self::LANGUAGE_SERVER_ID, worktree)
.unwrap_or_default();

Ok(Some(settings))
}

pub fn label_for_completion(&self, completion: Completion) -> Option<CodeLabel> {
match completion.kind? {
CompletionKind::Module | CompletionKind::Class => {
let name = completion.label;
let defmodule = "defmodule ";
let code = format!("{defmodule}{name}");

Some(CodeLabel {
code,
spans: vec![CodeLabelSpan::code_range(
defmodule.len()..defmodule.len() + name.len(),
)],
filter_range: (0..name.len()).into(),
})
}
CompletionKind::Function | CompletionKind::Constant => {
let name = completion.label;
let def = "def ";
let code = format!("{def}{name}");

Some(CodeLabel {
code,
spans: vec![CodeLabelSpan::code_range(def.len()..def.len() + name.len())],
filter_range: (0..name.len()).into(),
})
}
_ => None,
}
}

pub fn label_for_symbol(&self, symbol: Symbol) -> Option<CodeLabel> {
let name = &symbol.name;

let (code, filter_range, display_range) = match symbol.kind {
SymbolKind::Module | SymbolKind::Interface | SymbolKind::Struct => {
let defmodule = "defmodule ";
let code = format!("{defmodule}{name}");
let filter_range = 0..name.len();
let display_range = defmodule.len()..defmodule.len() + name.len();
(code, filter_range, display_range)
}
SymbolKind::Function | SymbolKind::Constant => {
let def = "def ";
let code = format!("{def}{name}");
let filter_range = 0..name.len();
let display_range = def.len()..def.len() + name.len();
(code, filter_range, display_range)
}
_ => return None,
};

Some(CodeLabel {
spans: vec![CodeLabelSpan::code_range(display_range)],
filter_range: filter_range.into(),
code,
})
}
}
Loading