1use std::fs;
2
3use zed::lsp::{Completion, CompletionKind, Symbol};
4use zed::{CodeLabel, CodeLabelSpan, LanguageServerId};
5use zed_extension_api::{self as zed, Result};
6
7pub struct Lexical {
8 cached_binary_path: Option<String>,
9}
10
11impl Lexical {
12 pub const LANGUAGE_SERVER_ID: &'static str = "lexical";
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("lexical") {
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 "lexical-lsp/lexical",
41 zed::GithubReleaseOptions {
42 require_assets: true,
43 pre_release: false,
44 },
45 )?;
46
47 let asset_name = format!("lexical-{version}.zip", version = release.version);
48
49 let asset = release
50 .assets
51 .iter()
52 .find(|asset| asset.name == asset_name)
53 .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?;
54
55 let version_dir = format!("lexical-{}", release.version);
56 let binary_path = format!("{version_dir}/lexical/bin/start_lexical.sh");
57
58 if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
59 zed::set_language_server_installation_status(
60 &language_server_id,
61 &zed::LanguageServerInstallationStatus::Downloading,
62 );
63
64 zed::download_file(
65 &asset.download_url,
66 &version_dir,
67 zed::DownloadedFileType::Zip,
68 )
69 .map_err(|e| format!("failed to download file: {e}"))?;
70
71 let entries =
72 fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
73 for entry in entries {
74 let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
75 if entry.file_name().to_str() != Some(&version_dir) {
76 fs::remove_dir_all(&entry.path()).ok();
77 }
78 }
79 }
80
81 self.cached_binary_path = Some(binary_path.clone());
82 Ok(binary_path)
83 }
84
85 pub fn label_for_completion(&self, completion: Completion) -> Option<CodeLabel> {
86 match completion.kind? {
87 CompletionKind::Module
88 | CompletionKind::Class
89 | CompletionKind::Interface
90 | CompletionKind::Struct => {
91 let name = completion.label;
92 let defmodule = "defmodule ";
93 let code = format!("{defmodule}{name}");
94
95 Some(CodeLabel {
96 code,
97 spans: vec![CodeLabelSpan::code_range(
98 defmodule.len()..defmodule.len() + name.len(),
99 )],
100 filter_range: (0..name.len()).into(),
101 })
102 }
103 CompletionKind::Function | CompletionKind::Constant => {
104 let name = completion.label;
105 let def = "def ";
106 let code = format!("{def}{name}");
107
108 Some(CodeLabel {
109 code,
110 spans: vec![CodeLabelSpan::code_range(def.len()..def.len() + name.len())],
111 filter_range: (0..name.len()).into(),
112 })
113 }
114 CompletionKind::Operator => {
115 let name = completion.label;
116 let def_a = "def a ";
117 let code = format!("{def_a}{name} b");
118
119 Some(CodeLabel {
120 code,
121 spans: vec![CodeLabelSpan::code_range(
122 def_a.len()..def_a.len() + name.len(),
123 )],
124 filter_range: (0..name.len()).into(),
125 })
126 }
127 _ => None,
128 }
129 }
130
131 pub fn label_for_symbol(&self, _symbol: Symbol) -> Option<CodeLabel> {
132 None
133 }
134}