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