1use anyhow::{Context, Result, anyhow, bail};
2use async_trait::async_trait;
3use futures::StreamExt;
4use gpui::{App, AsyncApp};
5use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
6pub use language::*;
7use lsp::{DiagnosticTag, InitializeParams, LanguageServerBinary, LanguageServerName};
8use project::lsp_store::clangd_ext;
9use serde_json::json;
10use smol::fs::{self, File};
11use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
12use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into};
13
14pub struct CLspAdapter;
15
16impl CLspAdapter {
17 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("clangd");
18}
19
20#[async_trait(?Send)]
21impl super::LspAdapter for CLspAdapter {
22 fn name(&self) -> LanguageServerName {
23 Self::SERVER_NAME.clone()
24 }
25
26 async fn check_if_user_installed(
27 &self,
28 delegate: &dyn LspAdapterDelegate,
29 _: Arc<dyn LanguageToolchainStore>,
30 _: &AsyncApp,
31 ) -> Option<LanguageServerBinary> {
32 let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
33 Some(LanguageServerBinary {
34 path,
35 arguments: vec![],
36 env: None,
37 })
38 }
39
40 async fn fetch_latest_server_version(
41 &self,
42 delegate: &dyn LspAdapterDelegate,
43 ) -> Result<Box<dyn 'static + Send + Any>> {
44 let release =
45 latest_github_release("clangd/clangd", true, false, delegate.http_client()).await?;
46 let os_suffix = match consts::OS {
47 "macos" => "mac",
48 "linux" => "linux",
49 "windows" => "windows",
50 other => bail!("Running on unsupported os: {other}"),
51 };
52 let asset_name = format!("clangd-{}-{}.zip", os_suffix, release.tag_name);
53 let asset = release
54 .assets
55 .iter()
56 .find(|asset| asset.name == asset_name)
57 .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
58 let version = GitHubLspBinaryVersion {
59 name: release.tag_name,
60 url: asset.browser_download_url.clone(),
61 };
62 Ok(Box::new(version) as Box<_>)
63 }
64
65 async fn fetch_server_binary(
66 &self,
67 version: Box<dyn 'static + Send + Any>,
68 container_dir: PathBuf,
69 delegate: &dyn LspAdapterDelegate,
70 ) -> Result<LanguageServerBinary> {
71 let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
72 let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
73 let version_dir = container_dir.join(format!("clangd_{}", version.name));
74 let binary_path = version_dir.join("bin/clangd");
75
76 if fs::metadata(&binary_path).await.is_err() {
77 let mut response = delegate
78 .http_client()
79 .get(&version.url, Default::default(), true)
80 .await
81 .context("error downloading release")?;
82 let mut file = File::create(&zip_path).await?;
83 if !response.status().is_success() {
84 Err(anyhow!(
85 "download failed with status {}",
86 response.status().to_string()
87 ))?;
88 }
89 futures::io::copy(response.body_mut(), &mut file).await?;
90
91 let unzip_status = util::command::new_smol_command("unzip")
92 .current_dir(&container_dir)
93 .arg(&zip_path)
94 .output()
95 .await?
96 .status;
97 if !unzip_status.success() {
98 Err(anyhow!("failed to unzip clangd archive"))?;
99 }
100
101 remove_matching(&container_dir, |entry| entry != version_dir).await;
102 }
103
104 Ok(LanguageServerBinary {
105 path: binary_path,
106 env: None,
107 arguments: vec![],
108 })
109 }
110
111 async fn cached_server_binary(
112 &self,
113 container_dir: PathBuf,
114 _: &dyn LspAdapterDelegate,
115 ) -> Option<LanguageServerBinary> {
116 get_cached_server_binary(container_dir).await
117 }
118
119 async fn label_for_completion(
120 &self,
121 completion: &lsp::CompletionItem,
122 language: &Arc<Language>,
123 ) -> Option<CodeLabel> {
124 let label_detail = match &completion.label_details {
125 Some(label_detail) => match &label_detail.detail {
126 Some(detail) => detail.trim(),
127 None => "",
128 },
129 None => "",
130 };
131
132 let label = completion
133 .label
134 .strip_prefix('•')
135 .unwrap_or(&completion.label)
136 .trim()
137 .to_owned()
138 + label_detail;
139
140 match completion.kind {
141 Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
142 let detail = completion.detail.as_ref().unwrap();
143 let text = format!("{} {}", detail, label);
144 let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
145 let runs = language.highlight_text(&source, 11..11 + text.len());
146 return Some(CodeLabel {
147 filter_range: detail.len() + 1..text.len(),
148 text,
149 runs,
150 });
151 }
152 Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
153 if completion.detail.is_some() =>
154 {
155 let detail = completion.detail.as_ref().unwrap();
156 let text = format!("{} {}", detail, label);
157 let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
158 return Some(CodeLabel {
159 filter_range: detail.len() + 1..text.len(),
160 text,
161 runs,
162 });
163 }
164 Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
165 if completion.detail.is_some() =>
166 {
167 let detail = completion.detail.as_ref().unwrap();
168 let text = format!("{} {}", detail, label);
169 let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
170 let filter_start = detail.len() + 1;
171 let filter_end =
172 if let Some(end) = text.rfind('(').filter(|end| *end > filter_start) {
173 end
174 } else {
175 text.len()
176 };
177
178 return Some(CodeLabel {
179 filter_range: filter_start..filter_end,
180 text,
181 runs,
182 });
183 }
184 Some(kind) => {
185 let highlight_name = match kind {
186 lsp::CompletionItemKind::STRUCT
187 | lsp::CompletionItemKind::INTERFACE
188 | lsp::CompletionItemKind::CLASS
189 | lsp::CompletionItemKind::ENUM => Some("type"),
190 lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
191 lsp::CompletionItemKind::KEYWORD => Some("keyword"),
192 lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
193 Some("constant")
194 }
195 _ => None,
196 };
197 if let Some(highlight_id) = language
198 .grammar()
199 .and_then(|g| g.highlight_id_for_name(highlight_name?))
200 {
201 let mut label = CodeLabel::plain(label.to_string(), None);
202 label.runs.push((
203 0..label.text.rfind('(').unwrap_or(label.text.len()),
204 highlight_id,
205 ));
206 return Some(label);
207 }
208 }
209 _ => {}
210 }
211 Some(CodeLabel::plain(label.to_string(), None))
212 }
213
214 async fn label_for_symbol(
215 &self,
216 name: &str,
217 kind: lsp::SymbolKind,
218 language: &Arc<Language>,
219 ) -> Option<CodeLabel> {
220 let (text, filter_range, display_range) = match kind {
221 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
222 let text = format!("void {} () {{}}", name);
223 let filter_range = 0..name.len();
224 let display_range = 5..5 + name.len();
225 (text, filter_range, display_range)
226 }
227 lsp::SymbolKind::STRUCT => {
228 let text = format!("struct {} {{}}", name);
229 let filter_range = 7..7 + name.len();
230 let display_range = 0..filter_range.end;
231 (text, filter_range, display_range)
232 }
233 lsp::SymbolKind::ENUM => {
234 let text = format!("enum {} {{}}", name);
235 let filter_range = 5..5 + name.len();
236 let display_range = 0..filter_range.end;
237 (text, filter_range, display_range)
238 }
239 lsp::SymbolKind::INTERFACE | lsp::SymbolKind::CLASS => {
240 let text = format!("class {} {{}}", name);
241 let filter_range = 6..6 + name.len();
242 let display_range = 0..filter_range.end;
243 (text, filter_range, display_range)
244 }
245 lsp::SymbolKind::CONSTANT => {
246 let text = format!("const int {} = 0;", name);
247 let filter_range = 10..10 + name.len();
248 let display_range = 0..filter_range.end;
249 (text, filter_range, display_range)
250 }
251 lsp::SymbolKind::MODULE => {
252 let text = format!("namespace {} {{}}", name);
253 let filter_range = 10..10 + name.len();
254 let display_range = 0..filter_range.end;
255 (text, filter_range, display_range)
256 }
257 lsp::SymbolKind::TYPE_PARAMETER => {
258 let text = format!("typename {} {{}};", name);
259 let filter_range = 9..9 + name.len();
260 let display_range = 0..filter_range.end;
261 (text, filter_range, display_range)
262 }
263 _ => return None,
264 };
265
266 Some(CodeLabel {
267 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
268 text: text[display_range].to_string(),
269 filter_range,
270 })
271 }
272
273 fn prepare_initialize_params(
274 &self,
275 mut original: InitializeParams,
276 _: &App,
277 ) -> Result<InitializeParams> {
278 let experimental = json!({
279 "textDocument": {
280 "completion" : {
281 // enable clangd's dot-to-arrow feature.
282 "editsNearCursor": true
283 },
284 "inactiveRegionsCapabilities": {
285 "inactiveRegions": true,
286 }
287 }
288 });
289 if let Some(ref mut original_experimental) = original.capabilities.experimental {
290 merge_json_value_into(experimental, original_experimental);
291 } else {
292 original.capabilities.experimental = Some(experimental);
293 }
294 Ok(original)
295 }
296
297 fn process_diagnostics(
298 &self,
299 params: &mut lsp::PublishDiagnosticsParams,
300 server_id: LanguageServerId,
301 buffer: Option<&'_ Buffer>,
302 ) {
303 if let Some(buffer) = buffer {
304 let snapshot = buffer.snapshot();
305 let inactive_regions = buffer
306 .get_diagnostics(server_id)
307 .into_iter()
308 .flat_map(|v| v.iter())
309 .filter(|diag| clangd_ext::is_inactive_region(&diag.diagnostic))
310 .map(move |diag| {
311 let range =
312 language::range_to_lsp(diag.range.to_point_utf16(&snapshot)).unwrap();
313 let mut tags = vec![];
314 if diag.diagnostic.is_unnecessary {
315 tags.push(DiagnosticTag::UNNECESSARY);
316 }
317 lsp::Diagnostic {
318 range,
319 severity: Some(diag.diagnostic.severity),
320 source: diag.diagnostic.source.clone(),
321 tags: Some(tags),
322 message: diag.diagnostic.message.clone(),
323 code: diag.diagnostic.code.clone(),
324 ..Default::default()
325 }
326 });
327 params.diagnostics.extend(inactive_regions);
328 }
329 }
330}
331
332async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
333 maybe!(async {
334 let mut last_clangd_dir = None;
335 let mut entries = fs::read_dir(&container_dir).await?;
336 while let Some(entry) = entries.next().await {
337 let entry = entry?;
338 if entry.file_type().await?.is_dir() {
339 last_clangd_dir = Some(entry.path());
340 }
341 }
342 let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
343 let clangd_bin = clangd_dir.join("bin/clangd");
344 if clangd_bin.exists() {
345 Ok(LanguageServerBinary {
346 path: clangd_bin,
347 env: None,
348 arguments: vec![],
349 })
350 } else {
351 Err(anyhow!(
352 "missing clangd binary in directory {:?}",
353 clangd_dir
354 ))
355 }
356 })
357 .await
358 .log_err()
359}
360
361#[cfg(test)]
362mod tests {
363 use gpui::{AppContext as _, BorrowAppContext, TestAppContext};
364 use language::{AutoindentMode, Buffer, language_settings::AllLanguageSettings};
365 use settings::SettingsStore;
366 use std::num::NonZeroU32;
367
368 #[gpui::test]
369 async fn test_c_autoindent(cx: &mut TestAppContext) {
370 // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
371 cx.update(|cx| {
372 let test_settings = SettingsStore::test(cx);
373 cx.set_global(test_settings);
374 language::init(cx);
375 cx.update_global::<SettingsStore, _>(|store, cx| {
376 store.update_user_settings::<AllLanguageSettings>(cx, |s| {
377 s.defaults.tab_size = NonZeroU32::new(2);
378 });
379 });
380 });
381 let language = crate::language("c", tree_sitter_c::LANGUAGE.into());
382
383 cx.new(|cx| {
384 let mut buffer = Buffer::local("", cx).with_language(language, cx);
385
386 // empty function
387 buffer.edit([(0..0, "int main() {}")], None, cx);
388
389 // indent inside braces
390 let ix = buffer.len() - 1;
391 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
392 assert_eq!(buffer.text(), "int main() {\n \n}");
393
394 // indent body of single-statement if statement
395 let ix = buffer.len() - 2;
396 buffer.edit([(ix..ix, "if (a)\nb;")], Some(AutoindentMode::EachLine), cx);
397 assert_eq!(buffer.text(), "int main() {\n if (a)\n b;\n}");
398
399 // indent inside field expression
400 let ix = buffer.len() - 3;
401 buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::EachLine), cx);
402 assert_eq!(buffer.text(), "int main() {\n if (a)\n b\n .c;\n}");
403
404 buffer
405 });
406 }
407}