1use anyhow::{Context as _, Result, 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::{InitializeParams, LanguageServerBinary, LanguageServerName};
8use project::lsp_store::clangd_ext;
9use serde_json::json;
10use smol::fs;
11use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
12use util::{ResultExt, archive::extract_zip, 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::new(),
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 .with_context(|| format!("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 version_dir = container_dir.join(format!("clangd_{}", version.name));
73 let binary_path = version_dir.join("bin/clangd");
74
75 if fs::metadata(&binary_path).await.is_err() {
76 let mut response = delegate
77 .http_client()
78 .get(&version.url, Default::default(), true)
79 .await
80 .context("error downloading release")?;
81 anyhow::ensure!(
82 response.status().is_success(),
83 "download failed with status {}",
84 response.status().to_string()
85 );
86 extract_zip(&container_dir, response.body_mut())
87 .await
88 .with_context(|| format!("unzipping clangd archive to {container_dir:?}"))?;
89 remove_matching(&container_dir, |entry| entry != version_dir).await;
90 }
91
92 Ok(LanguageServerBinary {
93 path: binary_path,
94 env: None,
95 arguments: Vec::new(),
96 })
97 }
98
99 async fn cached_server_binary(
100 &self,
101 container_dir: PathBuf,
102 _: &dyn LspAdapterDelegate,
103 ) -> Option<LanguageServerBinary> {
104 get_cached_server_binary(container_dir).await
105 }
106
107 async fn label_for_completion(
108 &self,
109 completion: &lsp::CompletionItem,
110 language: &Arc<Language>,
111 ) -> Option<CodeLabel> {
112 let label_detail = match &completion.label_details {
113 Some(label_detail) => match &label_detail.detail {
114 Some(detail) => detail.trim(),
115 None => "",
116 },
117 None => "",
118 };
119
120 let label = completion
121 .label
122 .strip_prefix('•')
123 .unwrap_or(&completion.label)
124 .trim()
125 .to_owned()
126 + label_detail;
127
128 match completion.kind {
129 Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
130 let detail = completion.detail.as_ref().unwrap();
131 let text = format!("{} {}", detail, label);
132 let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
133 let runs = language.highlight_text(&source, 11..11 + text.len());
134 let filter_range = completion
135 .filter_text
136 .as_deref()
137 .and_then(|filter_text| {
138 text.find(filter_text)
139 .map(|start| start..start + filter_text.len())
140 })
141 .unwrap_or(detail.len() + 1..text.len());
142 return Some(CodeLabel {
143 filter_range,
144 text,
145 runs,
146 });
147 }
148 Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
149 if completion.detail.is_some() =>
150 {
151 let detail = completion.detail.as_ref().unwrap();
152 let text = format!("{} {}", detail, label);
153 let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
154 let filter_range = completion
155 .filter_text
156 .as_deref()
157 .and_then(|filter_text| {
158 text.find(filter_text)
159 .map(|start| start..start + filter_text.len())
160 })
161 .unwrap_or(detail.len() + 1..text.len());
162 return Some(CodeLabel {
163 filter_range,
164 text,
165 runs,
166 });
167 }
168 Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
169 if completion.detail.is_some() =>
170 {
171 let detail = completion.detail.as_ref().unwrap();
172 let text = format!("{} {}", detail, label);
173 let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
174 let filter_range = completion
175 .filter_text
176 .as_deref()
177 .and_then(|filter_text| {
178 text.find(filter_text)
179 .map(|start| start..start + filter_text.len())
180 })
181 .unwrap_or_else(|| {
182 let filter_start = detail.len() + 1;
183 let filter_end = text
184 .rfind('(')
185 .filter(|end| *end > filter_start)
186 .unwrap_or(text.len());
187 filter_start..filter_end
188 });
189
190 return Some(CodeLabel {
191 filter_range,
192 text,
193 runs,
194 });
195 }
196 Some(kind) => {
197 let highlight_name = match kind {
198 lsp::CompletionItemKind::STRUCT
199 | lsp::CompletionItemKind::INTERFACE
200 | lsp::CompletionItemKind::CLASS
201 | lsp::CompletionItemKind::ENUM => Some("type"),
202 lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
203 lsp::CompletionItemKind::KEYWORD => Some("keyword"),
204 lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
205 Some("constant")
206 }
207 _ => None,
208 };
209 if let Some(highlight_id) = language
210 .grammar()
211 .and_then(|g| g.highlight_id_for_name(highlight_name?))
212 {
213 let mut label =
214 CodeLabel::plain(label.to_string(), completion.filter_text.as_deref());
215 label.runs.push((
216 0..label.text.rfind('(').unwrap_or(label.text.len()),
217 highlight_id,
218 ));
219 return Some(label);
220 }
221 }
222 _ => {}
223 }
224 Some(CodeLabel::plain(
225 label.to_string(),
226 completion.filter_text.as_deref(),
227 ))
228 }
229
230 async fn label_for_symbol(
231 &self,
232 name: &str,
233 kind: lsp::SymbolKind,
234 language: &Arc<Language>,
235 ) -> Option<CodeLabel> {
236 let (text, filter_range, display_range) = match kind {
237 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
238 let text = format!("void {} () {{}}", name);
239 let filter_range = 0..name.len();
240 let display_range = 5..5 + name.len();
241 (text, filter_range, display_range)
242 }
243 lsp::SymbolKind::STRUCT => {
244 let text = format!("struct {} {{}}", name);
245 let filter_range = 7..7 + name.len();
246 let display_range = 0..filter_range.end;
247 (text, filter_range, display_range)
248 }
249 lsp::SymbolKind::ENUM => {
250 let text = format!("enum {} {{}}", name);
251 let filter_range = 5..5 + name.len();
252 let display_range = 0..filter_range.end;
253 (text, filter_range, display_range)
254 }
255 lsp::SymbolKind::INTERFACE | lsp::SymbolKind::CLASS => {
256 let text = format!("class {} {{}}", name);
257 let filter_range = 6..6 + name.len();
258 let display_range = 0..filter_range.end;
259 (text, filter_range, display_range)
260 }
261 lsp::SymbolKind::CONSTANT => {
262 let text = format!("const int {} = 0;", name);
263 let filter_range = 10..10 + name.len();
264 let display_range = 0..filter_range.end;
265 (text, filter_range, display_range)
266 }
267 lsp::SymbolKind::MODULE => {
268 let text = format!("namespace {} {{}}", name);
269 let filter_range = 10..10 + name.len();
270 let display_range = 0..filter_range.end;
271 (text, filter_range, display_range)
272 }
273 lsp::SymbolKind::TYPE_PARAMETER => {
274 let text = format!("typename {} {{}};", name);
275 let filter_range = 9..9 + name.len();
276 let display_range = 0..filter_range.end;
277 (text, filter_range, display_range)
278 }
279 _ => return None,
280 };
281
282 Some(CodeLabel {
283 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
284 text: text[display_range].to_string(),
285 filter_range,
286 })
287 }
288
289 fn prepare_initialize_params(
290 &self,
291 mut original: InitializeParams,
292 _: &App,
293 ) -> Result<InitializeParams> {
294 let experimental = json!({
295 "textDocument": {
296 "completion" : {
297 // enable clangd's dot-to-arrow feature.
298 "editsNearCursor": true
299 },
300 "inactiveRegionsCapabilities": {
301 "inactiveRegions": true,
302 }
303 }
304 });
305 if let Some(ref mut original_experimental) = original.capabilities.experimental {
306 merge_json_value_into(experimental, original_experimental);
307 } else {
308 original.capabilities.experimental = Some(experimental);
309 }
310 Ok(original)
311 }
312
313 fn retain_old_diagnostic(&self, previous_diagnostic: &Diagnostic, _: &App) -> bool {
314 clangd_ext::is_inactive_region(previous_diagnostic)
315 }
316
317 fn underline_diagnostic(&self, diagnostic: &lsp::Diagnostic) -> bool {
318 !clangd_ext::is_lsp_inactive_region(diagnostic)
319 }
320}
321
322async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
323 maybe!(async {
324 let mut last_clangd_dir = None;
325 let mut entries = fs::read_dir(&container_dir).await?;
326 while let Some(entry) = entries.next().await {
327 let entry = entry?;
328 if entry.file_type().await?.is_dir() {
329 last_clangd_dir = Some(entry.path());
330 }
331 }
332 let clangd_dir = last_clangd_dir.context("no cached binary")?;
333 let clangd_bin = clangd_dir.join("bin/clangd");
334 anyhow::ensure!(
335 clangd_bin.exists(),
336 "missing clangd binary in directory {clangd_dir:?}"
337 );
338 Ok(LanguageServerBinary {
339 path: clangd_bin,
340 env: None,
341 arguments: Vec::new(),
342 })
343 })
344 .await
345 .log_err()
346}
347
348#[cfg(test)]
349mod tests {
350 use gpui::{AppContext as _, BorrowAppContext, TestAppContext};
351 use language::{AutoindentMode, Buffer, language_settings::AllLanguageSettings};
352 use settings::SettingsStore;
353 use std::num::NonZeroU32;
354
355 #[gpui::test]
356 async fn test_c_autoindent(cx: &mut TestAppContext) {
357 // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
358 cx.update(|cx| {
359 let test_settings = SettingsStore::test(cx);
360 cx.set_global(test_settings);
361 language::init(cx);
362 cx.update_global::<SettingsStore, _>(|store, cx| {
363 store.update_user_settings::<AllLanguageSettings>(cx, |s| {
364 s.defaults.tab_size = NonZeroU32::new(2);
365 });
366 });
367 });
368 let language = crate::language("c", tree_sitter_c::LANGUAGE.into());
369
370 cx.new(|cx| {
371 let mut buffer = Buffer::local("", cx).with_language(language, cx);
372
373 // empty function
374 buffer.edit([(0..0, "int main() {}")], None, cx);
375
376 // indent inside braces
377 let ix = buffer.len() - 1;
378 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
379 assert_eq!(buffer.text(), "int main() {\n \n}");
380
381 // indent body of single-statement if statement
382 let ix = buffer.len() - 2;
383 buffer.edit([(ix..ix, "if (a)\nb;")], Some(AutoindentMode::EachLine), cx);
384 assert_eq!(buffer.text(), "int main() {\n if (a)\n b;\n}");
385
386 // indent inside field expression
387 let ix = buffer.len() - 3;
388 buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::EachLine), cx);
389 assert_eq!(buffer.text(), "int main() {\n if (a)\n b\n .c;\n}");
390
391 buffer
392 });
393 }
394}