1use std::fs;
2
3use zed::lsp::{Completion, CompletionKind, Symbol, SymbolKind};
4use zed::{CodeLabel, CodeLabelSpan, LanguageServerId};
5use zed_extension_api::{self as zed, Result};
6
7pub struct NextLs {
8 cached_binary_path: Option<String>,
9}
10
11impl NextLs {
12 pub const LANGUAGE_SERVER_ID: &'static str = "next-ls";
13
14 pub fn new() -> Self {
15 Self {
16 cached_binary_path: None,
17 }
18 }
19
20 pub fn language_server_binary_path(
21 &mut self,
22 language_server_id: &LanguageServerId,
23 worktree: &zed::Worktree,
24 ) -> Result<String> {
25 if let Some(path) = worktree.which("nextls") {
26 return Ok(path);
27 }
28
29 if let Some(path) = &self.cached_binary_path {
30 if fs::metadata(path).map_or(false, |stat| stat.is_file()) {
31 return Ok(path.clone());
32 }
33 }
34
35 zed::set_language_server_installation_status(
36 language_server_id,
37 &zed::LanguageServerInstallationStatus::CheckingForUpdate,
38 );
39 let release = zed::latest_github_release(
40 "elixir-tools/next-ls",
41 zed::GithubReleaseOptions {
42 require_assets: true,
43 pre_release: false,
44 },
45 )?;
46
47 let (platform, arch) = zed::current_platform();
48 let asset_name = format!(
49 "next_ls_{os}_{arch}{extension}",
50 os = match platform {
51 zed::Os::Mac => "darwin",
52 zed::Os::Linux => "linux",
53 zed::Os::Windows => "windows",
54 },
55 arch = match arch {
56 zed::Architecture::Aarch64 => "arm64",
57 zed::Architecture::X8664 => "amd64",
58 zed::Architecture::X86 =>
59 return Err(format!("unsupported architecture: {arch:?}")),
60 },
61 extension = match platform {
62 zed::Os::Mac | zed::Os::Linux => "",
63 zed::Os::Windows => ".exe",
64 }
65 );
66
67 let asset = release
68 .assets
69 .iter()
70 .find(|asset| asset.name == asset_name)
71 .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?;
72
73 let version_dir = format!("next-ls-{}", release.version);
74 fs::create_dir_all(&version_dir).map_err(|e| format!("failed to create directory: {e}"))?;
75
76 let binary_path = format!("{version_dir}/next-ls");
77
78 if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
79 zed::set_language_server_installation_status(
80 language_server_id,
81 &zed::LanguageServerInstallationStatus::Downloading,
82 );
83
84 zed::download_file(
85 &asset.download_url,
86 &binary_path,
87 zed::DownloadedFileType::Uncompressed,
88 )
89 .map_err(|e| format!("failed to download file: {e}"))?;
90
91 zed::make_file_executable(&binary_path)?;
92
93 let entries =
94 fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
95 for entry in entries {
96 let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
97 if entry.file_name().to_str() != Some(&version_dir) {
98 fs::remove_dir_all(entry.path()).ok();
99 }
100 }
101 }
102
103 self.cached_binary_path = Some(binary_path.clone());
104 Ok(binary_path)
105 }
106
107 pub fn label_for_completion(&self, completion: Completion) -> Option<CodeLabel> {
108 match completion.kind? {
109 CompletionKind::Module
110 | CompletionKind::Class
111 | CompletionKind::Interface
112 | CompletionKind::Struct => {
113 let name = completion.label;
114 let defmodule = "defmodule ";
115 let code = format!("{defmodule}{name}");
116
117 Some(CodeLabel {
118 code,
119 spans: vec![CodeLabelSpan::code_range(
120 defmodule.len()..defmodule.len() + name.len(),
121 )],
122 filter_range: (0..name.len()).into(),
123 })
124 }
125 CompletionKind::Function | CompletionKind::Constant => {
126 let name = completion.label;
127 let def = "def ";
128 let code = format!("{def}{name}");
129
130 Some(CodeLabel {
131 code,
132 spans: vec![CodeLabelSpan::code_range(def.len()..def.len() + name.len())],
133 filter_range: (0..name.len()).into(),
134 })
135 }
136 CompletionKind::Operator => {
137 let name = completion.label;
138 let def_a = "def a ";
139 let code = format!("{def_a}{name} b");
140
141 Some(CodeLabel {
142 code,
143 spans: vec![CodeLabelSpan::code_range(
144 def_a.len()..def_a.len() + name.len(),
145 )],
146 filter_range: (0..name.len()).into(),
147 })
148 }
149 _ => None,
150 }
151 }
152
153 pub fn label_for_symbol(&self, symbol: Symbol) -> Option<CodeLabel> {
154 let name = &symbol.name;
155
156 let (code, filter_range, display_range) = match symbol.kind {
157 SymbolKind::Module | SymbolKind::Class | SymbolKind::Interface | SymbolKind::Struct => {
158 let defmodule = "defmodule ";
159 let code = format!("{defmodule}{name}");
160 let filter_range = 0..name.len();
161 let display_range = defmodule.len()..defmodule.len() + name.len();
162 (code, filter_range, display_range)
163 }
164 SymbolKind::Function | SymbolKind::Constant => {
165 let def = "def ";
166 let code = format!("{def}{name}");
167 let filter_range = 0..name.len();
168 let display_range = def.len()..def.len() + name.len();
169 (code, filter_range, display_range)
170 }
171 _ => return None,
172 };
173
174 Some(CodeLabel {
175 spans: vec![CodeLabelSpan::code_range(display_range)],
176 filter_range: filter_range.into(),
177 code,
178 })
179 }
180}