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