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