c.rs

  1use anyhow::{anyhow, bail, Context, Result};
  2use async_trait::async_trait;
  3use futures::StreamExt;
  4use gpui::AsyncAppContext;
  5use http_client::github::{latest_github_release, GitHubLspBinaryVersion};
  6pub use language::*;
  7use lsp::LanguageServerBinary;
  8use project::{lsp_store::language_server_settings, project_settings::BinarySettings};
  9use smol::fs::{self, File};
 10use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
 11use util::{fs::remove_matching, maybe, ResultExt};
 12
 13pub struct CLspAdapter;
 14
 15impl CLspAdapter {
 16    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("clangd");
 17}
 18
 19#[async_trait(?Send)]
 20impl super::LspAdapter for CLspAdapter {
 21    fn name(&self) -> LanguageServerName {
 22        Self::SERVER_NAME.clone()
 23    }
 24
 25    async fn check_if_user_installed(
 26        &self,
 27        delegate: &dyn LspAdapterDelegate,
 28        cx: &AsyncAppContext,
 29    ) -> Option<LanguageServerBinary> {
 30        let configured_binary = cx.update(|cx| {
 31            language_server_settings(delegate, &Self::SERVER_NAME, cx)
 32                .and_then(|s| s.binary.clone())
 33        });
 34
 35        match configured_binary {
 36            Ok(Some(BinarySettings {
 37                path: Some(path),
 38                arguments,
 39                ..
 40            })) => Some(LanguageServerBinary {
 41                path: path.into(),
 42                arguments: arguments
 43                    .unwrap_or_default()
 44                    .iter()
 45                    .map(|arg| arg.into())
 46                    .collect(),
 47                env: None,
 48            }),
 49            Ok(Some(BinarySettings {
 50                path_lookup: Some(false),
 51                ..
 52            })) => None,
 53            _ => {
 54                let env = delegate.shell_env().await;
 55                let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
 56                Some(LanguageServerBinary {
 57                    path,
 58                    arguments: vec![],
 59                    env: Some(env),
 60                })
 61            }
 62        }
 63    }
 64
 65    async fn fetch_latest_server_version(
 66        &self,
 67        delegate: &dyn LspAdapterDelegate,
 68    ) -> Result<Box<dyn 'static + Send + Any>> {
 69        let release =
 70            latest_github_release("clangd/clangd", true, false, delegate.http_client()).await?;
 71        let os_suffix = match consts::OS {
 72            "macos" => "mac",
 73            "linux" => "linux",
 74            "windows" => "windows",
 75            other => bail!("Running on unsupported os: {other}"),
 76        };
 77        let asset_name = format!("clangd-{}-{}.zip", os_suffix, release.tag_name);
 78        let asset = release
 79            .assets
 80            .iter()
 81            .find(|asset| asset.name == asset_name)
 82            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
 83        let version = GitHubLspBinaryVersion {
 84            name: release.tag_name,
 85            url: asset.browser_download_url.clone(),
 86        };
 87        Ok(Box::new(version) as Box<_>)
 88    }
 89
 90    async fn fetch_server_binary(
 91        &self,
 92        version: Box<dyn 'static + Send + Any>,
 93        container_dir: PathBuf,
 94        delegate: &dyn LspAdapterDelegate,
 95    ) -> Result<LanguageServerBinary> {
 96        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
 97        let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
 98        let version_dir = container_dir.join(format!("clangd_{}", version.name));
 99        let binary_path = version_dir.join("bin/clangd");
100
101        if fs::metadata(&binary_path).await.is_err() {
102            let mut response = delegate
103                .http_client()
104                .get(&version.url, Default::default(), true)
105                .await
106                .context("error downloading release")?;
107            let mut file = File::create(&zip_path).await?;
108            if !response.status().is_success() {
109                Err(anyhow!(
110                    "download failed with status {}",
111                    response.status().to_string()
112                ))?;
113            }
114            futures::io::copy(response.body_mut(), &mut file).await?;
115
116            let unzip_status = smol::process::Command::new("unzip")
117                .current_dir(&container_dir)
118                .arg(&zip_path)
119                .output()
120                .await?
121                .status;
122            if !unzip_status.success() {
123                Err(anyhow!("failed to unzip clangd archive"))?;
124            }
125
126            remove_matching(&container_dir, |entry| entry != version_dir).await;
127        }
128
129        Ok(LanguageServerBinary {
130            path: binary_path,
131            env: None,
132            arguments: vec![],
133        })
134    }
135
136    async fn cached_server_binary(
137        &self,
138        container_dir: PathBuf,
139        _: &dyn LspAdapterDelegate,
140    ) -> Option<LanguageServerBinary> {
141        get_cached_server_binary(container_dir).await
142    }
143
144    async fn installation_test_binary(
145        &self,
146        container_dir: PathBuf,
147    ) -> Option<LanguageServerBinary> {
148        get_cached_server_binary(container_dir)
149            .await
150            .map(|mut binary| {
151                binary.arguments = vec!["--help".into()];
152                binary
153            })
154    }
155
156    async fn label_for_completion(
157        &self,
158        completion: &lsp::CompletionItem,
159        language: &Arc<Language>,
160    ) -> Option<CodeLabel> {
161        let label = completion
162            .label
163            .strip_prefix('•')
164            .unwrap_or(&completion.label)
165            .trim();
166
167        match completion.kind {
168            Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
169                let detail = completion.detail.as_ref().unwrap();
170                let text = format!("{} {}", detail, label);
171                let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
172                let runs = language.highlight_text(&source, 11..11 + text.len());
173                return Some(CodeLabel {
174                    filter_range: detail.len() + 1..text.len(),
175                    text,
176                    runs,
177                });
178            }
179            Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
180                if completion.detail.is_some() =>
181            {
182                let detail = completion.detail.as_ref().unwrap();
183                let text = format!("{} {}", detail, label);
184                let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
185                return Some(CodeLabel {
186                    filter_range: detail.len() + 1..text.len(),
187                    text,
188                    runs,
189                });
190            }
191            Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
192                if completion.detail.is_some() =>
193            {
194                let detail = completion.detail.as_ref().unwrap();
195                let text = format!("{} {}", detail, label);
196                let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
197                let filter_start = detail.len() + 1;
198                let filter_end =
199                    if let Some(end) = text.rfind('(').filter(|end| *end > filter_start) {
200                        end
201                    } else {
202                        text.len()
203                    };
204
205                return Some(CodeLabel {
206                    filter_range: filter_start..filter_end,
207                    text,
208                    runs,
209                });
210            }
211            Some(kind) => {
212                let highlight_name = match kind {
213                    lsp::CompletionItemKind::STRUCT
214                    | lsp::CompletionItemKind::INTERFACE
215                    | lsp::CompletionItemKind::CLASS
216                    | lsp::CompletionItemKind::ENUM => Some("type"),
217                    lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
218                    lsp::CompletionItemKind::KEYWORD => Some("keyword"),
219                    lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
220                        Some("constant")
221                    }
222                    _ => None,
223                };
224                if let Some(highlight_id) = language
225                    .grammar()
226                    .and_then(|g| g.highlight_id_for_name(highlight_name?))
227                {
228                    let mut label = CodeLabel::plain(label.to_string(), None);
229                    label.runs.push((
230                        0..label.text.rfind('(').unwrap_or(label.text.len()),
231                        highlight_id,
232                    ));
233                    return Some(label);
234                }
235            }
236            _ => {}
237        }
238        Some(CodeLabel::plain(label.to_string(), None))
239    }
240
241    async fn label_for_symbol(
242        &self,
243        name: &str,
244        kind: lsp::SymbolKind,
245        language: &Arc<Language>,
246    ) -> Option<CodeLabel> {
247        let (text, filter_range, display_range) = match kind {
248            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
249                let text = format!("void {} () {{}}", name);
250                let filter_range = 0..name.len();
251                let display_range = 5..5 + name.len();
252                (text, filter_range, display_range)
253            }
254            lsp::SymbolKind::STRUCT => {
255                let text = format!("struct {} {{}}", name);
256                let filter_range = 7..7 + name.len();
257                let display_range = 0..filter_range.end;
258                (text, filter_range, display_range)
259            }
260            lsp::SymbolKind::ENUM => {
261                let text = format!("enum {} {{}}", name);
262                let filter_range = 5..5 + name.len();
263                let display_range = 0..filter_range.end;
264                (text, filter_range, display_range)
265            }
266            lsp::SymbolKind::INTERFACE | lsp::SymbolKind::CLASS => {
267                let text = format!("class {} {{}}", name);
268                let filter_range = 6..6 + name.len();
269                let display_range = 0..filter_range.end;
270                (text, filter_range, display_range)
271            }
272            lsp::SymbolKind::CONSTANT => {
273                let text = format!("const int {} = 0;", name);
274                let filter_range = 10..10 + name.len();
275                let display_range = 0..filter_range.end;
276                (text, filter_range, display_range)
277            }
278            lsp::SymbolKind::MODULE => {
279                let text = format!("namespace {} {{}}", name);
280                let filter_range = 10..10 + name.len();
281                let display_range = 0..filter_range.end;
282                (text, filter_range, display_range)
283            }
284            lsp::SymbolKind::TYPE_PARAMETER => {
285                let text = format!("typename {} {{}};", name);
286                let filter_range = 9..9 + name.len();
287                let display_range = 0..filter_range.end;
288                (text, filter_range, display_range)
289            }
290            _ => return None,
291        };
292
293        Some(CodeLabel {
294            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
295            text: text[display_range].to_string(),
296            filter_range,
297        })
298    }
299}
300
301async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
302    maybe!(async {
303        let mut last_clangd_dir = None;
304        let mut entries = fs::read_dir(&container_dir).await?;
305        while let Some(entry) = entries.next().await {
306            let entry = entry?;
307            if entry.file_type().await?.is_dir() {
308                last_clangd_dir = Some(entry.path());
309            }
310        }
311        let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
312        let clangd_bin = clangd_dir.join("bin/clangd");
313        if clangd_bin.exists() {
314            Ok(LanguageServerBinary {
315                path: clangd_bin,
316                env: None,
317                arguments: vec![],
318            })
319        } else {
320            Err(anyhow!(
321                "missing clangd binary in directory {:?}",
322                clangd_dir
323            ))
324        }
325    })
326    .await
327    .log_err()
328}
329
330#[cfg(test)]
331mod tests {
332    use gpui::{BorrowAppContext, Context, TestAppContext};
333    use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
334    use settings::SettingsStore;
335    use std::num::NonZeroU32;
336
337    #[gpui::test]
338    async fn test_c_autoindent(cx: &mut TestAppContext) {
339        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
340        cx.update(|cx| {
341            let test_settings = SettingsStore::test(cx);
342            cx.set_global(test_settings);
343            language::init(cx);
344            cx.update_global::<SettingsStore, _>(|store, cx| {
345                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
346                    s.defaults.tab_size = NonZeroU32::new(2);
347                });
348            });
349        });
350        let language = crate::language("c", tree_sitter_c::LANGUAGE.into());
351
352        cx.new_model(|cx| {
353            let mut buffer = Buffer::local("", cx).with_language(language, cx);
354
355            // empty function
356            buffer.edit([(0..0, "int main() {}")], None, cx);
357
358            // indent inside braces
359            let ix = buffer.len() - 1;
360            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
361            assert_eq!(buffer.text(), "int main() {\n  \n}");
362
363            // indent body of single-statement if statement
364            let ix = buffer.len() - 2;
365            buffer.edit([(ix..ix, "if (a)\nb;")], Some(AutoindentMode::EachLine), cx);
366            assert_eq!(buffer.text(), "int main() {\n  if (a)\n    b;\n}");
367
368            // indent inside field expression
369            let ix = buffer.len() - 3;
370            buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::EachLine), cx);
371            assert_eq!(buffer.text(), "int main() {\n  if (a)\n    b\n      .c;\n}");
372
373            buffer
374        });
375    }
376}