c.rs

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