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};
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 cx: &mut gpui::AppContext,
429 ) -> Result<TaskVariables> {
430 let local_abs_path = location
431 .buffer
432 .read(cx)
433 .file()
434 .and_then(|file| Some(file.as_local()?.abs_path(cx)));
435
436 let local_abs_path = local_abs_path.as_deref();
437
438 let is_main_function = task_variables
439 .get(&RUST_MAIN_FUNCTION_TASK_VARIABLE)
440 .is_some();
441
442 if is_main_function {
443 if let Some((package_name, bin_name)) = local_abs_path
444 .and_then(|path| package_name_and_bin_name_from_abs_path(path, project_env))
445 {
446 return Ok(TaskVariables::from_iter([
447 (RUST_PACKAGE_TASK_VARIABLE.clone(), package_name),
448 (RUST_BIN_NAME_TASK_VARIABLE.clone(), bin_name),
449 ]));
450 }
451 }
452
453 if let Some(package_name) = local_abs_path
454 .and_then(|local_abs_path| local_abs_path.parent())
455 .and_then(|path| human_readable_package_name(path, project_env))
456 {
457 return Ok(TaskVariables::from_iter([(
458 RUST_PACKAGE_TASK_VARIABLE.clone(),
459 package_name,
460 )]));
461 }
462
463 Ok(TaskVariables::default())
464 }
465
466 fn associated_tasks(
467 &self,
468 file: Option<Arc<dyn language::File>>,
469 cx: &AppContext,
470 ) -> Option<TaskTemplates> {
471 const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
472 let package_to_run = language_settings(Some("Rust".into()), file.as_ref(), cx)
473 .tasks
474 .variables
475 .get(DEFAULT_RUN_NAME_STR)
476 .cloned();
477 let run_task_args = if let Some(package_to_run) = package_to_run {
478 vec!["run".into(), "-p".into(), package_to_run]
479 } else {
480 vec!["run".into()]
481 };
482 Some(TaskTemplates(vec![
483 TaskTemplate {
484 label: format!(
485 "cargo check -p {}",
486 RUST_PACKAGE_TASK_VARIABLE.template_value(),
487 ),
488 command: "cargo".into(),
489 args: vec![
490 "check".into(),
491 "-p".into(),
492 RUST_PACKAGE_TASK_VARIABLE.template_value(),
493 ],
494 cwd: Some("$ZED_DIRNAME".to_owned()),
495 ..TaskTemplate::default()
496 },
497 TaskTemplate {
498 label: "cargo check --workspace --all-targets".into(),
499 command: "cargo".into(),
500 args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
501 cwd: Some("$ZED_DIRNAME".to_owned()),
502 ..TaskTemplate::default()
503 },
504 TaskTemplate {
505 label: format!(
506 "cargo test -p {} {} -- --nocapture",
507 RUST_PACKAGE_TASK_VARIABLE.template_value(),
508 VariableName::Symbol.template_value(),
509 ),
510 command: "cargo".into(),
511 args: vec![
512 "test".into(),
513 "-p".into(),
514 RUST_PACKAGE_TASK_VARIABLE.template_value(),
515 VariableName::Symbol.template_value(),
516 "--".into(),
517 "--nocapture".into(),
518 ],
519 tags: vec!["rust-test".to_owned()],
520 cwd: Some("$ZED_DIRNAME".to_owned()),
521 ..TaskTemplate::default()
522 },
523 TaskTemplate {
524 label: format!(
525 "cargo test -p {} {}",
526 RUST_PACKAGE_TASK_VARIABLE.template_value(),
527 VariableName::Stem.template_value(),
528 ),
529 command: "cargo".into(),
530 args: vec![
531 "test".into(),
532 "-p".into(),
533 RUST_PACKAGE_TASK_VARIABLE.template_value(),
534 VariableName::Stem.template_value(),
535 ],
536 tags: vec!["rust-mod-test".to_owned()],
537 cwd: Some("$ZED_DIRNAME".to_owned()),
538 ..TaskTemplate::default()
539 },
540 TaskTemplate {
541 label: format!(
542 "cargo run -p {} --bin {}",
543 RUST_PACKAGE_TASK_VARIABLE.template_value(),
544 RUST_BIN_NAME_TASK_VARIABLE.template_value(),
545 ),
546 command: "cargo".into(),
547 args: vec![
548 "run".into(),
549 "-p".into(),
550 RUST_PACKAGE_TASK_VARIABLE.template_value(),
551 "--bin".into(),
552 RUST_BIN_NAME_TASK_VARIABLE.template_value(),
553 ],
554 cwd: Some("$ZED_DIRNAME".to_owned()),
555 tags: vec!["rust-main".to_owned()],
556 ..TaskTemplate::default()
557 },
558 TaskTemplate {
559 label: format!(
560 "cargo test -p {}",
561 RUST_PACKAGE_TASK_VARIABLE.template_value()
562 ),
563 command: "cargo".into(),
564 args: vec![
565 "test".into(),
566 "-p".into(),
567 RUST_PACKAGE_TASK_VARIABLE.template_value(),
568 ],
569 cwd: Some("$ZED_DIRNAME".to_owned()),
570 ..TaskTemplate::default()
571 },
572 TaskTemplate {
573 label: "cargo run".into(),
574 command: "cargo".into(),
575 args: run_task_args,
576 cwd: Some("$ZED_DIRNAME".to_owned()),
577 ..TaskTemplate::default()
578 },
579 TaskTemplate {
580 label: "cargo clean".into(),
581 command: "cargo".into(),
582 args: vec!["clean".into()],
583 cwd: Some("$ZED_DIRNAME".to_owned()),
584 ..TaskTemplate::default()
585 },
586 ]))
587 }
588}
589
590/// Part of the data structure of Cargo metadata
591#[derive(serde::Deserialize)]
592struct CargoMetadata {
593 packages: Vec<CargoPackage>,
594}
595
596#[derive(serde::Deserialize)]
597struct CargoPackage {
598 id: String,
599 targets: Vec<CargoTarget>,
600}
601
602#[derive(serde::Deserialize)]
603struct CargoTarget {
604 name: String,
605 kind: Vec<String>,
606 src_path: String,
607}
608
609fn package_name_and_bin_name_from_abs_path(
610 abs_path: &Path,
611 project_env: Option<&HashMap<String, String>>,
612) -> Option<(String, String)> {
613 let mut command = std::process::Command::new("cargo");
614 if let Some(envs) = project_env {
615 command.envs(envs);
616 }
617 let output = command
618 .current_dir(abs_path.parent()?)
619 .arg("metadata")
620 .arg("--no-deps")
621 .arg("--format-version")
622 .arg("1")
623 .output()
624 .log_err()?
625 .stdout;
626
627 let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
628
629 retrieve_package_id_and_bin_name_from_metadata(metadata, abs_path).and_then(
630 |(package_id, bin_name)| {
631 let package_name = package_name_from_pkgid(&package_id);
632
633 package_name.map(|package_name| (package_name.to_owned(), bin_name))
634 },
635 )
636}
637
638fn retrieve_package_id_and_bin_name_from_metadata(
639 metadata: CargoMetadata,
640 abs_path: &Path,
641) -> Option<(String, String)> {
642 for package in metadata.packages {
643 for target in package.targets {
644 let is_bin = target.kind.iter().any(|kind| kind == "bin");
645 let target_path = PathBuf::from(target.src_path);
646 if target_path == abs_path && is_bin {
647 return Some((package.id, target.name));
648 }
649 }
650 }
651
652 None
653}
654
655fn human_readable_package_name(
656 package_directory: &Path,
657 project_env: Option<&HashMap<String, String>>,
658) -> Option<String> {
659 let mut command = std::process::Command::new("cargo");
660 if let Some(envs) = project_env {
661 command.envs(envs);
662 }
663
664 let pkgid = String::from_utf8(
665 command
666 .current_dir(package_directory)
667 .arg("pkgid")
668 .output()
669 .log_err()?
670 .stdout,
671 )
672 .ok()?;
673 Some(package_name_from_pkgid(&pkgid)?.to_owned())
674}
675
676// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
677// Output example in the root of Zed project:
678// ```sh
679// ❯ cargo pkgid zed
680// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
681// ```
682// Another variant, if a project has a custom package name or hyphen in the name:
683// ```
684// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
685// ```
686//
687// Extracts the package name from the output according to the spec:
688// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
689fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
690 fn split_off_suffix(input: &str, suffix_start: char) -> &str {
691 match input.rsplit_once(suffix_start) {
692 Some((without_suffix, _)) => without_suffix,
693 None => input,
694 }
695 }
696
697 let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
698 let package_name = match version_suffix.rsplit_once('@') {
699 Some((custom_package_name, _version)) => custom_package_name,
700 None => {
701 let host_and_path = split_off_suffix(version_prefix, '?');
702 let (_, package_name) = host_and_path.rsplit_once('/')?;
703 package_name
704 }
705 };
706 Some(package_name)
707}
708
709async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
710 maybe!(async {
711 let mut last = None;
712 let mut entries = fs::read_dir(&container_dir).await?;
713 while let Some(entry) = entries.next().await {
714 last = Some(entry?.path());
715 }
716
717 anyhow::Ok(LanguageServerBinary {
718 path: last.ok_or_else(|| anyhow!("no cached binary"))?,
719 env: None,
720 arguments: Default::default(),
721 })
722 })
723 .await
724 .log_err()
725}
726
727#[cfg(test)]
728mod tests {
729 use std::num::NonZeroU32;
730
731 use super::*;
732 use crate::language;
733 use gpui::{BorrowAppContext, Context, Hsla, TestAppContext};
734 use language::language_settings::AllLanguageSettings;
735 use lsp::CompletionItemLabelDetails;
736 use settings::SettingsStore;
737 use theme::SyntaxTheme;
738
739 #[gpui::test]
740 async fn test_process_rust_diagnostics() {
741 let mut params = lsp::PublishDiagnosticsParams {
742 uri: lsp::Url::from_file_path("/a").unwrap(),
743 version: None,
744 diagnostics: vec![
745 // no newlines
746 lsp::Diagnostic {
747 message: "use of moved value `a`".to_string(),
748 ..Default::default()
749 },
750 // newline at the end of a code span
751 lsp::Diagnostic {
752 message: "consider importing this struct: `use b::c;\n`".to_string(),
753 ..Default::default()
754 },
755 // code span starting right after a newline
756 lsp::Diagnostic {
757 message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
758 .to_string(),
759 ..Default::default()
760 },
761 ],
762 };
763 RustLspAdapter.process_diagnostics(&mut params);
764
765 assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
766
767 // remove trailing newline from code span
768 assert_eq!(
769 params.diagnostics[1].message,
770 "consider importing this struct: `use b::c;`"
771 );
772
773 // do not remove newline before the start of code span
774 assert_eq!(
775 params.diagnostics[2].message,
776 "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
777 );
778 }
779
780 #[gpui::test]
781 async fn test_rust_label_for_completion() {
782 let adapter = Arc::new(RustLspAdapter);
783 let language = language("rust", tree_sitter_rust::LANGUAGE.into());
784 let grammar = language.grammar().unwrap();
785 let theme = SyntaxTheme::new_test([
786 ("type", Hsla::default()),
787 ("keyword", Hsla::default()),
788 ("function", Hsla::default()),
789 ("property", Hsla::default()),
790 ]);
791
792 language.set_theme(&theme);
793
794 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
795 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
796 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
797 let highlight_field = grammar.highlight_id_for_name("property").unwrap();
798
799 assert_eq!(
800 adapter
801 .label_for_completion(
802 &lsp::CompletionItem {
803 kind: Some(lsp::CompletionItemKind::FUNCTION),
804 label: "hello(…)".to_string(),
805 label_details: Some(CompletionItemLabelDetails {
806 detail: Some(" (use crate::foo)".into()),
807 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string())
808 }),
809 ..Default::default()
810 },
811 &language
812 )
813 .await,
814 Some(CodeLabel {
815 text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
816 filter_range: 0..5,
817 runs: vec![
818 (0..5, highlight_function),
819 (7..10, highlight_keyword),
820 (11..17, highlight_type),
821 (18..19, highlight_type),
822 (25..28, highlight_type),
823 (29..30, highlight_type),
824 ],
825 })
826 );
827 assert_eq!(
828 adapter
829 .label_for_completion(
830 &lsp::CompletionItem {
831 kind: Some(lsp::CompletionItemKind::FUNCTION),
832 label: "hello(…)".to_string(),
833 label_details: Some(CompletionItemLabelDetails {
834 detail: Some(" (use crate::foo)".into()),
835 description: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
836 }),
837 ..Default::default()
838 },
839 &language
840 )
841 .await,
842 Some(CodeLabel {
843 text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
844 filter_range: 0..5,
845 runs: vec![
846 (0..5, highlight_function),
847 (7..10, highlight_keyword),
848 (11..17, highlight_type),
849 (18..19, highlight_type),
850 (25..28, highlight_type),
851 (29..30, highlight_type),
852 ],
853 })
854 );
855 assert_eq!(
856 adapter
857 .label_for_completion(
858 &lsp::CompletionItem {
859 kind: Some(lsp::CompletionItemKind::FIELD),
860 label: "len".to_string(),
861 detail: Some("usize".to_string()),
862 ..Default::default()
863 },
864 &language
865 )
866 .await,
867 Some(CodeLabel {
868 text: "len: usize".to_string(),
869 filter_range: 0..3,
870 runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
871 })
872 );
873
874 assert_eq!(
875 adapter
876 .label_for_completion(
877 &lsp::CompletionItem {
878 kind: Some(lsp::CompletionItemKind::FUNCTION),
879 label: "hello(…)".to_string(),
880 label_details: Some(CompletionItemLabelDetails {
881 detail: Some(" (use crate::foo)".to_string()),
882 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
883 }),
884
885 ..Default::default()
886 },
887 &language
888 )
889 .await,
890 Some(CodeLabel {
891 text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
892 filter_range: 0..5,
893 runs: vec![
894 (0..5, highlight_function),
895 (7..10, highlight_keyword),
896 (11..17, highlight_type),
897 (18..19, highlight_type),
898 (25..28, highlight_type),
899 (29..30, highlight_type),
900 ],
901 })
902 );
903 }
904
905 #[gpui::test]
906 async fn test_rust_label_for_symbol() {
907 let adapter = Arc::new(RustLspAdapter);
908 let language = language("rust", tree_sitter_rust::LANGUAGE.into());
909 let grammar = language.grammar().unwrap();
910 let theme = SyntaxTheme::new_test([
911 ("type", Hsla::default()),
912 ("keyword", Hsla::default()),
913 ("function", Hsla::default()),
914 ("property", Hsla::default()),
915 ]);
916
917 language.set_theme(&theme);
918
919 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
920 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
921 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
922
923 assert_eq!(
924 adapter
925 .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
926 .await,
927 Some(CodeLabel {
928 text: "fn hello".to_string(),
929 filter_range: 3..8,
930 runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
931 })
932 );
933
934 assert_eq!(
935 adapter
936 .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
937 .await,
938 Some(CodeLabel {
939 text: "type World".to_string(),
940 filter_range: 5..10,
941 runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
942 })
943 );
944 }
945
946 #[gpui::test]
947 async fn test_rust_autoindent(cx: &mut TestAppContext) {
948 // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
949 cx.update(|cx| {
950 let test_settings = SettingsStore::test(cx);
951 cx.set_global(test_settings);
952 language::init(cx);
953 cx.update_global::<SettingsStore, _>(|store, cx| {
954 store.update_user_settings::<AllLanguageSettings>(cx, |s| {
955 s.defaults.tab_size = NonZeroU32::new(2);
956 });
957 });
958 });
959
960 let language = crate::language("rust", tree_sitter_rust::LANGUAGE.into());
961
962 cx.new_model(|cx| {
963 let mut buffer = Buffer::local("", cx).with_language(language, cx);
964
965 // indent between braces
966 buffer.set_text("fn a() {}", cx);
967 let ix = buffer.len() - 1;
968 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
969 assert_eq!(buffer.text(), "fn a() {\n \n}");
970
971 // indent between braces, even after empty lines
972 buffer.set_text("fn a() {\n\n\n}", cx);
973 let ix = buffer.len() - 2;
974 buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
975 assert_eq!(buffer.text(), "fn a() {\n\n\n \n}");
976
977 // indent a line that continues a field expression
978 buffer.set_text("fn a() {\n \n}", cx);
979 let ix = buffer.len() - 2;
980 buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
981 assert_eq!(buffer.text(), "fn a() {\n b\n .c\n}");
982
983 // indent further lines that continue the field expression, even after empty lines
984 let ix = buffer.len() - 2;
985 buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
986 assert_eq!(buffer.text(), "fn a() {\n b\n .c\n \n .d\n}");
987
988 // dedent the line after the field expression
989 let ix = buffer.len() - 2;
990 buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
991 assert_eq!(
992 buffer.text(),
993 "fn a() {\n b\n .c\n \n .d;\n e\n}"
994 );
995
996 // indent inside a struct within a call
997 buffer.set_text("const a: B = c(D {});", cx);
998 let ix = buffer.len() - 3;
999 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1000 assert_eq!(buffer.text(), "const a: B = c(D {\n \n});");
1001
1002 // indent further inside a nested call
1003 let ix = buffer.len() - 4;
1004 buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
1005 assert_eq!(buffer.text(), "const a: B = c(D {\n e: f(\n \n )\n});");
1006
1007 // keep that indent after an empty line
1008 let ix = buffer.len() - 8;
1009 buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1010 assert_eq!(
1011 buffer.text(),
1012 "const a: B = c(D {\n e: f(\n \n \n )\n});"
1013 );
1014
1015 buffer
1016 });
1017 }
1018
1019 #[test]
1020 fn test_package_name_from_pkgid() {
1021 for (input, expected) in [
1022 (
1023 "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
1024 "zed",
1025 ),
1026 (
1027 "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
1028 "my-custom-package",
1029 ),
1030 ] {
1031 assert_eq!(package_name_from_pkgid(input), Some(expected));
1032 }
1033 }
1034
1035 #[test]
1036 fn test_retrieve_package_id_and_bin_name_from_metadata() {
1037 for (input, absolute_path, expected) in [
1038 (
1039 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"}]}]}"#,
1040 "/path/to/zed/src/main.rs",
1041 Some(("path+file:///path/to/zed/crates/zed#0.131.0", "zed")),
1042 ),
1043 (
1044 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"}]}]}"#,
1045 "/path/to/custom-package/src/main.rs",
1046 Some((
1047 "path+file:///path/to/custom-package#my-custom-package@0.1.0",
1048 "my-custom-bin",
1049 )),
1050 ),
1051 (
1052 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"}]}]}"#,
1053 "/path/to/custom-package/src/main.rs",
1054 None,
1055 ),
1056 ] {
1057 let metadata: CargoMetadata = serde_json::from_str(input).unwrap();
1058
1059 let absolute_path = Path::new(absolute_path);
1060
1061 assert_eq!(
1062 retrieve_package_id_and_bin_name_from_metadata(metadata, absolute_path),
1063 expected.map(|(pkgid, bin)| (pkgid.to_owned(), bin.to_owned()))
1064 );
1065 }
1066 }
1067}