1use anyhow::{anyhow, Context, Result};
2use async_trait::async_trait;
3use futures::StreamExt;
4pub use language::*;
5use lsp::LanguageServerBinary;
6use smol::fs::{self, File};
7use std::{any::Any, path::PathBuf, sync::Arc};
8use util::{
9 fs::remove_matching,
10 github::{latest_github_release, GitHubLspBinaryVersion},
11 ResultExt,
12};
13
14pub struct CLspAdapter;
15
16#[async_trait]
17impl super::LspAdapter for CLspAdapter {
18 async fn name(&self) -> LanguageServerName {
19 LanguageServerName("clangd".into())
20 }
21
22 fn short_name(&self) -> &'static str {
23 "clangd"
24 }
25
26 async fn fetch_latest_server_version(
27 &self,
28 delegate: &dyn LspAdapterDelegate,
29 ) -> Result<Box<dyn 'static + Send + Any>> {
30 let release = latest_github_release("clangd/clangd", false, delegate.http_client()).await?;
31 let asset_name = format!("clangd-mac-{}.zip", release.name);
32 let asset = release
33 .assets
34 .iter()
35 .find(|asset| asset.name == asset_name)
36 .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
37 let version = GitHubLspBinaryVersion {
38 name: release.name,
39 url: asset.browser_download_url.clone(),
40 };
41 Ok(Box::new(version) as Box<_>)
42 }
43
44 async fn fetch_server_binary(
45 &self,
46 version: Box<dyn 'static + Send + Any>,
47 container_dir: PathBuf,
48 delegate: &dyn LspAdapterDelegate,
49 ) -> Result<LanguageServerBinary> {
50 let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
51 let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
52 let version_dir = container_dir.join(format!("clangd_{}", version.name));
53 let binary_path = version_dir.join("bin/clangd");
54
55 if fs::metadata(&binary_path).await.is_err() {
56 let mut response = delegate
57 .http_client()
58 .get(&version.url, Default::default(), true)
59 .await
60 .context("error downloading release")?;
61 let mut file = File::create(&zip_path).await?;
62 if !response.status().is_success() {
63 Err(anyhow!(
64 "download failed with status {}",
65 response.status().to_string()
66 ))?;
67 }
68 futures::io::copy(response.body_mut(), &mut file).await?;
69
70 let unzip_status = smol::process::Command::new("unzip")
71 .current_dir(&container_dir)
72 .arg(&zip_path)
73 .output()
74 .await?
75 .status;
76 if !unzip_status.success() {
77 Err(anyhow!("failed to unzip clangd archive"))?;
78 }
79
80 remove_matching(&container_dir, |entry| entry != version_dir).await;
81 }
82
83 Ok(LanguageServerBinary {
84 path: binary_path,
85 arguments: vec![],
86 })
87 }
88
89 async fn cached_server_binary(
90 &self,
91 container_dir: PathBuf,
92 _: &dyn LspAdapterDelegate,
93 ) -> Option<LanguageServerBinary> {
94 get_cached_server_binary(container_dir).await
95 }
96
97 async fn installation_test_binary(
98 &self,
99 container_dir: PathBuf,
100 ) -> Option<LanguageServerBinary> {
101 get_cached_server_binary(container_dir)
102 .await
103 .map(|mut binary| {
104 binary.arguments = vec!["--help".into()];
105 binary
106 })
107 }
108
109 async fn label_for_completion(
110 &self,
111 completion: &lsp::CompletionItem,
112 language: &Arc<Language>,
113 ) -> Option<CodeLabel> {
114 let label = completion
115 .label
116 .strip_prefix('•')
117 .unwrap_or(&completion.label)
118 .trim();
119
120 match completion.kind {
121 Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
122 let detail = completion.detail.as_ref().unwrap();
123 let text = format!("{} {}", detail, label);
124 let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
125 let runs = language.highlight_text(&source, 11..11 + text.len());
126 return Some(CodeLabel {
127 filter_range: detail.len() + 1..text.len(),
128 text,
129 runs,
130 });
131 }
132 Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
133 if completion.detail.is_some() =>
134 {
135 let detail = completion.detail.as_ref().unwrap();
136 let text = format!("{} {}", detail, label);
137 let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
138 return Some(CodeLabel {
139 filter_range: detail.len() + 1..text.len(),
140 text,
141 runs,
142 });
143 }
144 Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
145 if completion.detail.is_some() =>
146 {
147 let detail = completion.detail.as_ref().unwrap();
148 let text = format!("{} {}", detail, label);
149 let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
150 return Some(CodeLabel {
151 filter_range: detail.len() + 1..text.rfind('(').unwrap_or(text.len()),
152 text,
153 runs,
154 });
155 }
156 Some(kind) => {
157 let highlight_name = match kind {
158 lsp::CompletionItemKind::STRUCT
159 | lsp::CompletionItemKind::INTERFACE
160 | lsp::CompletionItemKind::CLASS
161 | lsp::CompletionItemKind::ENUM => Some("type"),
162 lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
163 lsp::CompletionItemKind::KEYWORD => Some("keyword"),
164 lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
165 Some("constant")
166 }
167 _ => None,
168 };
169 if let Some(highlight_id) = language
170 .grammar()
171 .and_then(|g| g.highlight_id_for_name(highlight_name?))
172 {
173 let mut label = CodeLabel::plain(label.to_string(), None);
174 label.runs.push((
175 0..label.text.rfind('(').unwrap_or(label.text.len()),
176 highlight_id,
177 ));
178 return Some(label);
179 }
180 }
181 _ => {}
182 }
183 Some(CodeLabel::plain(label.to_string(), None))
184 }
185
186 async fn label_for_symbol(
187 &self,
188 name: &str,
189 kind: lsp::SymbolKind,
190 language: &Arc<Language>,
191 ) -> Option<CodeLabel> {
192 let (text, filter_range, display_range) = match kind {
193 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
194 let text = format!("void {} () {{}}", name);
195 let filter_range = 0..name.len();
196 let display_range = 5..5 + name.len();
197 (text, filter_range, display_range)
198 }
199 lsp::SymbolKind::STRUCT => {
200 let text = format!("struct {} {{}}", name);
201 let filter_range = 7..7 + name.len();
202 let display_range = 0..filter_range.end;
203 (text, filter_range, display_range)
204 }
205 lsp::SymbolKind::ENUM => {
206 let text = format!("enum {} {{}}", name);
207 let filter_range = 5..5 + name.len();
208 let display_range = 0..filter_range.end;
209 (text, filter_range, display_range)
210 }
211 lsp::SymbolKind::INTERFACE | lsp::SymbolKind::CLASS => {
212 let text = format!("class {} {{}}", name);
213 let filter_range = 6..6 + name.len();
214 let display_range = 0..filter_range.end;
215 (text, filter_range, display_range)
216 }
217 lsp::SymbolKind::CONSTANT => {
218 let text = format!("const int {} = 0;", name);
219 let filter_range = 10..10 + name.len();
220 let display_range = 0..filter_range.end;
221 (text, filter_range, display_range)
222 }
223 lsp::SymbolKind::MODULE => {
224 let text = format!("namespace {} {{}}", name);
225 let filter_range = 10..10 + name.len();
226 let display_range = 0..filter_range.end;
227 (text, filter_range, display_range)
228 }
229 lsp::SymbolKind::TYPE_PARAMETER => {
230 let text = format!("typename {} {{}};", name);
231 let filter_range = 9..9 + name.len();
232 let display_range = 0..filter_range.end;
233 (text, filter_range, display_range)
234 }
235 _ => return None,
236 };
237
238 Some(CodeLabel {
239 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
240 text: text[display_range].to_string(),
241 filter_range,
242 })
243 }
244}
245
246async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
247 (|| async move {
248 let mut last_clangd_dir = None;
249 let mut entries = fs::read_dir(&container_dir).await?;
250 while let Some(entry) = entries.next().await {
251 let entry = entry?;
252 if entry.file_type().await?.is_dir() {
253 last_clangd_dir = Some(entry.path());
254 }
255 }
256 let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
257 let clangd_bin = clangd_dir.join("bin/clangd");
258 if clangd_bin.exists() {
259 Ok(LanguageServerBinary {
260 path: clangd_bin,
261 arguments: vec![],
262 })
263 } else {
264 Err(anyhow!(
265 "missing clangd binary in directory {:?}",
266 clangd_dir
267 ))
268 }
269 })()
270 .await
271 .log_err()
272}
273
274#[cfg(test)]
275mod tests {
276 use gpui::{Context, TestAppContext};
277 use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
278 use settings::SettingsStore;
279 use std::num::NonZeroU32;
280
281 #[gpui::test]
282 async fn test_c_autoindent(cx: &mut TestAppContext) {
283 // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
284 cx.update(|cx| {
285 let test_settings = SettingsStore::test(cx);
286 cx.set_global(test_settings);
287 language::init(cx);
288 cx.update_global::<SettingsStore, _>(|store, cx| {
289 store.update_user_settings::<AllLanguageSettings>(cx, |s| {
290 s.defaults.tab_size = NonZeroU32::new(2);
291 });
292 });
293 });
294 let language = crate::languages::language("c", tree_sitter_c::language(), None).await;
295
296 cx.build_model(|cx| {
297 let mut buffer =
298 Buffer::new(0, cx.entity_id().as_u64(), "").with_language(language, cx);
299
300 // empty function
301 buffer.edit([(0..0, "int main() {}")], None, cx);
302
303 // indent inside braces
304 let ix = buffer.len() - 1;
305 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
306 assert_eq!(buffer.text(), "int main() {\n \n}");
307
308 // indent body of single-statement if statement
309 let ix = buffer.len() - 2;
310 buffer.edit([(ix..ix, "if (a)\nb;")], Some(AutoindentMode::EachLine), cx);
311 assert_eq!(buffer.text(), "int main() {\n if (a)\n b;\n}");
312
313 // indent inside field expression
314 let ix = buffer.len() - 3;
315 buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::EachLine), cx);
316 assert_eq!(buffer.text(), "int main() {\n if (a)\n b\n .c;\n}");
317
318 buffer
319 });
320 }
321}