1use std::{env, fs};
2use zed::{
3 serde_json::{self, Value},
4 settings::LspSettings,
5};
6use zed_extension_api::{self as zed, Result};
7
8const SERVER_PATH: &str = "node_modules/@elm-tooling/elm-language-server/out/node/index.js";
9const PACKAGE_NAME: &str = "@elm-tooling/elm-language-server";
10
11struct ElmExtension {
12 did_find_server: bool,
13}
14
15impl ElmExtension {
16 fn server_exists(&self) -> bool {
17 fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file())
18 }
19
20 fn server_script_path(&mut self, server_id: &zed::LanguageServerId) -> Result<String> {
21 let server_exists = self.server_exists();
22 if self.did_find_server && server_exists {
23 return Ok(SERVER_PATH.to_string());
24 }
25
26 zed::set_language_server_installation_status(
27 server_id,
28 &zed::LanguageServerInstallationStatus::CheckingForUpdate,
29 );
30 let version = zed::npm_package_latest_version(PACKAGE_NAME)?;
31
32 if !server_exists
33 || zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version)
34 {
35 zed::set_language_server_installation_status(
36 server_id,
37 &zed::LanguageServerInstallationStatus::Downloading,
38 );
39 let result = zed::npm_install_package(PACKAGE_NAME, &version);
40 match result {
41 Ok(()) => {
42 if !self.server_exists() {
43 Err(format!(
44 "installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'",
45 ))?;
46 }
47 }
48 Err(error) => {
49 if !self.server_exists() {
50 Err(error)?;
51 }
52 }
53 }
54 }
55
56 self.did_find_server = true;
57 Ok(SERVER_PATH.to_string())
58 }
59}
60
61impl zed::Extension for ElmExtension {
62 fn new() -> Self {
63 Self {
64 did_find_server: false,
65 }
66 }
67
68 fn language_server_command(
69 &mut self,
70 server_id: &zed::LanguageServerId,
71 _worktree: &zed::Worktree,
72 ) -> Result<zed::Command> {
73 let server_path = self.server_script_path(server_id)?;
74 Ok(zed::Command {
75 command: zed::node_binary_path()?,
76 args: vec![
77 env::current_dir()
78 .unwrap()
79 .join(&server_path)
80 .to_string_lossy()
81 .to_string(),
82 "--stdio".to_string(),
83 ],
84 env: Default::default(),
85 })
86 }
87
88 fn language_server_workspace_configuration(
89 &mut self,
90 server_id: &zed::LanguageServerId,
91 worktree: &zed::Worktree,
92 ) -> Result<Option<Value>> {
93 // elm-language-server expects workspace didChangeConfiguration notification
94 // params to be the same as lsp initialization_options
95 let initialization_options = LspSettings::for_worktree(server_id.as_ref(), worktree)?
96 .initialization_options
97 .clone()
98 .unwrap_or_default();
99
100 Ok(Some(match initialization_options.clone().as_object_mut() {
101 Some(op) => {
102 // elm-language-server requests workspace configuration
103 // for the `elmLS` section, so we have to nest
104 // another copy of initialization_options there
105 op.insert("elmLS".into(), initialization_options);
106 serde_json::to_value(op).unwrap_or_default()
107 }
108 None => initialization_options,
109 }))
110 }
111}
112
113zed::register_extension!(ElmExtension);