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