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