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}