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