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