1use anyhow::{anyhow, Context, Result};
2use async_compression::futures::bufread::GzipDecoder;
3use async_trait::async_trait;
4use collections::HashMap;
5use futures::{io::BufReader, StreamExt};
6use gpui::{AppContext, AsyncAppContext, Task};
7use http_client::github::AssetKind;
8use http_client::github::{latest_github_release, GitHubLspBinaryVersion};
9pub use language::*;
10use lsp::{LanguageServerBinary, LanguageServerName};
11use regex::Regex;
12use smol::fs::{self};
13use std::{
14 any::Any,
15 borrow::Cow,
16 path::{Path, PathBuf},
17 sync::Arc,
18 sync::LazyLock,
19};
20use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
21use util::{fs::remove_matching, maybe, ResultExt};
22
23use crate::language_settings::language_settings;
24
25pub struct RustLspAdapter;
26
27#[cfg(target_os = "macos")]
28impl RustLspAdapter {
29 const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
30 const ARCH_SERVER_NAME: &str = "apple-darwin";
31}
32
33#[cfg(target_os = "linux")]
34impl RustLspAdapter {
35 const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
36 const ARCH_SERVER_NAME: &str = "unknown-linux-gnu";
37}
38
39#[cfg(target_os = "windows")]
40impl RustLspAdapter {
41 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
42 const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
43}
44
45impl RustLspAdapter {
46 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer");
47
48 fn build_asset_name() -> String {
49 let extension = match Self::GITHUB_ASSET_KIND {
50 AssetKind::TarGz => "gz", // Nb: rust-analyzer releases use .gz not .tar.gz
51 AssetKind::Zip => "zip",
52 };
53
54 format!(
55 "{}-{}-{}.{}",
56 Self::SERVER_NAME,
57 std::env::consts::ARCH,
58 Self::ARCH_SERVER_NAME,
59 extension
60 )
61 }
62}
63
64#[async_trait(?Send)]
65impl LspAdapter for RustLspAdapter {
66 fn name(&self) -> LanguageServerName {
67 Self::SERVER_NAME.clone()
68 }
69
70 async fn check_if_user_installed(
71 &self,
72 delegate: &dyn LspAdapterDelegate,
73 _: &AsyncAppContext,
74 ) -> Option<LanguageServerBinary> {
75 let path = delegate.which("rust-analyzer".as_ref()).await?;
76 let env = delegate.shell_env().await;
77
78 // It is surprisingly common for ~/.cargo/bin/rust-analyzer to be a symlink to
79 // /usr/bin/rust-analyzer that fails when you run it; so we need to test it.
80 log::info!("found rust-analyzer in PATH. trying to run `rust-analyzer --help`");
81 let result = delegate
82 .try_exec(LanguageServerBinary {
83 path: path.clone(),
84 arguments: vec!["--help".into()],
85 env: Some(env.clone()),
86 })
87 .await;
88 if let Err(err) = result {
89 log::error!(
90 "failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {}",
91 path,
92 err
93 );
94 return None;
95 }
96
97 Some(LanguageServerBinary {
98 path,
99 env: Some(env),
100 arguments: vec![],
101 })
102 }
103
104 async fn fetch_latest_server_version(
105 &self,
106 delegate: &dyn LspAdapterDelegate,
107 ) -> Result<Box<dyn 'static + Send + Any>> {
108 let release = latest_github_release(
109 "rust-lang/rust-analyzer",
110 true,
111 false,
112 delegate.http_client(),
113 )
114 .await?;
115 let asset_name = Self::build_asset_name();
116
117 let asset = release
118 .assets
119 .iter()
120 .find(|asset| asset.name == asset_name)
121 .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
122 Ok(Box::new(GitHubLspBinaryVersion {
123 name: release.tag_name,
124 url: asset.browser_download_url.clone(),
125 }))
126 }
127
128 async fn fetch_server_binary(
129 &self,
130 version: Box<dyn 'static + Send + Any>,
131 container_dir: PathBuf,
132 delegate: &dyn LspAdapterDelegate,
133 ) -> Result<LanguageServerBinary> {
134 let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
135 let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
136 let server_path = match Self::GITHUB_ASSET_KIND {
137 AssetKind::TarGz => destination_path.clone(), // Tar extracts in place.
138 AssetKind::Zip => destination_path.clone().join("rust-analyzer.exe"), // zip contains a .exe
139 };
140
141 if fs::metadata(&server_path).await.is_err() {
142 remove_matching(&container_dir, |entry| entry != destination_path).await;
143
144 let mut response = delegate
145 .http_client()
146 .get(&version.url, Default::default(), true)
147 .await
148 .map_err(|err| anyhow!("error downloading release: {}", err))?;
149 match Self::GITHUB_ASSET_KIND {
150 AssetKind::TarGz => {
151 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
152 let archive = async_tar::Archive::new(decompressed_bytes);
153 archive.unpack(&destination_path).await?;
154 }
155 AssetKind::Zip => {
156 node_runtime::extract_zip(
157 &destination_path,
158 BufReader::new(response.body_mut()),
159 )
160 .await?;
161 }
162 };
163
164 // todo("windows")
165 #[cfg(not(windows))]
166 {
167 fs::set_permissions(
168 &server_path,
169 <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
170 )
171 .await?;
172 }
173 }
174
175 Ok(LanguageServerBinary {
176 path: server_path,
177 env: None,
178 arguments: Default::default(),
179 })
180 }
181
182 async fn cached_server_binary(
183 &self,
184 container_dir: PathBuf,
185 _: &dyn LspAdapterDelegate,
186 ) -> Option<LanguageServerBinary> {
187 get_cached_server_binary(container_dir).await
188 }
189
190 fn disk_based_diagnostic_sources(&self) -> Vec<String> {
191 vec!["rustc".into()]
192 }
193
194 fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
195 Some("rust-analyzer/flycheck".into())
196 }
197
198 fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
199 static REGEX: LazyLock<Regex> =
200 LazyLock::new(|| Regex::new(r"(?m)`([^`]+)\n`$").expect("Failed to create REGEX"));
201
202 for diagnostic in &mut params.diagnostics {
203 for message in diagnostic
204 .related_information
205 .iter_mut()
206 .flatten()
207 .map(|info| &mut info.message)
208 .chain([&mut diagnostic.message])
209 {
210 if let Cow::Owned(sanitized) = REGEX.replace_all(message, "`$1`") {
211 *message = sanitized;
212 }
213 }
214 }
215 }
216
217 async fn label_for_completion(
218 &self,
219 completion: &lsp::CompletionItem,
220 language: &Arc<Language>,
221 ) -> Option<CodeLabel> {
222 let detail = completion
223 .label_details
224 .as_ref()
225 .and_then(|detail| detail.detail.as_ref())
226 .or(completion.detail.as_ref())
227 .map(ToOwned::to_owned);
228 let function_signature = completion
229 .label_details
230 .as_ref()
231 .and_then(|detail| detail.description.as_ref())
232 .or(completion.detail.as_ref())
233 .map(ToOwned::to_owned);
234 match completion.kind {
235 Some(lsp::CompletionItemKind::FIELD) if detail.is_some() => {
236 let name = &completion.label;
237 let text = format!("{}: {}", name, detail.unwrap());
238 let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
239 let runs = language.highlight_text(&source, 11..11 + text.len());
240 return Some(CodeLabel {
241 text,
242 runs,
243 filter_range: 0..name.len(),
244 });
245 }
246 Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
247 if detail.is_some()
248 && completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) =>
249 {
250 let name = &completion.label;
251 let text = format!(
252 "{}: {}",
253 name,
254 completion.detail.as_ref().or(detail.as_ref()).unwrap()
255 );
256 let source = Rope::from(format!("let {} = ();", text).as_str());
257 let runs = language.highlight_text(&source, 4..4 + text.len());
258 return Some(CodeLabel {
259 text,
260 runs,
261 filter_range: 0..name.len(),
262 });
263 }
264 Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
265 if detail.is_some() =>
266 {
267 static REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("\\(…?\\)").unwrap());
268
269 let detail = detail.unwrap();
270 const FUNCTION_PREFIXES: [&str; 6] = [
271 "async fn",
272 "async unsafe fn",
273 "const fn",
274 "const unsafe fn",
275 "unsafe fn",
276 "fn",
277 ];
278 // Is it function `async`?
279 let fn_keyword = FUNCTION_PREFIXES.iter().find_map(|prefix| {
280 function_signature.as_ref().and_then(|signature| {
281 signature
282 .strip_prefix(*prefix)
283 .map(|suffix| (*prefix, suffix))
284 })
285 });
286 // fn keyword should be followed by opening parenthesis.
287 if let Some((prefix, suffix)) = fn_keyword {
288 let mut text = REGEX.replace(&completion.label, suffix).to_string();
289 let source = Rope::from(format!("{prefix} {} {{}}", text).as_str());
290 let run_start = prefix.len() + 1;
291 let runs = language.highlight_text(&source, run_start..run_start + text.len());
292 if detail.starts_with(" (") {
293 text.push_str(&detail);
294 }
295
296 return Some(CodeLabel {
297 filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
298 text,
299 runs,
300 });
301 } else if completion
302 .detail
303 .as_ref()
304 .map_or(false, |detail| detail.starts_with("macro_rules! "))
305 {
306 let source = Rope::from(completion.label.as_str());
307 let runs = language.highlight_text(&source, 0..completion.label.len());
308
309 return Some(CodeLabel {
310 filter_range: 0..completion.label.len(),
311 text: completion.label.clone(),
312 runs,
313 });
314 }
315 }
316 Some(kind) => {
317 let highlight_name = match kind {
318 lsp::CompletionItemKind::STRUCT
319 | lsp::CompletionItemKind::INTERFACE
320 | lsp::CompletionItemKind::ENUM => Some("type"),
321 lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
322 lsp::CompletionItemKind::KEYWORD => Some("keyword"),
323 lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
324 Some("constant")
325 }
326 _ => None,
327 };
328
329 let mut label = completion.label.clone();
330 if let Some(detail) = detail.filter(|detail| detail.starts_with(" (")) {
331 use std::fmt::Write;
332 write!(label, "{detail}").ok()?;
333 }
334 let mut label = CodeLabel::plain(label, None);
335 if let Some(highlight_name) = highlight_name {
336 let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name)?;
337 label.runs.push((
338 0..label.text.rfind('(').unwrap_or(completion.label.len()),
339 highlight_id,
340 ));
341 }
342
343 return Some(label);
344 }
345 _ => {}
346 }
347 None
348 }
349
350 async fn label_for_symbol(
351 &self,
352 name: &str,
353 kind: lsp::SymbolKind,
354 language: &Arc<Language>,
355 ) -> Option<CodeLabel> {
356 let (text, filter_range, display_range) = match kind {
357 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
358 let text = format!("fn {} () {{}}", name);
359 let filter_range = 3..3 + name.len();
360 let display_range = 0..filter_range.end;
361 (text, filter_range, display_range)
362 }
363 lsp::SymbolKind::STRUCT => {
364 let text = format!("struct {} {{}}", name);
365 let filter_range = 7..7 + name.len();
366 let display_range = 0..filter_range.end;
367 (text, filter_range, display_range)
368 }
369 lsp::SymbolKind::ENUM => {
370 let text = format!("enum {} {{}}", name);
371 let filter_range = 5..5 + name.len();
372 let display_range = 0..filter_range.end;
373 (text, filter_range, display_range)
374 }
375 lsp::SymbolKind::INTERFACE => {
376 let text = format!("trait {} {{}}", name);
377 let filter_range = 6..6 + name.len();
378 let display_range = 0..filter_range.end;
379 (text, filter_range, display_range)
380 }
381 lsp::SymbolKind::CONSTANT => {
382 let text = format!("const {}: () = ();", name);
383 let filter_range = 6..6 + name.len();
384 let display_range = 0..filter_range.end;
385 (text, filter_range, display_range)
386 }
387 lsp::SymbolKind::MODULE => {
388 let text = format!("mod {} {{}}", name);
389 let filter_range = 4..4 + name.len();
390 let display_range = 0..filter_range.end;
391 (text, filter_range, display_range)
392 }
393 lsp::SymbolKind::TYPE_PARAMETER => {
394 let text = format!("type {} {{}}", name);
395 let filter_range = 5..5 + name.len();
396 let display_range = 0..filter_range.end;
397 (text, filter_range, display_range)
398 }
399 _ => return None,
400 };
401
402 Some(CodeLabel {
403 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
404 text: text[display_range].to_string(),
405 filter_range,
406 })
407 }
408}
409
410pub(crate) struct RustContextProvider;
411
412const RUST_PACKAGE_TASK_VARIABLE: VariableName =
413 VariableName::Custom(Cow::Borrowed("RUST_PACKAGE"));
414
415/// The bin name corresponding to the current file in Cargo.toml
416const RUST_BIN_NAME_TASK_VARIABLE: VariableName =
417 VariableName::Custom(Cow::Borrowed("RUST_BIN_NAME"));
418
419const RUST_MAIN_FUNCTION_TASK_VARIABLE: VariableName =
420 VariableName::Custom(Cow::Borrowed("_rust_main_function_end"));
421
422impl ContextProvider for RustContextProvider {
423 fn build_context(
424 &self,
425 task_variables: &TaskVariables,
426 location: &Location,
427 project_env: Option<HashMap<String, String>>,
428 _: Arc<dyn LanguageToolchainStore>,
429 cx: &mut gpui::AppContext,
430 ) -> Task<Result<TaskVariables>> {
431 let local_abs_path = location
432 .buffer
433 .read(cx)
434 .file()
435 .and_then(|file| Some(file.as_local()?.abs_path(cx)));
436
437 let local_abs_path = local_abs_path.as_deref();
438
439 let is_main_function = task_variables
440 .get(&RUST_MAIN_FUNCTION_TASK_VARIABLE)
441 .is_some();
442
443 if is_main_function {
444 if let Some((package_name, bin_name)) = local_abs_path.and_then(|path| {
445 package_name_and_bin_name_from_abs_path(path, project_env.as_ref())
446 }) {
447 return Task::ready(Ok(TaskVariables::from_iter([
448 (RUST_PACKAGE_TASK_VARIABLE.clone(), package_name),
449 (RUST_BIN_NAME_TASK_VARIABLE.clone(), bin_name),
450 ])));
451 }
452 }
453
454 if let Some(package_name) = local_abs_path
455 .and_then(|local_abs_path| local_abs_path.parent())
456 .and_then(|path| human_readable_package_name(path, project_env.as_ref()))
457 {
458 return Task::ready(Ok(TaskVariables::from_iter([(
459 RUST_PACKAGE_TASK_VARIABLE.clone(),
460 package_name,
461 )])));
462 }
463
464 Task::ready(Ok(TaskVariables::default()))
465 }
466
467 fn associated_tasks(
468 &self,
469 file: Option<Arc<dyn language::File>>,
470 cx: &AppContext,
471 ) -> Option<TaskTemplates> {
472 const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
473 let package_to_run = language_settings(Some("Rust".into()), file.as_ref(), cx)
474 .tasks
475 .variables
476 .get(DEFAULT_RUN_NAME_STR)
477 .cloned();
478 let run_task_args = if let Some(package_to_run) = package_to_run {
479 vec!["run".into(), "-p".into(), package_to_run]
480 } else {
481 vec!["run".into()]
482 };
483 Some(TaskTemplates(vec![
484 TaskTemplate {
485 label: format!(
486 "cargo check -p {}",
487 RUST_PACKAGE_TASK_VARIABLE.template_value(),
488 ),
489 command: "cargo".into(),
490 args: vec![
491 "check".into(),
492 "-p".into(),
493 RUST_PACKAGE_TASK_VARIABLE.template_value(),
494 ],
495 cwd: Some("$ZED_DIRNAME".to_owned()),
496 ..TaskTemplate::default()
497 },
498 TaskTemplate {
499 label: "cargo check --workspace --all-targets".into(),
500 command: "cargo".into(),
501 args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
502 cwd: Some("$ZED_DIRNAME".to_owned()),
503 ..TaskTemplate::default()
504 },
505 TaskTemplate {
506 label: format!(
507 "cargo test -p {} {} -- --nocapture",
508 RUST_PACKAGE_TASK_VARIABLE.template_value(),
509 VariableName::Symbol.template_value(),
510 ),
511 command: "cargo".into(),
512 args: vec![
513 "test".into(),
514 "-p".into(),
515 RUST_PACKAGE_TASK_VARIABLE.template_value(),
516 VariableName::Symbol.template_value(),
517 "--".into(),
518 "--nocapture".into(),
519 ],
520 tags: vec!["rust-test".to_owned()],
521 cwd: Some("$ZED_DIRNAME".to_owned()),
522 ..TaskTemplate::default()
523 },
524 TaskTemplate {
525 label: format!(
526 "cargo test -p {} {}",
527 RUST_PACKAGE_TASK_VARIABLE.template_value(),
528 VariableName::Stem.template_value(),
529 ),
530 command: "cargo".into(),
531 args: vec![
532 "test".into(),
533 "-p".into(),
534 RUST_PACKAGE_TASK_VARIABLE.template_value(),
535 VariableName::Stem.template_value(),
536 ],
537 tags: vec!["rust-mod-test".to_owned()],
538 cwd: Some("$ZED_DIRNAME".to_owned()),
539 ..TaskTemplate::default()
540 },
541 TaskTemplate {
542 label: format!(
543 "cargo run -p {} --bin {}",
544 RUST_PACKAGE_TASK_VARIABLE.template_value(),
545 RUST_BIN_NAME_TASK_VARIABLE.template_value(),
546 ),
547 command: "cargo".into(),
548 args: vec![
549 "run".into(),
550 "-p".into(),
551 RUST_PACKAGE_TASK_VARIABLE.template_value(),
552 "--bin".into(),
553 RUST_BIN_NAME_TASK_VARIABLE.template_value(),
554 ],
555 cwd: Some("$ZED_DIRNAME".to_owned()),
556 tags: vec!["rust-main".to_owned()],
557 ..TaskTemplate::default()
558 },
559 TaskTemplate {
560 label: format!(
561 "cargo test -p {}",
562 RUST_PACKAGE_TASK_VARIABLE.template_value()
563 ),
564 command: "cargo".into(),
565 args: vec![
566 "test".into(),
567 "-p".into(),
568 RUST_PACKAGE_TASK_VARIABLE.template_value(),
569 ],
570 cwd: Some("$ZED_DIRNAME".to_owned()),
571 ..TaskTemplate::default()
572 },
573 TaskTemplate {
574 label: "cargo run".into(),
575 command: "cargo".into(),
576 args: run_task_args,
577 cwd: Some("$ZED_DIRNAME".to_owned()),
578 ..TaskTemplate::default()
579 },
580 TaskTemplate {
581 label: "cargo clean".into(),
582 command: "cargo".into(),
583 args: vec!["clean".into()],
584 cwd: Some("$ZED_DIRNAME".to_owned()),
585 ..TaskTemplate::default()
586 },
587 ]))
588 }
589}
590
591/// Part of the data structure of Cargo metadata
592#[derive(serde::Deserialize)]
593struct CargoMetadata {
594 packages: Vec<CargoPackage>,
595}
596
597#[derive(serde::Deserialize)]
598struct CargoPackage {
599 id: String,
600 targets: Vec<CargoTarget>,
601}
602
603#[derive(serde::Deserialize)]
604struct CargoTarget {
605 name: String,
606 kind: Vec<String>,
607 src_path: String,
608}
609
610fn package_name_and_bin_name_from_abs_path(
611 abs_path: &Path,
612 project_env: Option<&HashMap<String, String>>,
613) -> Option<(String, String)> {
614 let mut command = std::process::Command::new("cargo");
615 if let Some(envs) = project_env {
616 command.envs(envs);
617 }
618 let output = command
619 .current_dir(abs_path.parent()?)
620 .arg("metadata")
621 .arg("--no-deps")
622 .arg("--format-version")
623 .arg("1")
624 .output()
625 .log_err()?
626 .stdout;
627
628 let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
629
630 retrieve_package_id_and_bin_name_from_metadata(metadata, abs_path).and_then(
631 |(package_id, bin_name)| {
632 let package_name = package_name_from_pkgid(&package_id);
633
634 package_name.map(|package_name| (package_name.to_owned(), bin_name))
635 },
636 )
637}
638
639fn retrieve_package_id_and_bin_name_from_metadata(
640 metadata: CargoMetadata,
641 abs_path: &Path,
642) -> Option<(String, String)> {
643 for package in metadata.packages {
644 for target in package.targets {
645 let is_bin = target.kind.iter().any(|kind| kind == "bin");
646 let target_path = PathBuf::from(target.src_path);
647 if target_path == abs_path && is_bin {
648 return Some((package.id, target.name));
649 }
650 }
651 }
652
653 None
654}
655
656fn human_readable_package_name(
657 package_directory: &Path,
658 project_env: Option<&HashMap<String, String>>,
659) -> Option<String> {
660 let mut command = std::process::Command::new("cargo");
661 if let Some(envs) = project_env {
662 command.envs(envs);
663 }
664
665 let pkgid = String::from_utf8(
666 command
667 .current_dir(package_directory)
668 .arg("pkgid")
669 .output()
670 .log_err()?
671 .stdout,
672 )
673 .ok()?;
674 Some(package_name_from_pkgid(&pkgid)?.to_owned())
675}
676
677// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
678// Output example in the root of Zed project:
679// ```sh
680// ❯ cargo pkgid zed
681// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
682// ```
683// Another variant, if a project has a custom package name or hyphen in the name:
684// ```
685// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
686// ```
687//
688// Extracts the package name from the output according to the spec:
689// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
690fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
691 fn split_off_suffix(input: &str, suffix_start: char) -> &str {
692 match input.rsplit_once(suffix_start) {
693 Some((without_suffix, _)) => without_suffix,
694 None => input,
695 }
696 }
697
698 let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
699 let package_name = match version_suffix.rsplit_once('@') {
700 Some((custom_package_name, _version)) => custom_package_name,
701 None => {
702 let host_and_path = split_off_suffix(version_prefix, '?');
703 let (_, package_name) = host_and_path.rsplit_once('/')?;
704 package_name
705 }
706 };
707 Some(package_name)
708}
709
710async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
711 maybe!(async {
712 let mut last = None;
713 let mut entries = fs::read_dir(&container_dir).await?;
714 while let Some(entry) = entries.next().await {
715 last = Some(entry?.path());
716 }
717
718 anyhow::Ok(LanguageServerBinary {
719 path: last.ok_or_else(|| anyhow!("no cached binary"))?,
720 env: None,
721 arguments: Default::default(),
722 })
723 })
724 .await
725 .log_err()
726}
727
728#[cfg(test)]
729mod tests {
730 use std::num::NonZeroU32;
731
732 use super::*;
733 use crate::language;
734 use gpui::{BorrowAppContext, Context, Hsla, TestAppContext};
735 use language::language_settings::AllLanguageSettings;
736 use lsp::CompletionItemLabelDetails;
737 use settings::SettingsStore;
738 use theme::SyntaxTheme;
739
740 #[gpui::test]
741 async fn test_process_rust_diagnostics() {
742 let mut params = lsp::PublishDiagnosticsParams {
743 uri: lsp::Url::from_file_path("/a").unwrap(),
744 version: None,
745 diagnostics: vec![
746 // no newlines
747 lsp::Diagnostic {
748 message: "use of moved value `a`".to_string(),
749 ..Default::default()
750 },
751 // newline at the end of a code span
752 lsp::Diagnostic {
753 message: "consider importing this struct: `use b::c;\n`".to_string(),
754 ..Default::default()
755 },
756 // code span starting right after a newline
757 lsp::Diagnostic {
758 message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
759 .to_string(),
760 ..Default::default()
761 },
762 ],
763 };
764 RustLspAdapter.process_diagnostics(&mut params);
765
766 assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
767
768 // remove trailing newline from code span
769 assert_eq!(
770 params.diagnostics[1].message,
771 "consider importing this struct: `use b::c;`"
772 );
773
774 // do not remove newline before the start of code span
775 assert_eq!(
776 params.diagnostics[2].message,
777 "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
778 );
779 }
780
781 #[gpui::test]
782 async fn test_rust_label_for_completion() {
783 let adapter = Arc::new(RustLspAdapter);
784 let language = language("rust", tree_sitter_rust::LANGUAGE.into());
785 let grammar = language.grammar().unwrap();
786 let theme = SyntaxTheme::new_test([
787 ("type", Hsla::default()),
788 ("keyword", Hsla::default()),
789 ("function", Hsla::default()),
790 ("property", Hsla::default()),
791 ]);
792
793 language.set_theme(&theme);
794
795 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
796 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
797 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
798 let highlight_field = grammar.highlight_id_for_name("property").unwrap();
799
800 assert_eq!(
801 adapter
802 .label_for_completion(
803 &lsp::CompletionItem {
804 kind: Some(lsp::CompletionItemKind::FUNCTION),
805 label: "hello(…)".to_string(),
806 label_details: Some(CompletionItemLabelDetails {
807 detail: Some(" (use crate::foo)".into()),
808 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string())
809 }),
810 ..Default::default()
811 },
812 &language
813 )
814 .await,
815 Some(CodeLabel {
816 text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
817 filter_range: 0..5,
818 runs: vec![
819 (0..5, highlight_function),
820 (7..10, highlight_keyword),
821 (11..17, highlight_type),
822 (18..19, highlight_type),
823 (25..28, highlight_type),
824 (29..30, highlight_type),
825 ],
826 })
827 );
828 assert_eq!(
829 adapter
830 .label_for_completion(
831 &lsp::CompletionItem {
832 kind: Some(lsp::CompletionItemKind::FUNCTION),
833 label: "hello(…)".to_string(),
834 label_details: Some(CompletionItemLabelDetails {
835 detail: Some(" (use crate::foo)".into()),
836 description: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
837 }),
838 ..Default::default()
839 },
840 &language
841 )
842 .await,
843 Some(CodeLabel {
844 text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
845 filter_range: 0..5,
846 runs: vec![
847 (0..5, highlight_function),
848 (7..10, highlight_keyword),
849 (11..17, highlight_type),
850 (18..19, highlight_type),
851 (25..28, highlight_type),
852 (29..30, highlight_type),
853 ],
854 })
855 );
856 assert_eq!(
857 adapter
858 .label_for_completion(
859 &lsp::CompletionItem {
860 kind: Some(lsp::CompletionItemKind::FIELD),
861 label: "len".to_string(),
862 detail: Some("usize".to_string()),
863 ..Default::default()
864 },
865 &language
866 )
867 .await,
868 Some(CodeLabel {
869 text: "len: usize".to_string(),
870 filter_range: 0..3,
871 runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
872 })
873 );
874
875 assert_eq!(
876 adapter
877 .label_for_completion(
878 &lsp::CompletionItem {
879 kind: Some(lsp::CompletionItemKind::FUNCTION),
880 label: "hello(…)".to_string(),
881 label_details: Some(CompletionItemLabelDetails {
882 detail: Some(" (use crate::foo)".to_string()),
883 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
884 }),
885
886 ..Default::default()
887 },
888 &language
889 )
890 .await,
891 Some(CodeLabel {
892 text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
893 filter_range: 0..5,
894 runs: vec![
895 (0..5, highlight_function),
896 (7..10, highlight_keyword),
897 (11..17, highlight_type),
898 (18..19, highlight_type),
899 (25..28, highlight_type),
900 (29..30, highlight_type),
901 ],
902 })
903 );
904 }
905
906 #[gpui::test]
907 async fn test_rust_label_for_symbol() {
908 let adapter = Arc::new(RustLspAdapter);
909 let language = language("rust", tree_sitter_rust::LANGUAGE.into());
910 let grammar = language.grammar().unwrap();
911 let theme = SyntaxTheme::new_test([
912 ("type", Hsla::default()),
913 ("keyword", Hsla::default()),
914 ("function", Hsla::default()),
915 ("property", Hsla::default()),
916 ]);
917
918 language.set_theme(&theme);
919
920 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
921 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
922 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
923
924 assert_eq!(
925 adapter
926 .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
927 .await,
928 Some(CodeLabel {
929 text: "fn hello".to_string(),
930 filter_range: 3..8,
931 runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
932 })
933 );
934
935 assert_eq!(
936 adapter
937 .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
938 .await,
939 Some(CodeLabel {
940 text: "type World".to_string(),
941 filter_range: 5..10,
942 runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
943 })
944 );
945 }
946
947 #[gpui::test]
948 async fn test_rust_autoindent(cx: &mut TestAppContext) {
949 // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
950 cx.update(|cx| {
951 let test_settings = SettingsStore::test(cx);
952 cx.set_global(test_settings);
953 language::init(cx);
954 cx.update_global::<SettingsStore, _>(|store, cx| {
955 store.update_user_settings::<AllLanguageSettings>(cx, |s| {
956 s.defaults.tab_size = NonZeroU32::new(2);
957 });
958 });
959 });
960
961 let language = crate::language("rust", tree_sitter_rust::LANGUAGE.into());
962
963 cx.new_model(|cx| {
964 let mut buffer = Buffer::local("", cx).with_language(language, cx);
965
966 // indent between braces
967 buffer.set_text("fn a() {}", cx);
968 let ix = buffer.len() - 1;
969 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
970 assert_eq!(buffer.text(), "fn a() {\n \n}");
971
972 // indent between braces, even after empty lines
973 buffer.set_text("fn a() {\n\n\n}", cx);
974 let ix = buffer.len() - 2;
975 buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
976 assert_eq!(buffer.text(), "fn a() {\n\n\n \n}");
977
978 // indent a line that continues a field expression
979 buffer.set_text("fn a() {\n \n}", cx);
980 let ix = buffer.len() - 2;
981 buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
982 assert_eq!(buffer.text(), "fn a() {\n b\n .c\n}");
983
984 // indent further lines that continue the field expression, even after empty lines
985 let ix = buffer.len() - 2;
986 buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
987 assert_eq!(buffer.text(), "fn a() {\n b\n .c\n \n .d\n}");
988
989 // dedent the line after the field expression
990 let ix = buffer.len() - 2;
991 buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
992 assert_eq!(
993 buffer.text(),
994 "fn a() {\n b\n .c\n \n .d;\n e\n}"
995 );
996
997 // indent inside a struct within a call
998 buffer.set_text("const a: B = c(D {});", cx);
999 let ix = buffer.len() - 3;
1000 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1001 assert_eq!(buffer.text(), "const a: B = c(D {\n \n});");
1002
1003 // indent further inside a nested call
1004 let ix = buffer.len() - 4;
1005 buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
1006 assert_eq!(buffer.text(), "const a: B = c(D {\n e: f(\n \n )\n});");
1007
1008 // keep that indent after an empty line
1009 let ix = buffer.len() - 8;
1010 buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1011 assert_eq!(
1012 buffer.text(),
1013 "const a: B = c(D {\n e: f(\n \n \n )\n});"
1014 );
1015
1016 buffer
1017 });
1018 }
1019
1020 #[test]
1021 fn test_package_name_from_pkgid() {
1022 for (input, expected) in [
1023 (
1024 "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
1025 "zed",
1026 ),
1027 (
1028 "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
1029 "my-custom-package",
1030 ),
1031 ] {
1032 assert_eq!(package_name_from_pkgid(input), Some(expected));
1033 }
1034 }
1035
1036 #[test]
1037 fn test_retrieve_package_id_and_bin_name_from_metadata() {
1038 for (input, absolute_path, expected) in [
1039 (
1040 r#"{"packages":[{"id":"path+file:///path/to/zed/crates/zed#0.131.0","targets":[{"name":"zed","kind":["bin"],"src_path":"/path/to/zed/src/main.rs"}]}]}"#,
1041 "/path/to/zed/src/main.rs",
1042 Some(("path+file:///path/to/zed/crates/zed#0.131.0", "zed")),
1043 ),
1044 (
1045 r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","targets":[{"name":"my-custom-bin","kind":["bin"],"src_path":"/path/to/custom-package/src/main.rs"}]}]}"#,
1046 "/path/to/custom-package/src/main.rs",
1047 Some((
1048 "path+file:///path/to/custom-package#my-custom-package@0.1.0",
1049 "my-custom-bin",
1050 )),
1051 ),
1052 (
1053 r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","targets":[{"name":"my-custom-package","kind":["lib"],"src_path":"/path/to/custom-package/src/main.rs"}]}]}"#,
1054 "/path/to/custom-package/src/main.rs",
1055 None,
1056 ),
1057 ] {
1058 let metadata: CargoMetadata = serde_json::from_str(input).unwrap();
1059
1060 let absolute_path = Path::new(absolute_path);
1061
1062 assert_eq!(
1063 retrieve_package_id_and_bin_name_from_metadata(metadata, absolute_path),
1064 expected.map(|(pkgid, bin)| (pkgid.to_owned(), bin.to_owned()))
1065 );
1066 }
1067 }
1068}