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 language::language_settings::AllLanguageSettings;
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::<AllLanguageSettings>(cx, |s| {
1385 s.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}