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