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