c.rs

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