1use anyhow::{Context as _, Result};
2use async_trait::async_trait;
3use collections::HashMap;
4use futures::StreamExt;
5use gpui::{App, AppContext, AsyncApp, SharedString, Task};
6use http_client::github::AssetKind;
7use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
8pub use language::*;
9use lsp::{InitializeParams, LanguageServerBinary};
10use project::lsp_store::rust_analyzer_ext::CARGO_DIAGNOSTICS_SOURCE_NAME;
11use project::project_settings::ProjectSettings;
12use regex::Regex;
13use serde_json::json;
14use settings::Settings as _;
15use smol::fs::{self};
16use std::fmt::Display;
17use std::ops::Range;
18use std::{
19 borrow::Cow,
20 path::{Path, PathBuf},
21 sync::{Arc, LazyLock},
22};
23use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
24use util::fs::{make_file_executable, remove_matching};
25use util::merge_json_value_into;
26use util::{ResultExt, maybe};
27
28use crate::github_download::{GithubBinaryMetadata, download_server_binary};
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
108 }
109
110 fn disk_based_diagnostic_sources(&self) -> Vec<String> {
111 vec![CARGO_DIAGNOSTICS_SOURCE_NAME.to_owned()]
112 }
113
114 fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
115 Some("rust-analyzer/flycheck".into())
116 }
117
118 fn process_diagnostics(
119 &self,
120 params: &mut lsp::PublishDiagnosticsParams,
121 _: LanguageServerId,
122 _: Option<&'_ Buffer>,
123 ) {
124 static REGEX: LazyLock<Regex> =
125 LazyLock::new(|| Regex::new(r"(?m)`([^`]+)\n`$").expect("Failed to create REGEX"));
126
127 for diagnostic in &mut params.diagnostics {
128 for message in diagnostic
129 .related_information
130 .iter_mut()
131 .flatten()
132 .map(|info| &mut info.message)
133 .chain([&mut diagnostic.message])
134 {
135 if let Cow::Owned(sanitized) = REGEX.replace_all(message, "`$1`") {
136 *message = sanitized;
137 }
138 }
139 }
140 }
141
142 fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
143 static REGEX: LazyLock<Regex> =
144 LazyLock::new(|| Regex::new(r"(?m)\n *").expect("Failed to create REGEX"));
145 Some(REGEX.replace_all(message, "\n\n").to_string())
146 }
147
148 async fn label_for_completion(
149 &self,
150 completion: &lsp::CompletionItem,
151 language: &Arc<Language>,
152 ) -> Option<CodeLabel> {
153 // rust-analyzer calls these detail left and detail right in terms of where it expects things to be rendered
154 // this usually contains signatures of the thing to be completed
155 let detail_right = completion
156 .label_details
157 .as_ref()
158 .and_then(|detail| detail.description.as_ref())
159 .or(completion.detail.as_ref())
160 .map(|detail| detail.trim());
161 // this tends to contain alias and import information
162 let detail_left = completion
163 .label_details
164 .as_ref()
165 .and_then(|detail| detail.detail.as_deref());
166 let mk_label = |text: String, filter_range: &dyn Fn() -> Range<usize>, runs| {
167 let filter_range = completion
168 .filter_text
169 .as_deref()
170 .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
171 .or_else(|| {
172 text.find(&completion.label)
173 .map(|ix| ix..ix + completion.label.len())
174 })
175 .unwrap_or_else(filter_range);
176
177 CodeLabel {
178 text,
179 runs,
180 filter_range,
181 }
182 };
183 let mut label = match (detail_right, completion.kind) {
184 (Some(signature), Some(lsp::CompletionItemKind::FIELD)) => {
185 let name = &completion.label;
186 let text = format!("{name}: {signature}");
187 let prefix = "struct S { ";
188 let source = Rope::from_iter([prefix, &text, " }"]);
189 let runs =
190 language.highlight_text(&source, prefix.len()..prefix.len() + text.len());
191 mk_label(text, &|| 0..completion.label.len(), runs)
192 }
193 (
194 Some(signature),
195 Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE),
196 ) if completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) => {
197 let name = &completion.label;
198 let text = format!("{name}: {signature}",);
199 let prefix = "let ";
200 let source = Rope::from_iter([prefix, &text, " = ();"]);
201 let runs =
202 language.highlight_text(&source, prefix.len()..prefix.len() + text.len());
203 mk_label(text, &|| 0..completion.label.len(), runs)
204 }
205 (
206 function_signature,
207 Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD),
208 ) => {
209 const FUNCTION_PREFIXES: [&str; 6] = [
210 "async fn",
211 "async unsafe fn",
212 "const fn",
213 "const unsafe fn",
214 "unsafe fn",
215 "fn",
216 ];
217 let fn_prefixed = FUNCTION_PREFIXES.iter().find_map(|&prefix| {
218 function_signature?
219 .strip_prefix(prefix)
220 .map(|suffix| (prefix, suffix))
221 });
222 let label = if let Some(label) = completion
223 .label
224 .strip_suffix("(…)")
225 .or_else(|| completion.label.strip_suffix("()"))
226 {
227 label
228 } else {
229 &completion.label
230 };
231
232 static FULL_SIGNATURE_REGEX: LazyLock<Regex> =
233 LazyLock::new(|| Regex::new(r"fn (.?+)\(").expect("Failed to create REGEX"));
234 if let Some((function_signature, match_)) = function_signature
235 .filter(|it| it.contains(&label))
236 .and_then(|it| Some((it, FULL_SIGNATURE_REGEX.find(it)?)))
237 {
238 let source = Rope::from(function_signature);
239 let runs = language.highlight_text(&source, 0..function_signature.len());
240 mk_label(
241 function_signature.to_owned(),
242 &|| match_.range().start - 3..match_.range().end - 1,
243 runs,
244 )
245 } else if let Some((prefix, suffix)) = fn_prefixed {
246 let text = format!("{label}{suffix}");
247 let source = Rope::from_iter([prefix, " ", &text, " {}"]);
248 let run_start = prefix.len() + 1;
249 let runs = language.highlight_text(&source, run_start..run_start + text.len());
250 mk_label(text, &|| 0..label.len(), runs)
251 } else if completion
252 .detail
253 .as_ref()
254 .is_some_and(|detail| detail.starts_with("macro_rules! "))
255 {
256 let text = completion.label.clone();
257 let len = text.len();
258 let source = Rope::from(text.as_str());
259 let runs = language.highlight_text(&source, 0..len);
260 mk_label(text, &|| 0..completion.label.len(), runs)
261 } else if detail_left.is_none() {
262 return None;
263 } else {
264 mk_label(
265 completion.label.clone(),
266 &|| 0..completion.label.len(),
267 vec![],
268 )
269 }
270 }
271 (_, kind) => {
272 let highlight_name = kind.and_then(|kind| match kind {
273 lsp::CompletionItemKind::STRUCT
274 | lsp::CompletionItemKind::INTERFACE
275 | lsp::CompletionItemKind::ENUM => Some("type"),
276 lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
277 lsp::CompletionItemKind::KEYWORD => Some("keyword"),
278 lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
279 Some("constant")
280 }
281 _ => None,
282 });
283
284 let label = completion.label.clone();
285 let mut runs = vec![];
286 if let Some(highlight_name) = highlight_name {
287 let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name)?;
288 runs.push((
289 0..label.rfind('(').unwrap_or(completion.label.len()),
290 highlight_id,
291 ));
292 } else if detail_left.is_none() {
293 return None;
294 }
295
296 mk_label(label, &|| 0..completion.label.len(), runs)
297 }
298 };
299
300 if let Some(detail_left) = detail_left {
301 label.text.push(' ');
302 if !detail_left.starts_with('(') {
303 label.text.push('(');
304 }
305 label.text.push_str(detail_left);
306 if !detail_left.ends_with(')') {
307 label.text.push(')');
308 }
309 }
310 Some(label)
311 }
312
313 async fn label_for_symbol(
314 &self,
315 name: &str,
316 kind: lsp::SymbolKind,
317 language: &Arc<Language>,
318 ) -> Option<CodeLabel> {
319 let (prefix, suffix) = match kind {
320 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => ("fn ", " () {}"),
321 lsp::SymbolKind::STRUCT => ("struct ", " {}"),
322 lsp::SymbolKind::ENUM => ("enum ", " {}"),
323 lsp::SymbolKind::INTERFACE => ("trait ", " {}"),
324 lsp::SymbolKind::CONSTANT => ("const ", ": () = ();"),
325 lsp::SymbolKind::MODULE => ("mod ", " {}"),
326 lsp::SymbolKind::TYPE_PARAMETER => ("type ", " {}"),
327 _ => return None,
328 };
329
330 let filter_range = prefix.len()..prefix.len() + name.len();
331 let display_range = 0..filter_range.end;
332 Some(CodeLabel {
333 runs: language.highlight_text(&Rope::from_iter([prefix, name, suffix]), display_range),
334 text: format!("{prefix}{name}"),
335 filter_range,
336 })
337 }
338
339 fn prepare_initialize_params(
340 &self,
341 mut original: InitializeParams,
342 cx: &App,
343 ) -> Result<InitializeParams> {
344 let enable_lsp_tasks = ProjectSettings::get_global(cx)
345 .lsp
346 .get(&SERVER_NAME)
347 .is_some_and(|s| s.enable_lsp_tasks);
348 if enable_lsp_tasks {
349 let experimental = json!({
350 "runnables": {
351 "kinds": [ "cargo", "shell" ],
352 },
353 });
354 if let Some(original_experimental) = &mut original.capabilities.experimental {
355 merge_json_value_into(experimental, original_experimental);
356 } else {
357 original.capabilities.experimental = Some(experimental);
358 }
359 }
360
361 Ok(original)
362 }
363}
364
365impl LspInstaller for RustLspAdapter {
366 type BinaryVersion = GitHubLspBinaryVersion;
367 async fn check_if_user_installed(
368 &self,
369 delegate: &dyn LspAdapterDelegate,
370 _: Option<Toolchain>,
371 _: &AsyncApp,
372 ) -> Option<LanguageServerBinary> {
373 let path = delegate.which("rust-analyzer".as_ref()).await?;
374 let env = delegate.shell_env().await;
375
376 // It is surprisingly common for ~/.cargo/bin/rust-analyzer to be a symlink to
377 // /usr/bin/rust-analyzer that fails when you run it; so we need to test it.
378 log::info!("found rust-analyzer in PATH. trying to run `rust-analyzer --help`");
379 let result = delegate
380 .try_exec(LanguageServerBinary {
381 path: path.clone(),
382 arguments: vec!["--help".into()],
383 env: Some(env.clone()),
384 })
385 .await;
386 if let Err(err) = result {
387 log::debug!(
388 "failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {}",
389 path,
390 err
391 );
392 return None;
393 }
394
395 Some(LanguageServerBinary {
396 path,
397 env: Some(env),
398 arguments: vec![],
399 })
400 }
401
402 async fn fetch_latest_server_version(
403 &self,
404 delegate: &dyn LspAdapterDelegate,
405 pre_release: bool,
406 _: &mut AsyncApp,
407 ) -> Result<GitHubLspBinaryVersion> {
408 let release = latest_github_release(
409 "rust-lang/rust-analyzer",
410 true,
411 pre_release,
412 delegate.http_client(),
413 )
414 .await?;
415 let asset_name = Self::build_asset_name();
416 let asset = release
417 .assets
418 .into_iter()
419 .find(|asset| asset.name == asset_name)
420 .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
421 Ok(GitHubLspBinaryVersion {
422 name: release.tag_name,
423 url: asset.browser_download_url,
424 digest: asset.digest,
425 })
426 }
427
428 async fn fetch_server_binary(
429 &self,
430 version: GitHubLspBinaryVersion,
431 container_dir: PathBuf,
432 delegate: &dyn LspAdapterDelegate,
433 ) -> Result<LanguageServerBinary> {
434 let GitHubLspBinaryVersion {
435 name,
436 url,
437 digest: expected_digest,
438 } = version;
439 let destination_path = container_dir.join(format!("rust-analyzer-{name}"));
440 let server_path = match Self::GITHUB_ASSET_KIND {
441 AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place.
442 AssetKind::Zip => destination_path.clone().join("rust-analyzer.exe"), // zip contains a .exe
443 };
444
445 let binary = LanguageServerBinary {
446 path: server_path.clone(),
447 env: None,
448 arguments: Default::default(),
449 };
450
451 let metadata_path = destination_path.with_extension("metadata");
452 let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
453 .await
454 .ok();
455 if let Some(metadata) = metadata {
456 let validity_check = async || {
457 delegate
458 .try_exec(LanguageServerBinary {
459 path: server_path.clone(),
460 arguments: vec!["--version".into()],
461 env: None,
462 })
463 .await
464 .inspect_err(|err| {
465 log::warn!("Unable to run {server_path:?} asset, redownloading: {err}",)
466 })
467 };
468 if let (Some(actual_digest), Some(expected_digest)) =
469 (&metadata.digest, &expected_digest)
470 {
471 if actual_digest == expected_digest {
472 if validity_check().await.is_ok() {
473 return Ok(binary);
474 }
475 } else {
476 log::info!(
477 "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
478 );
479 }
480 } else if validity_check().await.is_ok() {
481 return Ok(binary);
482 }
483 }
484
485 download_server_binary(
486 delegate,
487 &url,
488 expected_digest.as_deref(),
489 &destination_path,
490 Self::GITHUB_ASSET_KIND,
491 )
492 .await?;
493 make_file_executable(&server_path).await?;
494 remove_matching(&container_dir, |path| path != destination_path).await;
495 GithubBinaryMetadata::write_to_file(
496 &GithubBinaryMetadata {
497 metadata_version: 1,
498 digest: expected_digest,
499 },
500 &metadata_path,
501 )
502 .await?;
503
504 Ok(LanguageServerBinary {
505 path: server_path,
506 env: None,
507 arguments: Default::default(),
508 })
509 }
510
511 async fn cached_server_binary(
512 &self,
513 container_dir: PathBuf,
514 _: &dyn LspAdapterDelegate,
515 ) -> Option<LanguageServerBinary> {
516 get_cached_server_binary(container_dir).await
517 }
518}
519
520pub(crate) struct RustContextProvider;
521
522const RUST_PACKAGE_TASK_VARIABLE: VariableName =
523 VariableName::Custom(Cow::Borrowed("RUST_PACKAGE"));
524
525/// The bin name corresponding to the current file in Cargo.toml
526const RUST_BIN_NAME_TASK_VARIABLE: VariableName =
527 VariableName::Custom(Cow::Borrowed("RUST_BIN_NAME"));
528
529/// The bin kind (bin/example) corresponding to the current file in Cargo.toml
530const RUST_BIN_KIND_TASK_VARIABLE: VariableName =
531 VariableName::Custom(Cow::Borrowed("RUST_BIN_KIND"));
532
533/// The flag to list required features for executing a bin, if any
534const RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE: VariableName =
535 VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES_FLAG"));
536
537/// The list of required features for executing a bin, if any
538const RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE: VariableName =
539 VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES"));
540
541const RUST_TEST_FRAGMENT_TASK_VARIABLE: VariableName =
542 VariableName::Custom(Cow::Borrowed("RUST_TEST_FRAGMENT"));
543
544const RUST_DOC_TEST_NAME_TASK_VARIABLE: VariableName =
545 VariableName::Custom(Cow::Borrowed("RUST_DOC_TEST_NAME"));
546
547const RUST_TEST_NAME_TASK_VARIABLE: VariableName =
548 VariableName::Custom(Cow::Borrowed("RUST_TEST_NAME"));
549
550const RUST_MANIFEST_DIRNAME_TASK_VARIABLE: VariableName =
551 VariableName::Custom(Cow::Borrowed("RUST_MANIFEST_DIRNAME"));
552
553impl ContextProvider for RustContextProvider {
554 fn build_context(
555 &self,
556 task_variables: &TaskVariables,
557 location: ContextLocation<'_>,
558 project_env: Option<HashMap<String, String>>,
559 _: Arc<dyn LanguageToolchainStore>,
560 cx: &mut gpui::App,
561 ) -> Task<Result<TaskVariables>> {
562 let local_abs_path = location
563 .file_location
564 .buffer
565 .read(cx)
566 .file()
567 .and_then(|file| Some(file.as_local()?.abs_path(cx)));
568
569 let mut variables = TaskVariables::default();
570
571 if let (Some(path), Some(stem)) = (&local_abs_path, task_variables.get(&VariableName::Stem))
572 {
573 let fragment = test_fragment(&variables, path, stem);
574 variables.insert(RUST_TEST_FRAGMENT_TASK_VARIABLE, fragment);
575 };
576 if let Some(test_name) =
577 task_variables.get(&VariableName::Custom(Cow::Borrowed("_test_name")))
578 {
579 variables.insert(RUST_TEST_NAME_TASK_VARIABLE, test_name.into());
580 }
581 if let Some(doc_test_name) =
582 task_variables.get(&VariableName::Custom(Cow::Borrowed("_doc_test_name")))
583 {
584 variables.insert(RUST_DOC_TEST_NAME_TASK_VARIABLE, doc_test_name.into());
585 }
586 cx.background_spawn(async move {
587 if let Some(path) = local_abs_path
588 .as_deref()
589 .and_then(|local_abs_path| local_abs_path.parent())
590 && let Some(package_name) =
591 human_readable_package_name(path, project_env.as_ref()).await
592 {
593 variables.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name);
594 }
595 if let Some(path) = local_abs_path.as_ref()
596 && let Some((target, manifest_path)) =
597 target_info_from_abs_path(path, project_env.as_ref()).await
598 {
599 if let Some(target) = target {
600 variables.extend(TaskVariables::from_iter([
601 (RUST_PACKAGE_TASK_VARIABLE.clone(), target.package_name),
602 (RUST_BIN_NAME_TASK_VARIABLE.clone(), target.target_name),
603 (
604 RUST_BIN_KIND_TASK_VARIABLE.clone(),
605 target.target_kind.to_string(),
606 ),
607 ]));
608 if target.required_features.is_empty() {
609 variables.insert(RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE, "".into());
610 variables.insert(RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE, "".into());
611 } else {
612 variables.insert(
613 RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.clone(),
614 "--features".to_string(),
615 );
616 variables.insert(
617 RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.clone(),
618 target.required_features.join(","),
619 );
620 }
621 }
622 variables.extend(TaskVariables::from_iter([(
623 RUST_MANIFEST_DIRNAME_TASK_VARIABLE.clone(),
624 manifest_path.to_string_lossy().into_owned(),
625 )]));
626 }
627 Ok(variables)
628 })
629 }
630
631 fn associated_tasks(
632 &self,
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 {
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(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
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(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
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(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
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(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
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(Debug, serde::Deserialize)]
813struct CargoMetadata {
814 packages: Vec<CargoPackage>,
815}
816
817#[derive(Debug, serde::Deserialize)]
818struct CargoPackage {
819 id: String,
820 targets: Vec<CargoTarget>,
821 manifest_path: Arc<Path>,
822}
823
824#[derive(Debug, serde::Deserialize)]
825struct CargoTarget {
826 name: String,
827 kind: Vec<String>,
828 src_path: String,
829 #[serde(rename = "required-features", default)]
830 required_features: Vec<String>,
831}
832
833#[derive(Debug, PartialEq)]
834enum TargetKind {
835 Bin,
836 Example,
837}
838
839impl Display for TargetKind {
840 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
841 match self {
842 TargetKind::Bin => write!(f, "bin"),
843 TargetKind::Example => write!(f, "example"),
844 }
845 }
846}
847
848impl TryFrom<&str> for TargetKind {
849 type Error = ();
850 fn try_from(value: &str) -> Result<Self, ()> {
851 match value {
852 "bin" => Ok(Self::Bin),
853 "example" => Ok(Self::Example),
854 _ => Err(()),
855 }
856 }
857}
858/// Which package and binary target are we in?
859#[derive(Debug, PartialEq)]
860struct TargetInfo {
861 package_name: String,
862 target_name: String,
863 target_kind: TargetKind,
864 required_features: Vec<String>,
865}
866
867async fn target_info_from_abs_path(
868 abs_path: &Path,
869 project_env: Option<&HashMap<String, String>>,
870) -> Option<(Option<TargetInfo>, Arc<Path>)> {
871 let mut command = util::command::new_smol_command("cargo");
872 if let Some(envs) = project_env {
873 command.envs(envs);
874 }
875 let output = command
876 .current_dir(abs_path.parent()?)
877 .arg("metadata")
878 .arg("--no-deps")
879 .arg("--format-version")
880 .arg("1")
881 .output()
882 .await
883 .log_err()?
884 .stdout;
885
886 let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
887 target_info_from_metadata(metadata, abs_path)
888}
889
890fn target_info_from_metadata(
891 metadata: CargoMetadata,
892 abs_path: &Path,
893) -> Option<(Option<TargetInfo>, Arc<Path>)> {
894 let mut manifest_path = None;
895 for package in metadata.packages {
896 let Some(manifest_dir_path) = package.manifest_path.parent() else {
897 continue;
898 };
899
900 let Some(path_from_manifest_dir) = abs_path.strip_prefix(manifest_dir_path).ok() else {
901 continue;
902 };
903 let candidate_path_length = path_from_manifest_dir.components().count();
904 // Pick the most specific manifest path
905 if let Some((path, current_length)) = &mut manifest_path {
906 if candidate_path_length > *current_length {
907 *path = Arc::from(manifest_dir_path);
908 *current_length = candidate_path_length;
909 }
910 } else {
911 manifest_path = Some((Arc::from(manifest_dir_path), candidate_path_length));
912 };
913
914 for target in package.targets {
915 let Some(bin_kind) = target
916 .kind
917 .iter()
918 .find_map(|kind| TargetKind::try_from(kind.as_ref()).ok())
919 else {
920 continue;
921 };
922 let target_path = PathBuf::from(target.src_path);
923 if target_path == abs_path {
924 return manifest_path.map(|(path, _)| {
925 (
926 package_name_from_pkgid(&package.id).map(|package_name| TargetInfo {
927 package_name: package_name.to_owned(),
928 target_name: target.name,
929 required_features: target.required_features,
930 target_kind: bin_kind,
931 }),
932 path,
933 )
934 });
935 }
936 }
937 }
938
939 manifest_path.map(|(path, _)| (None, path))
940}
941
942async fn human_readable_package_name(
943 package_directory: &Path,
944 project_env: Option<&HashMap<String, String>>,
945) -> Option<String> {
946 let mut command = util::command::new_smol_command("cargo");
947 if let Some(envs) = project_env {
948 command.envs(envs);
949 }
950 let pkgid = String::from_utf8(
951 command
952 .current_dir(package_directory)
953 .arg("pkgid")
954 .output()
955 .await
956 .log_err()?
957 .stdout,
958 )
959 .ok()?;
960 Some(package_name_from_pkgid(&pkgid)?.to_owned())
961}
962
963// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
964// Output example in the root of Zed project:
965// ```sh
966// ❯ cargo pkgid zed
967// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
968// ```
969// Another variant, if a project has a custom package name or hyphen in the name:
970// ```
971// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
972// ```
973//
974// Extracts the package name from the output according to the spec:
975// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
976fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
977 fn split_off_suffix(input: &str, suffix_start: char) -> &str {
978 match input.rsplit_once(suffix_start) {
979 Some((without_suffix, _)) => without_suffix,
980 None => input,
981 }
982 }
983
984 let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
985 let package_name = match version_suffix.rsplit_once('@') {
986 Some((custom_package_name, _version)) => custom_package_name,
987 None => {
988 let host_and_path = split_off_suffix(version_prefix, '?');
989 let (_, package_name) = host_and_path.rsplit_once('/')?;
990 package_name
991 }
992 };
993 Some(package_name)
994}
995
996async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
997 maybe!(async {
998 let mut last = None;
999 let mut entries = fs::read_dir(&container_dir).await?;
1000 while let Some(entry) = entries.next().await {
1001 let path = entry?.path();
1002 if path.extension().is_some_and(|ext| ext == "metadata") {
1003 continue;
1004 }
1005 last = Some(path);
1006 }
1007
1008 let path = last.context("no cached binary")?;
1009 let path = match RustLspAdapter::GITHUB_ASSET_KIND {
1010 AssetKind::TarGz | AssetKind::Gz => path, // Tar and gzip extract in place.
1011 AssetKind::Zip => path.join("rust-analyzer.exe"), // zip contains a .exe
1012 };
1013
1014 anyhow::Ok(LanguageServerBinary {
1015 path,
1016 env: None,
1017 arguments: Default::default(),
1018 })
1019 })
1020 .await
1021 .log_err()
1022}
1023
1024fn test_fragment(variables: &TaskVariables, path: &Path, stem: &str) -> String {
1025 let fragment = if stem == "lib" {
1026 // This isn't quite right---it runs the tests for the entire library, rather than
1027 // just for the top-level `mod tests`. But we don't really have the means here to
1028 // filter out just that module.
1029 Some("--lib".to_owned())
1030 } else if stem == "mod" {
1031 maybe!({ Some(path.parent()?.file_name()?.to_string_lossy().to_string()) })
1032 } else if stem == "main" {
1033 if let (Some(bin_name), Some(bin_kind)) = (
1034 variables.get(&RUST_BIN_NAME_TASK_VARIABLE),
1035 variables.get(&RUST_BIN_KIND_TASK_VARIABLE),
1036 ) {
1037 Some(format!("--{bin_kind}={bin_name}"))
1038 } else {
1039 None
1040 }
1041 } else {
1042 Some(stem.to_owned())
1043 };
1044 fragment.unwrap_or_else(|| "--".to_owned())
1045}
1046
1047#[cfg(test)]
1048mod tests {
1049 use std::num::NonZeroU32;
1050
1051 use super::*;
1052 use crate::language;
1053 use gpui::{BorrowAppContext, Hsla, TestAppContext};
1054 use lsp::CompletionItemLabelDetails;
1055 use settings::SettingsStore;
1056 use theme::SyntaxTheme;
1057 use util::path;
1058
1059 #[gpui::test]
1060 async fn test_process_rust_diagnostics() {
1061 let mut params = lsp::PublishDiagnosticsParams {
1062 uri: lsp::Uri::from_file_path(path!("/a")).unwrap(),
1063 version: None,
1064 diagnostics: vec![
1065 // no newlines
1066 lsp::Diagnostic {
1067 message: "use of moved value `a`".to_string(),
1068 ..Default::default()
1069 },
1070 // newline at the end of a code span
1071 lsp::Diagnostic {
1072 message: "consider importing this struct: `use b::c;\n`".to_string(),
1073 ..Default::default()
1074 },
1075 // code span starting right after a newline
1076 lsp::Diagnostic {
1077 message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1078 .to_string(),
1079 ..Default::default()
1080 },
1081 ],
1082 };
1083 RustLspAdapter.process_diagnostics(&mut params, LanguageServerId(0), None);
1084
1085 assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
1086
1087 // remove trailing newline from code span
1088 assert_eq!(
1089 params.diagnostics[1].message,
1090 "consider importing this struct: `use b::c;`"
1091 );
1092
1093 // do not remove newline before the start of code span
1094 assert_eq!(
1095 params.diagnostics[2].message,
1096 "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1097 );
1098 }
1099
1100 #[gpui::test]
1101 async fn test_rust_label_for_completion() {
1102 let adapter = Arc::new(RustLspAdapter);
1103 let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1104 let grammar = language.grammar().unwrap();
1105 let theme = SyntaxTheme::new_test([
1106 ("type", Hsla::default()),
1107 ("keyword", Hsla::default()),
1108 ("function", Hsla::default()),
1109 ("property", Hsla::default()),
1110 ]);
1111
1112 language.set_theme(&theme);
1113
1114 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1115 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1116 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1117 let highlight_field = grammar.highlight_id_for_name("property").unwrap();
1118
1119 assert_eq!(
1120 adapter
1121 .label_for_completion(
1122 &lsp::CompletionItem {
1123 kind: Some(lsp::CompletionItemKind::FUNCTION),
1124 label: "hello(…)".to_string(),
1125 label_details: Some(CompletionItemLabelDetails {
1126 detail: Some("(use crate::foo)".into()),
1127 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string())
1128 }),
1129 ..Default::default()
1130 },
1131 &language
1132 )
1133 .await,
1134 Some(CodeLabel {
1135 text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1136 filter_range: 0..5,
1137 runs: vec![
1138 (0..5, highlight_function),
1139 (7..10, highlight_keyword),
1140 (11..17, highlight_type),
1141 (18..19, highlight_type),
1142 (25..28, highlight_type),
1143 (29..30, highlight_type),
1144 ],
1145 })
1146 );
1147 assert_eq!(
1148 adapter
1149 .label_for_completion(
1150 &lsp::CompletionItem {
1151 kind: Some(lsp::CompletionItemKind::FUNCTION),
1152 label: "hello(…)".to_string(),
1153 label_details: Some(CompletionItemLabelDetails {
1154 detail: Some("(use crate::foo)".into()),
1155 description: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
1156 }),
1157 ..Default::default()
1158 },
1159 &language
1160 )
1161 .await,
1162 Some(CodeLabel {
1163 text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1164 filter_range: 0..5,
1165 runs: vec![
1166 (0..5, highlight_function),
1167 (7..10, highlight_keyword),
1168 (11..17, highlight_type),
1169 (18..19, highlight_type),
1170 (25..28, highlight_type),
1171 (29..30, highlight_type),
1172 ],
1173 })
1174 );
1175 assert_eq!(
1176 adapter
1177 .label_for_completion(
1178 &lsp::CompletionItem {
1179 kind: Some(lsp::CompletionItemKind::FIELD),
1180 label: "len".to_string(),
1181 detail: Some("usize".to_string()),
1182 ..Default::default()
1183 },
1184 &language
1185 )
1186 .await,
1187 Some(CodeLabel {
1188 text: "len: usize".to_string(),
1189 filter_range: 0..3,
1190 runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
1191 })
1192 );
1193
1194 assert_eq!(
1195 adapter
1196 .label_for_completion(
1197 &lsp::CompletionItem {
1198 kind: Some(lsp::CompletionItemKind::FUNCTION),
1199 label: "hello(…)".to_string(),
1200 label_details: Some(CompletionItemLabelDetails {
1201 detail: Some("(use crate::foo)".to_string()),
1202 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1203 }),
1204
1205 ..Default::default()
1206 },
1207 &language
1208 )
1209 .await,
1210 Some(CodeLabel {
1211 text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1212 filter_range: 0..5,
1213 runs: vec![
1214 (0..5, highlight_function),
1215 (7..10, highlight_keyword),
1216 (11..17, highlight_type),
1217 (18..19, highlight_type),
1218 (25..28, highlight_type),
1219 (29..30, highlight_type),
1220 ],
1221 })
1222 );
1223
1224 assert_eq!(
1225 adapter
1226 .label_for_completion(
1227 &lsp::CompletionItem {
1228 kind: Some(lsp::CompletionItemKind::FUNCTION),
1229 label: "hello".to_string(),
1230 label_details: Some(CompletionItemLabelDetails {
1231 detail: Some("(use crate::foo)".to_string()),
1232 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1233 }),
1234 ..Default::default()
1235 },
1236 &language
1237 )
1238 .await,
1239 Some(CodeLabel {
1240 text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1241 filter_range: 0..5,
1242 runs: vec![
1243 (0..5, highlight_function),
1244 (7..10, highlight_keyword),
1245 (11..17, highlight_type),
1246 (18..19, highlight_type),
1247 (25..28, highlight_type),
1248 (29..30, highlight_type),
1249 ],
1250 })
1251 );
1252
1253 assert_eq!(
1254 adapter
1255 .label_for_completion(
1256 &lsp::CompletionItem {
1257 kind: Some(lsp::CompletionItemKind::METHOD),
1258 label: "await.as_deref_mut()".to_string(),
1259 filter_text: Some("as_deref_mut".to_string()),
1260 label_details: Some(CompletionItemLabelDetails {
1261 detail: None,
1262 description: Some("fn(&mut self) -> IterMut<'_, T>".to_string()),
1263 }),
1264 ..Default::default()
1265 },
1266 &language
1267 )
1268 .await,
1269 Some(CodeLabel {
1270 text: "await.as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1271 filter_range: 6..18,
1272 runs: vec![
1273 (6..18, HighlightId(2)),
1274 (20..23, HighlightId(1)),
1275 (33..40, HighlightId(0)),
1276 (45..46, HighlightId(0))
1277 ],
1278 })
1279 );
1280
1281 assert_eq!(
1282 adapter
1283 .label_for_completion(
1284 &lsp::CompletionItem {
1285 kind: Some(lsp::CompletionItemKind::METHOD),
1286 label: "as_deref_mut()".to_string(),
1287 filter_text: Some("as_deref_mut".to_string()),
1288 label_details: Some(CompletionItemLabelDetails {
1289 detail: None,
1290 description: Some(
1291 "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string()
1292 ),
1293 }),
1294 ..Default::default()
1295 },
1296 &language
1297 )
1298 .await,
1299 Some(CodeLabel {
1300 text: "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1301 filter_range: 7..19,
1302 runs: vec![
1303 (0..3, HighlightId(1)),
1304 (4..6, HighlightId(1)),
1305 (7..19, HighlightId(2)),
1306 (21..24, HighlightId(1)),
1307 (34..41, HighlightId(0)),
1308 (46..47, HighlightId(0))
1309 ],
1310 })
1311 );
1312
1313 assert_eq!(
1314 adapter
1315 .label_for_completion(
1316 &lsp::CompletionItem {
1317 kind: Some(lsp::CompletionItemKind::FIELD),
1318 label: "inner_value".to_string(),
1319 filter_text: Some("value".to_string()),
1320 detail: Some("String".to_string()),
1321 ..Default::default()
1322 },
1323 &language,
1324 )
1325 .await,
1326 Some(CodeLabel {
1327 text: "inner_value: String".to_string(),
1328 filter_range: 6..11,
1329 runs: vec![(0..11, HighlightId(3)), (13..19, HighlightId(0))],
1330 })
1331 );
1332 }
1333
1334 #[gpui::test]
1335 async fn test_rust_label_for_symbol() {
1336 let adapter = Arc::new(RustLspAdapter);
1337 let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1338 let grammar = language.grammar().unwrap();
1339 let theme = SyntaxTheme::new_test([
1340 ("type", Hsla::default()),
1341 ("keyword", Hsla::default()),
1342 ("function", Hsla::default()),
1343 ("property", Hsla::default()),
1344 ]);
1345
1346 language.set_theme(&theme);
1347
1348 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1349 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1350 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1351
1352 assert_eq!(
1353 adapter
1354 .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
1355 .await,
1356 Some(CodeLabel {
1357 text: "fn hello".to_string(),
1358 filter_range: 3..8,
1359 runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
1360 })
1361 );
1362
1363 assert_eq!(
1364 adapter
1365 .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
1366 .await,
1367 Some(CodeLabel {
1368 text: "type World".to_string(),
1369 filter_range: 5..10,
1370 runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
1371 })
1372 );
1373 }
1374
1375 #[gpui::test]
1376 async fn test_rust_autoindent(cx: &mut TestAppContext) {
1377 // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
1378 cx.update(|cx| {
1379 let test_settings = SettingsStore::test(cx);
1380 cx.set_global(test_settings);
1381 language::init(cx);
1382 cx.update_global::<SettingsStore, _>(|store, cx| {
1383 store.update_user_settings(cx, |s| {
1384 s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
1385 });
1386 });
1387 });
1388
1389 let language = crate::language("rust", tree_sitter_rust::LANGUAGE.into());
1390
1391 cx.new(|cx| {
1392 let mut buffer = Buffer::local("", cx).with_language(language, cx);
1393
1394 // indent between braces
1395 buffer.set_text("fn a() {}", cx);
1396 let ix = buffer.len() - 1;
1397 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1398 assert_eq!(buffer.text(), "fn a() {\n \n}");
1399
1400 // indent between braces, even after empty lines
1401 buffer.set_text("fn a() {\n\n\n}", cx);
1402 let ix = buffer.len() - 2;
1403 buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1404 assert_eq!(buffer.text(), "fn a() {\n\n\n \n}");
1405
1406 // indent a line that continues a field expression
1407 buffer.set_text("fn a() {\n \n}", cx);
1408 let ix = buffer.len() - 2;
1409 buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
1410 assert_eq!(buffer.text(), "fn a() {\n b\n .c\n}");
1411
1412 // indent further lines that continue the field expression, even after empty lines
1413 let ix = buffer.len() - 2;
1414 buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
1415 assert_eq!(buffer.text(), "fn a() {\n b\n .c\n \n .d\n}");
1416
1417 // dedent the line after the field expression
1418 let ix = buffer.len() - 2;
1419 buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
1420 assert_eq!(
1421 buffer.text(),
1422 "fn a() {\n b\n .c\n \n .d;\n e\n}"
1423 );
1424
1425 // indent inside a struct within a call
1426 buffer.set_text("const a: B = c(D {});", cx);
1427 let ix = buffer.len() - 3;
1428 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1429 assert_eq!(buffer.text(), "const a: B = c(D {\n \n});");
1430
1431 // indent further inside a nested call
1432 let ix = buffer.len() - 4;
1433 buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
1434 assert_eq!(buffer.text(), "const a: B = c(D {\n e: f(\n \n )\n});");
1435
1436 // keep that indent after an empty line
1437 let ix = buffer.len() - 8;
1438 buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1439 assert_eq!(
1440 buffer.text(),
1441 "const a: B = c(D {\n e: f(\n \n \n )\n});"
1442 );
1443
1444 buffer
1445 });
1446 }
1447
1448 #[test]
1449 fn test_package_name_from_pkgid() {
1450 for (input, expected) in [
1451 (
1452 "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
1453 "zed",
1454 ),
1455 (
1456 "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
1457 "my-custom-package",
1458 ),
1459 ] {
1460 assert_eq!(package_name_from_pkgid(input), Some(expected));
1461 }
1462 }
1463
1464 #[test]
1465 fn test_target_info_from_metadata() {
1466 for (input, absolute_path, expected) in [
1467 (
1468 r#"{"packages":[{"id":"path+file:///absolute/path/to/project/zed/crates/zed#0.131.0","manifest_path":"/path/to/zed/Cargo.toml","targets":[{"name":"zed","kind":["bin"],"src_path":"/path/to/zed/src/main.rs"}]}]}"#,
1469 "/path/to/zed/src/main.rs",
1470 Some((
1471 Some(TargetInfo {
1472 package_name: "zed".into(),
1473 target_name: "zed".into(),
1474 required_features: Vec::new(),
1475 target_kind: TargetKind::Bin,
1476 }),
1477 Arc::from("/path/to/zed".as_ref()),
1478 )),
1479 ),
1480 (
1481 r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","manifest_path":"/path/to/custom-package/Cargo.toml","targets":[{"name":"my-custom-bin","kind":["bin"],"src_path":"/path/to/custom-package/src/main.rs"}]}]}"#,
1482 "/path/to/custom-package/src/main.rs",
1483 Some((
1484 Some(TargetInfo {
1485 package_name: "my-custom-package".into(),
1486 target_name: "my-custom-bin".into(),
1487 required_features: Vec::new(),
1488 target_kind: TargetKind::Bin,
1489 }),
1490 Arc::from("/path/to/custom-package".as_ref()),
1491 )),
1492 ),
1493 (
1494 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"}],"manifest_path":"/path/to/custom-package/Cargo.toml"}]}"#,
1495 "/path/to/custom-package/src/main.rs",
1496 Some((
1497 Some(TargetInfo {
1498 package_name: "my-custom-package".into(),
1499 target_name: "my-custom-bin".into(),
1500 required_features: Vec::new(),
1501 target_kind: TargetKind::Example,
1502 }),
1503 Arc::from("/path/to/custom-package".as_ref()),
1504 )),
1505 ),
1506 (
1507 r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","manifest_path":"/path/to/custom-package/Cargo.toml","targets":[{"name":"my-custom-bin","kind":["example"],"src_path":"/path/to/custom-package/src/main.rs","required-features":["foo","bar"]}]}]}"#,
1508 "/path/to/custom-package/src/main.rs",
1509 Some((
1510 Some(TargetInfo {
1511 package_name: "my-custom-package".into(),
1512 target_name: "my-custom-bin".into(),
1513 required_features: vec!["foo".to_owned(), "bar".to_owned()],
1514 target_kind: TargetKind::Example,
1515 }),
1516 Arc::from("/path/to/custom-package".as_ref()),
1517 )),
1518 ),
1519 (
1520 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":[]}],"manifest_path":"/path/to/custom-package/Cargo.toml"}]}"#,
1521 "/path/to/custom-package/src/main.rs",
1522 Some((
1523 Some(TargetInfo {
1524 package_name: "my-custom-package".into(),
1525 target_name: "my-custom-bin".into(),
1526 required_features: vec![],
1527 target_kind: TargetKind::Example,
1528 }),
1529 Arc::from("/path/to/custom-package".as_ref()),
1530 )),
1531 ),
1532 (
1533 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"}],"manifest_path":"/path/to/custom-package/Cargo.toml"}]}"#,
1534 "/path/to/custom-package/src/main.rs",
1535 Some((None, Arc::from("/path/to/custom-package".as_ref()))),
1536 ),
1537 ] {
1538 let metadata: CargoMetadata = serde_json::from_str(input).context(input).unwrap();
1539
1540 let absolute_path = Path::new(absolute_path);
1541
1542 assert_eq!(target_info_from_metadata(metadata, absolute_path), expected);
1543 }
1544 }
1545
1546 #[test]
1547 fn test_rust_test_fragment() {
1548 #[track_caller]
1549 fn check(
1550 variables: impl IntoIterator<Item = (VariableName, &'static str)>,
1551 path: &str,
1552 expected: &str,
1553 ) {
1554 let path = Path::new(path);
1555 let found = test_fragment(
1556 &TaskVariables::from_iter(variables.into_iter().map(|(k, v)| (k, v.to_owned()))),
1557 path,
1558 path.file_stem().unwrap().to_str().unwrap(),
1559 );
1560 assert_eq!(expected, found);
1561 }
1562
1563 check([], "/project/src/lib.rs", "--lib");
1564 check([], "/project/src/foo/mod.rs", "foo");
1565 check(
1566 [
1567 (RUST_BIN_KIND_TASK_VARIABLE.clone(), "bin"),
1568 (RUST_BIN_NAME_TASK_VARIABLE, "x"),
1569 ],
1570 "/project/src/main.rs",
1571 "--bin=x",
1572 );
1573 check([], "/project/src/main.rs", "--");
1574 }
1575}