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