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, LanguageServerName};
8use smol::fs::{self, File};
9use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
10use util::{fs::remove_matching, maybe, ResultExt};
11
12pub struct CLspAdapter;
13
14impl CLspAdapter {
15 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("clangd");
16}
17
18#[async_trait(?Send)]
19impl super::LspAdapter for CLspAdapter {
20 fn name(&self) -> LanguageServerName {
21 Self::SERVER_NAME.clone()
22 }
23
24 async fn check_if_user_installed(
25 &self,
26 delegate: &dyn LspAdapterDelegate,
27 _: Arc<dyn LanguageToolchainStore>,
28 _: &AsyncAppContext,
29 ) -> Option<LanguageServerBinary> {
30 let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
31 Some(LanguageServerBinary {
32 path,
33 arguments: vec![],
34 env: None,
35 })
36 }
37
38 async fn fetch_latest_server_version(
39 &self,
40 delegate: &dyn LspAdapterDelegate,
41 ) -> Result<Box<dyn 'static + Send + Any>> {
42 let release =
43 latest_github_release("clangd/clangd", true, false, delegate.http_client()).await?;
44 let os_suffix = match consts::OS {
45 "macos" => "mac",
46 "linux" => "linux",
47 "windows" => "windows",
48 other => bail!("Running on unsupported os: {other}"),
49 };
50 let asset_name = format!("clangd-{}-{}.zip", os_suffix, release.tag_name);
51 let asset = release
52 .assets
53 .iter()
54 .find(|asset| asset.name == asset_name)
55 .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
56 let version = GitHubLspBinaryVersion {
57 name: release.tag_name,
58 url: asset.browser_download_url.clone(),
59 };
60 Ok(Box::new(version) as Box<_>)
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 = util::command::new_smol_command("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 label_for_completion(
118 &self,
119 completion: &lsp::CompletionItem,
120 language: &Arc<Language>,
121 ) -> Option<CodeLabel> {
122 let label = completion
123 .label
124 .strip_prefix('•')
125 .unwrap_or(&completion.label)
126 .trim();
127
128 match completion.kind {
129 Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
130 let detail = completion.detail.as_ref().unwrap();
131 let text = format!("{} {}", detail, label);
132 let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
133 let runs = language.highlight_text(&source, 11..11 + text.len());
134 return Some(CodeLabel {
135 filter_range: detail.len() + 1..text.len(),
136 text,
137 runs,
138 });
139 }
140 Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
141 if completion.detail.is_some() =>
142 {
143 let detail = completion.detail.as_ref().unwrap();
144 let text = format!("{} {}", detail, label);
145 let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
146 return Some(CodeLabel {
147 filter_range: detail.len() + 1..text.len(),
148 text,
149 runs,
150 });
151 }
152 Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
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 let filter_start = detail.len() + 1;
159 let filter_end =
160 if let Some(end) = text.rfind('(').filter(|end| *end > filter_start) {
161 end
162 } else {
163 text.len()
164 };
165
166 return Some(CodeLabel {
167 filter_range: filter_start..filter_end,
168 text,
169 runs,
170 });
171 }
172 Some(kind) => {
173 let highlight_name = match kind {
174 lsp::CompletionItemKind::STRUCT
175 | lsp::CompletionItemKind::INTERFACE
176 | lsp::CompletionItemKind::CLASS
177 | lsp::CompletionItemKind::ENUM => Some("type"),
178 lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
179 lsp::CompletionItemKind::KEYWORD => Some("keyword"),
180 lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
181 Some("constant")
182 }
183 _ => None,
184 };
185 if let Some(highlight_id) = language
186 .grammar()
187 .and_then(|g| g.highlight_id_for_name(highlight_name?))
188 {
189 let mut label = CodeLabel::plain(label.to_string(), None);
190 label.runs.push((
191 0..label.text.rfind('(').unwrap_or(label.text.len()),
192 highlight_id,
193 ));
194 return Some(label);
195 }
196 }
197 _ => {}
198 }
199 Some(CodeLabel::plain(label.to_string(), None))
200 }
201
202 async fn label_for_symbol(
203 &self,
204 name: &str,
205 kind: lsp::SymbolKind,
206 language: &Arc<Language>,
207 ) -> Option<CodeLabel> {
208 let (text, filter_range, display_range) = match kind {
209 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
210 let text = format!("void {} () {{}}", name);
211 let filter_range = 0..name.len();
212 let display_range = 5..5 + name.len();
213 (text, filter_range, display_range)
214 }
215 lsp::SymbolKind::STRUCT => {
216 let text = format!("struct {} {{}}", name);
217 let filter_range = 7..7 + name.len();
218 let display_range = 0..filter_range.end;
219 (text, filter_range, display_range)
220 }
221 lsp::SymbolKind::ENUM => {
222 let text = format!("enum {} {{}}", name);
223 let filter_range = 5..5 + name.len();
224 let display_range = 0..filter_range.end;
225 (text, filter_range, display_range)
226 }
227 lsp::SymbolKind::INTERFACE | lsp::SymbolKind::CLASS => {
228 let text = format!("class {} {{}}", name);
229 let filter_range = 6..6 + name.len();
230 let display_range = 0..filter_range.end;
231 (text, filter_range, display_range)
232 }
233 lsp::SymbolKind::CONSTANT => {
234 let text = format!("const int {} = 0;", name);
235 let filter_range = 10..10 + name.len();
236 let display_range = 0..filter_range.end;
237 (text, filter_range, display_range)
238 }
239 lsp::SymbolKind::MODULE => {
240 let text = format!("namespace {} {{}}", name);
241 let filter_range = 10..10 + name.len();
242 let display_range = 0..filter_range.end;
243 (text, filter_range, display_range)
244 }
245 lsp::SymbolKind::TYPE_PARAMETER => {
246 let text = format!("typename {} {{}};", name);
247 let filter_range = 9..9 + name.len();
248 let display_range = 0..filter_range.end;
249 (text, filter_range, display_range)
250 }
251 _ => return None,
252 };
253
254 Some(CodeLabel {
255 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
256 text: text[display_range].to_string(),
257 filter_range,
258 })
259 }
260}
261
262async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
263 maybe!(async {
264 let mut last_clangd_dir = None;
265 let mut entries = fs::read_dir(&container_dir).await?;
266 while let Some(entry) = entries.next().await {
267 let entry = entry?;
268 if entry.file_type().await?.is_dir() {
269 last_clangd_dir = Some(entry.path());
270 }
271 }
272 let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
273 let clangd_bin = clangd_dir.join("bin/clangd");
274 if clangd_bin.exists() {
275 Ok(LanguageServerBinary {
276 path: clangd_bin,
277 env: None,
278 arguments: vec![],
279 })
280 } else {
281 Err(anyhow!(
282 "missing clangd binary in directory {:?}",
283 clangd_dir
284 ))
285 }
286 })
287 .await
288 .log_err()
289}
290
291#[cfg(test)]
292mod tests {
293 use gpui::{BorrowAppContext, Context, TestAppContext};
294 use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
295 use settings::SettingsStore;
296 use std::num::NonZeroU32;
297
298 #[gpui::test]
299 async fn test_c_autoindent(cx: &mut TestAppContext) {
300 // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
301 cx.update(|cx| {
302 let test_settings = SettingsStore::test(cx);
303 cx.set_global(test_settings);
304 language::init(cx);
305 cx.update_global::<SettingsStore, _>(|store, cx| {
306 store.update_user_settings::<AllLanguageSettings>(cx, |s| {
307 s.defaults.tab_size = NonZeroU32::new(2);
308 });
309 });
310 });
311 let language = crate::language("c", tree_sitter_c::LANGUAGE.into());
312
313 cx.new_model(|cx| {
314 let mut buffer = Buffer::local("", cx).with_language(language, cx);
315
316 // empty function
317 buffer.edit([(0..0, "int main() {}")], None, cx);
318
319 // indent inside braces
320 let ix = buffer.len() - 1;
321 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
322 assert_eq!(buffer.text(), "int main() {\n \n}");
323
324 // indent body of single-statement if statement
325 let ix = buffer.len() - 2;
326 buffer.edit([(ix..ix, "if (a)\nb;")], Some(AutoindentMode::EachLine), cx);
327 assert_eq!(buffer.text(), "int main() {\n if (a)\n b;\n}");
328
329 // indent inside field expression
330 let ix = buffer.len() - 3;
331 buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::EachLine), cx);
332 assert_eq!(buffer.text(), "int main() {\n if (a)\n b\n .c;\n}");
333
334 buffer
335 });
336 }
337}