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