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