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 any::Any,
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::{ResultExt, maybe};
28
29use crate::github_download::{GithubBinaryMetadata, download_server_binary};
30use crate::language_settings::language_settings;
31
32pub struct RustLspAdapter;
33
34#[cfg(target_os = "macos")]
35impl RustLspAdapter {
36 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
37 const ARCH_SERVER_NAME: &str = "apple-darwin";
38}
39
40#[cfg(target_os = "linux")]
41impl RustLspAdapter {
42 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
43 const ARCH_SERVER_NAME: &str = "unknown-linux-gnu";
44}
45
46#[cfg(target_os = "freebsd")]
47impl RustLspAdapter {
48 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
49 const ARCH_SERVER_NAME: &str = "unknown-freebsd";
50}
51
52#[cfg(target_os = "windows")]
53impl RustLspAdapter {
54 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
55 const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
56}
57
58const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer");
59
60impl RustLspAdapter {
61 fn build_asset_name() -> String {
62 let extension = match Self::GITHUB_ASSET_KIND {
63 AssetKind::TarGz => "tar.gz",
64 AssetKind::Gz => "gz",
65 AssetKind::Zip => "zip",
66 };
67
68 format!(
69 "{}-{}-{}.{}",
70 SERVER_NAME,
71 std::env::consts::ARCH,
72 Self::ARCH_SERVER_NAME,
73 extension
74 )
75 }
76}
77
78pub(crate) struct CargoManifestProvider;
79
80impl ManifestProvider for CargoManifestProvider {
81 fn name(&self) -> ManifestName {
82 SharedString::new_static("Cargo.toml").into()
83 }
84
85 fn search(
86 &self,
87 ManifestQuery {
88 path,
89 depth,
90 delegate,
91 }: ManifestQuery,
92 ) -> Option<Arc<Path>> {
93 let mut outermost_cargo_toml = None;
94 for path in path.ancestors().take(depth) {
95 let p = path.join("Cargo.toml");
96 if delegate.exists(&p, Some(false)) {
97 outermost_cargo_toml = Some(Arc::from(path));
98 }
99 }
100
101 outermost_cargo_toml
102 }
103}
104
105#[async_trait(?Send)]
106impl LspAdapter for RustLspAdapter {
107 fn name(&self) -> LanguageServerName {
108 SERVER_NAME
109 }
110
111 async fn check_if_user_installed(
112 &self,
113 delegate: &dyn LspAdapterDelegate,
114 _: Option<Toolchain>,
115 _: &AsyncApp,
116 ) -> Option<LanguageServerBinary> {
117 let path = delegate.which("rust-analyzer".as_ref()).await?;
118 let env = delegate.shell_env().await;
119
120 // It is surprisingly common for ~/.cargo/bin/rust-analyzer to be a symlink to
121 // /usr/bin/rust-analyzer that fails when you run it; so we need to test it.
122 log::info!("found rust-analyzer in PATH. trying to run `rust-analyzer --help`");
123 let result = delegate
124 .try_exec(LanguageServerBinary {
125 path: path.clone(),
126 arguments: vec!["--help".into()],
127 env: Some(env.clone()),
128 })
129 .await;
130 if let Err(err) = result {
131 log::debug!(
132 "failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {}",
133 path,
134 err
135 );
136 return None;
137 }
138
139 Some(LanguageServerBinary {
140 path,
141 env: Some(env),
142 arguments: vec![],
143 })
144 }
145
146 async fn fetch_latest_server_version(
147 &self,
148 delegate: &dyn LspAdapterDelegate,
149 cx: &AsyncApp,
150 ) -> Result<Box<dyn 'static + Send + Any>> {
151 let release = latest_github_release(
152 "rust-lang/rust-analyzer",
153 true,
154 ProjectSettings::try_read_global(cx, |s| {
155 s.lsp.get(&SERVER_NAME)?.fetch.as_ref()?.pre_release
156 })
157 .flatten()
158 .unwrap_or(false),
159 delegate.http_client(),
160 )
161 .await?;
162 let asset_name = Self::build_asset_name();
163 let asset = release
164 .assets
165 .into_iter()
166 .find(|asset| asset.name == asset_name)
167 .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
168 Ok(Box::new(GitHubLspBinaryVersion {
169 name: release.tag_name,
170 url: asset.browser_download_url,
171 digest: asset.digest,
172 }))
173 }
174
175 async fn fetch_server_binary(
176 &self,
177 version: Box<dyn 'static + Send + Any>,
178 container_dir: PathBuf,
179 delegate: &dyn LspAdapterDelegate,
180 ) -> Result<LanguageServerBinary> {
181 let GitHubLspBinaryVersion {
182 name,
183 url,
184 digest: expected_digest,
185 } = *version.downcast::<GitHubLspBinaryVersion>().unwrap();
186 let destination_path = container_dir.join(format!("rust-analyzer-{name}"));
187 let server_path = match Self::GITHUB_ASSET_KIND {
188 AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place.
189 AssetKind::Zip => destination_path.clone().join("rust-analyzer.exe"), // zip contains a .exe
190 };
191
192 let binary = LanguageServerBinary {
193 path: server_path.clone(),
194 env: None,
195 arguments: Default::default(),
196 };
197
198 let metadata_path = destination_path.with_extension("metadata");
199 let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
200 .await
201 .ok();
202 if let Some(metadata) = metadata {
203 let validity_check = async || {
204 delegate
205 .try_exec(LanguageServerBinary {
206 path: server_path.clone(),
207 arguments: vec!["--version".into()],
208 env: None,
209 })
210 .await
211 .inspect_err(|err| {
212 log::warn!("Unable to run {server_path:?} asset, redownloading: {err}",)
213 })
214 };
215 if let (Some(actual_digest), Some(expected_digest)) =
216 (&metadata.digest, &expected_digest)
217 {
218 if actual_digest == expected_digest {
219 if validity_check().await.is_ok() {
220 return Ok(binary);
221 }
222 } else {
223 log::info!(
224 "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
225 );
226 }
227 } else if validity_check().await.is_ok() {
228 return Ok(binary);
229 }
230 }
231
232 download_server_binary(
233 delegate,
234 &url,
235 expected_digest.as_deref(),
236 &destination_path,
237 Self::GITHUB_ASSET_KIND,
238 )
239 .await?;
240 make_file_executable(&server_path).await?;
241 remove_matching(&container_dir, |path| path != destination_path).await;
242 GithubBinaryMetadata::write_to_file(
243 &GithubBinaryMetadata {
244 metadata_version: 1,
245 digest: expected_digest,
246 },
247 &metadata_path,
248 )
249 .await?;
250
251 Ok(LanguageServerBinary {
252 path: server_path,
253 env: None,
254 arguments: Default::default(),
255 })
256 }
257
258 async fn cached_server_binary(
259 &self,
260 container_dir: PathBuf,
261 _: &dyn LspAdapterDelegate,
262 ) -> Option<LanguageServerBinary> {
263 get_cached_server_binary(container_dir).await
264 }
265
266 fn disk_based_diagnostic_sources(&self) -> Vec<String> {
267 vec![CARGO_DIAGNOSTICS_SOURCE_NAME.to_owned()]
268 }
269
270 fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
271 Some("rust-analyzer/flycheck".into())
272 }
273
274 fn process_diagnostics(
275 &self,
276 params: &mut lsp::PublishDiagnosticsParams,
277 _: LanguageServerId,
278 _: Option<&'_ Buffer>,
279 ) {
280 static REGEX: LazyLock<Regex> =
281 LazyLock::new(|| Regex::new(r"(?m)`([^`]+)\n`$").expect("Failed to create REGEX"));
282
283 for diagnostic in &mut params.diagnostics {
284 for message in diagnostic
285 .related_information
286 .iter_mut()
287 .flatten()
288 .map(|info| &mut info.message)
289 .chain([&mut diagnostic.message])
290 {
291 if let Cow::Owned(sanitized) = REGEX.replace_all(message, "`$1`") {
292 *message = sanitized;
293 }
294 }
295 }
296 }
297
298 fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
299 static REGEX: LazyLock<Regex> =
300 LazyLock::new(|| Regex::new(r"(?m)\n *").expect("Failed to create REGEX"));
301 Some(REGEX.replace_all(message, "\n\n").to_string())
302 }
303
304 async fn label_for_completion(
305 &self,
306 completion: &lsp::CompletionItem,
307 language: &Arc<Language>,
308 ) -> Option<CodeLabel> {
309 // rust-analyzer calls these detail left and detail right in terms of where it expects things to be rendered
310 // this usually contains signatures of the thing to be completed
311 let detail_right = completion
312 .label_details
313 .as_ref()
314 .and_then(|detail| detail.description.as_ref())
315 .or(completion.detail.as_ref())
316 .map(|detail| detail.trim());
317 // this tends to contain alias and import information
318 let detail_left = completion
319 .label_details
320 .as_ref()
321 .and_then(|detail| detail.detail.as_deref());
322 let mk_label = |text: String, filter_range: &dyn Fn() -> Range<usize>, runs| {
323 let filter_range = completion
324 .filter_text
325 .as_deref()
326 .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
327 .or_else(|| {
328 text.find(&completion.label)
329 .map(|ix| ix..ix + completion.label.len())
330 })
331 .unwrap_or_else(filter_range);
332
333 CodeLabel {
334 text,
335 runs,
336 filter_range,
337 }
338 };
339 let mut label = match (detail_right, completion.kind) {
340 (Some(signature), Some(lsp::CompletionItemKind::FIELD)) => {
341 let name = &completion.label;
342 let text = format!("{name}: {signature}");
343 let prefix = "struct S { ";
344 let source = Rope::from_iter([prefix, &text, " }"]);
345 let runs =
346 language.highlight_text(&source, prefix.len()..prefix.len() + text.len());
347 mk_label(text, &|| 0..completion.label.len(), runs)
348 }
349 (
350 Some(signature),
351 Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE),
352 ) if completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) => {
353 let name = &completion.label;
354 let text = format!("{name}: {signature}",);
355 let prefix = "let ";
356 let source = Rope::from_iter([prefix, &text, " = ();"]);
357 let runs =
358 language.highlight_text(&source, prefix.len()..prefix.len() + text.len());
359 mk_label(text, &|| 0..completion.label.len(), runs)
360 }
361 (
362 function_signature,
363 Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD),
364 ) => {
365 const FUNCTION_PREFIXES: [&str; 6] = [
366 "async fn",
367 "async unsafe fn",
368 "const fn",
369 "const unsafe fn",
370 "unsafe fn",
371 "fn",
372 ];
373 let fn_prefixed = FUNCTION_PREFIXES.iter().find_map(|&prefix| {
374 function_signature?
375 .strip_prefix(prefix)
376 .map(|suffix| (prefix, suffix))
377 });
378 let label = if let Some(label) = completion
379 .label
380 .strip_suffix("(…)")
381 .or_else(|| completion.label.strip_suffix("()"))
382 {
383 label
384 } else {
385 &completion.label
386 };
387
388 static FULL_SIGNATURE_REGEX: LazyLock<Regex> =
389 LazyLock::new(|| Regex::new(r"fn (.?+)\(").expect("Failed to create REGEX"));
390 if let Some((function_signature, match_)) = function_signature
391 .filter(|it| it.contains(&label))
392 .and_then(|it| Some((it, FULL_SIGNATURE_REGEX.find(it)?)))
393 {
394 let source = Rope::from(function_signature);
395 let runs = language.highlight_text(&source, 0..function_signature.len());
396 mk_label(
397 function_signature.to_owned(),
398 &|| match_.range().start - 3..match_.range().end - 1,
399 runs,
400 )
401 } else if let Some((prefix, suffix)) = fn_prefixed {
402 let text = format!("{label}{suffix}");
403 let source = Rope::from_iter([prefix, " ", &text, " {}"]);
404 let run_start = prefix.len() + 1;
405 let runs = language.highlight_text(&source, run_start..run_start + text.len());
406 mk_label(text, &|| 0..label.len(), runs)
407 } else if completion
408 .detail
409 .as_ref()
410 .is_some_and(|detail| detail.starts_with("macro_rules! "))
411 {
412 let text = completion.label.clone();
413 let len = text.len();
414 let source = Rope::from(text.as_str());
415 let runs = language.highlight_text(&source, 0..len);
416 mk_label(text, &|| 0..completion.label.len(), runs)
417 } else if detail_left.is_none() {
418 return None;
419 } else {
420 mk_label(
421 completion.label.clone(),
422 &|| 0..completion.label.len(),
423 vec![],
424 )
425 }
426 }
427 (_, kind) => {
428 let highlight_name = kind.and_then(|kind| match kind {
429 lsp::CompletionItemKind::STRUCT
430 | lsp::CompletionItemKind::INTERFACE
431 | lsp::CompletionItemKind::ENUM => Some("type"),
432 lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
433 lsp::CompletionItemKind::KEYWORD => Some("keyword"),
434 lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
435 Some("constant")
436 }
437 _ => None,
438 });
439
440 let label = completion.label.clone();
441 let mut runs = vec![];
442 if let Some(highlight_name) = highlight_name {
443 let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name)?;
444 runs.push((
445 0..label.rfind('(').unwrap_or(completion.label.len()),
446 highlight_id,
447 ));
448 } else if detail_left.is_none() {
449 return None;
450 }
451
452 mk_label(label, &|| 0..completion.label.len(), runs)
453 }
454 };
455
456 if let Some(detail_left) = detail_left {
457 label.text.push(' ');
458 if !detail_left.starts_with('(') {
459 label.text.push('(');
460 }
461 label.text.push_str(detail_left);
462 if !detail_left.ends_with(')') {
463 label.text.push(')');
464 }
465 }
466 Some(label)
467 }
468
469 async fn label_for_symbol(
470 &self,
471 name: &str,
472 kind: lsp::SymbolKind,
473 language: &Arc<Language>,
474 ) -> Option<CodeLabel> {
475 let (prefix, suffix) = match kind {
476 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => ("fn ", " () {}"),
477 lsp::SymbolKind::STRUCT => ("struct ", " {}"),
478 lsp::SymbolKind::ENUM => ("enum ", " {}"),
479 lsp::SymbolKind::INTERFACE => ("trait ", " {}"),
480 lsp::SymbolKind::CONSTANT => ("const ", ": () = ();"),
481 lsp::SymbolKind::MODULE => ("mod ", " {}"),
482 lsp::SymbolKind::TYPE_PARAMETER => ("type ", " {}"),
483 _ => return None,
484 };
485
486 let filter_range = prefix.len()..prefix.len() + name.len();
487 let display_range = 0..filter_range.end;
488 Some(CodeLabel {
489 runs: language.highlight_text(&Rope::from_iter([prefix, name, suffix]), display_range),
490 text: format!("{prefix}{name}"),
491 filter_range,
492 })
493 }
494
495 fn prepare_initialize_params(
496 &self,
497 mut original: InitializeParams,
498 cx: &App,
499 ) -> Result<InitializeParams> {
500 let enable_lsp_tasks = ProjectSettings::get_global(cx)
501 .lsp
502 .get(&SERVER_NAME)
503 .is_some_and(|s| s.enable_lsp_tasks);
504 if enable_lsp_tasks {
505 let experimental = json!({
506 "runnables": {
507 "kinds": [ "cargo", "shell" ],
508 },
509 });
510 if let Some(original_experimental) = &mut original.capabilities.experimental {
511 merge_json_value_into(experimental, original_experimental);
512 } else {
513 original.capabilities.experimental = Some(experimental);
514 }
515 }
516
517 Ok(original)
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().to_string()) })
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 language::language_settings::AllLanguageSettings;
1056 use lsp::CompletionItemLabelDetails;
1057 use settings::SettingsStore;
1058 use theme::SyntaxTheme;
1059 use util::path;
1060
1061 #[gpui::test]
1062 async fn test_process_rust_diagnostics() {
1063 let mut params = lsp::PublishDiagnosticsParams {
1064 uri: lsp::Uri::from_file_path(path!("/a")).unwrap(),
1065 version: None,
1066 diagnostics: vec![
1067 // no newlines
1068 lsp::Diagnostic {
1069 message: "use of moved value `a`".to_string(),
1070 ..Default::default()
1071 },
1072 // newline at the end of a code span
1073 lsp::Diagnostic {
1074 message: "consider importing this struct: `use b::c;\n`".to_string(),
1075 ..Default::default()
1076 },
1077 // code span starting right after a newline
1078 lsp::Diagnostic {
1079 message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1080 .to_string(),
1081 ..Default::default()
1082 },
1083 ],
1084 };
1085 RustLspAdapter.process_diagnostics(&mut params, LanguageServerId(0), None);
1086
1087 assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
1088
1089 // remove trailing newline from code span
1090 assert_eq!(
1091 params.diagnostics[1].message,
1092 "consider importing this struct: `use b::c;`"
1093 );
1094
1095 // do not remove newline before the start of code span
1096 assert_eq!(
1097 params.diagnostics[2].message,
1098 "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1099 );
1100 }
1101
1102 #[gpui::test]
1103 async fn test_rust_label_for_completion() {
1104 let adapter = Arc::new(RustLspAdapter);
1105 let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1106 let grammar = language.grammar().unwrap();
1107 let theme = SyntaxTheme::new_test([
1108 ("type", Hsla::default()),
1109 ("keyword", Hsla::default()),
1110 ("function", Hsla::default()),
1111 ("property", Hsla::default()),
1112 ]);
1113
1114 language.set_theme(&theme);
1115
1116 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1117 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1118 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1119 let highlight_field = grammar.highlight_id_for_name("property").unwrap();
1120
1121 assert_eq!(
1122 adapter
1123 .label_for_completion(
1124 &lsp::CompletionItem {
1125 kind: Some(lsp::CompletionItemKind::FUNCTION),
1126 label: "hello(…)".to_string(),
1127 label_details: Some(CompletionItemLabelDetails {
1128 detail: Some("(use crate::foo)".into()),
1129 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string())
1130 }),
1131 ..Default::default()
1132 },
1133 &language
1134 )
1135 .await,
1136 Some(CodeLabel {
1137 text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1138 filter_range: 0..5,
1139 runs: vec![
1140 (0..5, highlight_function),
1141 (7..10, highlight_keyword),
1142 (11..17, highlight_type),
1143 (18..19, highlight_type),
1144 (25..28, highlight_type),
1145 (29..30, highlight_type),
1146 ],
1147 })
1148 );
1149 assert_eq!(
1150 adapter
1151 .label_for_completion(
1152 &lsp::CompletionItem {
1153 kind: Some(lsp::CompletionItemKind::FUNCTION),
1154 label: "hello(…)".to_string(),
1155 label_details: Some(CompletionItemLabelDetails {
1156 detail: Some("(use crate::foo)".into()),
1157 description: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
1158 }),
1159 ..Default::default()
1160 },
1161 &language
1162 )
1163 .await,
1164 Some(CodeLabel {
1165 text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1166 filter_range: 0..5,
1167 runs: vec![
1168 (0..5, highlight_function),
1169 (7..10, highlight_keyword),
1170 (11..17, highlight_type),
1171 (18..19, highlight_type),
1172 (25..28, highlight_type),
1173 (29..30, highlight_type),
1174 ],
1175 })
1176 );
1177 assert_eq!(
1178 adapter
1179 .label_for_completion(
1180 &lsp::CompletionItem {
1181 kind: Some(lsp::CompletionItemKind::FIELD),
1182 label: "len".to_string(),
1183 detail: Some("usize".to_string()),
1184 ..Default::default()
1185 },
1186 &language
1187 )
1188 .await,
1189 Some(CodeLabel {
1190 text: "len: usize".to_string(),
1191 filter_range: 0..3,
1192 runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
1193 })
1194 );
1195
1196 assert_eq!(
1197 adapter
1198 .label_for_completion(
1199 &lsp::CompletionItem {
1200 kind: Some(lsp::CompletionItemKind::FUNCTION),
1201 label: "hello(…)".to_string(),
1202 label_details: Some(CompletionItemLabelDetails {
1203 detail: Some("(use crate::foo)".to_string()),
1204 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1205 }),
1206
1207 ..Default::default()
1208 },
1209 &language
1210 )
1211 .await,
1212 Some(CodeLabel {
1213 text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1214 filter_range: 0..5,
1215 runs: vec![
1216 (0..5, highlight_function),
1217 (7..10, highlight_keyword),
1218 (11..17, highlight_type),
1219 (18..19, highlight_type),
1220 (25..28, highlight_type),
1221 (29..30, highlight_type),
1222 ],
1223 })
1224 );
1225
1226 assert_eq!(
1227 adapter
1228 .label_for_completion(
1229 &lsp::CompletionItem {
1230 kind: Some(lsp::CompletionItemKind::FUNCTION),
1231 label: "hello".to_string(),
1232 label_details: Some(CompletionItemLabelDetails {
1233 detail: Some("(use crate::foo)".to_string()),
1234 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1235 }),
1236 ..Default::default()
1237 },
1238 &language
1239 )
1240 .await,
1241 Some(CodeLabel {
1242 text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1243 filter_range: 0..5,
1244 runs: vec![
1245 (0..5, highlight_function),
1246 (7..10, highlight_keyword),
1247 (11..17, highlight_type),
1248 (18..19, highlight_type),
1249 (25..28, highlight_type),
1250 (29..30, highlight_type),
1251 ],
1252 })
1253 );
1254
1255 assert_eq!(
1256 adapter
1257 .label_for_completion(
1258 &lsp::CompletionItem {
1259 kind: Some(lsp::CompletionItemKind::METHOD),
1260 label: "await.as_deref_mut()".to_string(),
1261 filter_text: Some("as_deref_mut".to_string()),
1262 label_details: Some(CompletionItemLabelDetails {
1263 detail: None,
1264 description: Some("fn(&mut self) -> IterMut<'_, T>".to_string()),
1265 }),
1266 ..Default::default()
1267 },
1268 &language
1269 )
1270 .await,
1271 Some(CodeLabel {
1272 text: "await.as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1273 filter_range: 6..18,
1274 runs: vec![
1275 (6..18, HighlightId(2)),
1276 (20..23, HighlightId(1)),
1277 (33..40, HighlightId(0)),
1278 (45..46, HighlightId(0))
1279 ],
1280 })
1281 );
1282
1283 assert_eq!(
1284 adapter
1285 .label_for_completion(
1286 &lsp::CompletionItem {
1287 kind: Some(lsp::CompletionItemKind::METHOD),
1288 label: "as_deref_mut()".to_string(),
1289 filter_text: Some("as_deref_mut".to_string()),
1290 label_details: Some(CompletionItemLabelDetails {
1291 detail: None,
1292 description: Some(
1293 "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string()
1294 ),
1295 }),
1296 ..Default::default()
1297 },
1298 &language
1299 )
1300 .await,
1301 Some(CodeLabel {
1302 text: "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1303 filter_range: 7..19,
1304 runs: vec![
1305 (0..3, HighlightId(1)),
1306 (4..6, HighlightId(1)),
1307 (7..19, HighlightId(2)),
1308 (21..24, HighlightId(1)),
1309 (34..41, HighlightId(0)),
1310 (46..47, HighlightId(0))
1311 ],
1312 })
1313 );
1314
1315 assert_eq!(
1316 adapter
1317 .label_for_completion(
1318 &lsp::CompletionItem {
1319 kind: Some(lsp::CompletionItemKind::FIELD),
1320 label: "inner_value".to_string(),
1321 filter_text: Some("value".to_string()),
1322 detail: Some("String".to_string()),
1323 ..Default::default()
1324 },
1325 &language,
1326 )
1327 .await,
1328 Some(CodeLabel {
1329 text: "inner_value: String".to_string(),
1330 filter_range: 6..11,
1331 runs: vec![(0..11, HighlightId(3)), (13..19, HighlightId(0))],
1332 })
1333 );
1334 }
1335
1336 #[gpui::test]
1337 async fn test_rust_label_for_symbol() {
1338 let adapter = Arc::new(RustLspAdapter);
1339 let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1340 let grammar = language.grammar().unwrap();
1341 let theme = SyntaxTheme::new_test([
1342 ("type", Hsla::default()),
1343 ("keyword", Hsla::default()),
1344 ("function", Hsla::default()),
1345 ("property", Hsla::default()),
1346 ]);
1347
1348 language.set_theme(&theme);
1349
1350 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1351 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1352 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1353
1354 assert_eq!(
1355 adapter
1356 .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
1357 .await,
1358 Some(CodeLabel {
1359 text: "fn hello".to_string(),
1360 filter_range: 3..8,
1361 runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
1362 })
1363 );
1364
1365 assert_eq!(
1366 adapter
1367 .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
1368 .await,
1369 Some(CodeLabel {
1370 text: "type World".to_string(),
1371 filter_range: 5..10,
1372 runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
1373 })
1374 );
1375 }
1376
1377 #[gpui::test]
1378 async fn test_rust_autoindent(cx: &mut TestAppContext) {
1379 // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
1380 cx.update(|cx| {
1381 let test_settings = SettingsStore::test(cx);
1382 cx.set_global(test_settings);
1383 language::init(cx);
1384 cx.update_global::<SettingsStore, _>(|store, cx| {
1385 store.update_user_settings::<AllLanguageSettings>(cx, |s| {
1386 s.defaults.tab_size = NonZeroU32::new(2);
1387 });
1388 });
1389 });
1390
1391 let language = crate::language("rust", tree_sitter_rust::LANGUAGE.into());
1392
1393 cx.new(|cx| {
1394 let mut buffer = Buffer::local("", cx).with_language(language, cx);
1395
1396 // indent between braces
1397 buffer.set_text("fn a() {}", cx);
1398 let ix = buffer.len() - 1;
1399 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1400 assert_eq!(buffer.text(), "fn a() {\n \n}");
1401
1402 // indent between braces, even after empty lines
1403 buffer.set_text("fn a() {\n\n\n}", cx);
1404 let ix = buffer.len() - 2;
1405 buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1406 assert_eq!(buffer.text(), "fn a() {\n\n\n \n}");
1407
1408 // indent a line that continues a field expression
1409 buffer.set_text("fn a() {\n \n}", cx);
1410 let ix = buffer.len() - 2;
1411 buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
1412 assert_eq!(buffer.text(), "fn a() {\n b\n .c\n}");
1413
1414 // indent further lines that continue the field expression, even after empty lines
1415 let ix = buffer.len() - 2;
1416 buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
1417 assert_eq!(buffer.text(), "fn a() {\n b\n .c\n \n .d\n}");
1418
1419 // dedent the line after the field expression
1420 let ix = buffer.len() - 2;
1421 buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
1422 assert_eq!(
1423 buffer.text(),
1424 "fn a() {\n b\n .c\n \n .d;\n e\n}"
1425 );
1426
1427 // indent inside a struct within a call
1428 buffer.set_text("const a: B = c(D {});", cx);
1429 let ix = buffer.len() - 3;
1430 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1431 assert_eq!(buffer.text(), "const a: B = c(D {\n \n});");
1432
1433 // indent further inside a nested call
1434 let ix = buffer.len() - 4;
1435 buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
1436 assert_eq!(buffer.text(), "const a: B = c(D {\n e: f(\n \n )\n});");
1437
1438 // keep that indent after an empty line
1439 let ix = buffer.len() - 8;
1440 buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1441 assert_eq!(
1442 buffer.text(),
1443 "const a: B = c(D {\n e: f(\n \n \n )\n});"
1444 );
1445
1446 buffer
1447 });
1448 }
1449
1450 #[test]
1451 fn test_package_name_from_pkgid() {
1452 for (input, expected) in [
1453 (
1454 "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
1455 "zed",
1456 ),
1457 (
1458 "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
1459 "my-custom-package",
1460 ),
1461 ] {
1462 assert_eq!(package_name_from_pkgid(input), Some(expected));
1463 }
1464 }
1465
1466 #[test]
1467 fn test_target_info_from_metadata() {
1468 for (input, absolute_path, expected) in [
1469 (
1470 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"}]}]}"#,
1471 "/path/to/zed/src/main.rs",
1472 Some((
1473 Some(TargetInfo {
1474 package_name: "zed".into(),
1475 target_name: "zed".into(),
1476 required_features: Vec::new(),
1477 target_kind: TargetKind::Bin,
1478 }),
1479 Arc::from("/path/to/zed".as_ref()),
1480 )),
1481 ),
1482 (
1483 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"}]}]}"#,
1484 "/path/to/custom-package/src/main.rs",
1485 Some((
1486 Some(TargetInfo {
1487 package_name: "my-custom-package".into(),
1488 target_name: "my-custom-bin".into(),
1489 required_features: Vec::new(),
1490 target_kind: TargetKind::Bin,
1491 }),
1492 Arc::from("/path/to/custom-package".as_ref()),
1493 )),
1494 ),
1495 (
1496 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"}]}"#,
1497 "/path/to/custom-package/src/main.rs",
1498 Some((
1499 Some(TargetInfo {
1500 package_name: "my-custom-package".into(),
1501 target_name: "my-custom-bin".into(),
1502 required_features: Vec::new(),
1503 target_kind: TargetKind::Example,
1504 }),
1505 Arc::from("/path/to/custom-package".as_ref()),
1506 )),
1507 ),
1508 (
1509 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"]}]}]}"#,
1510 "/path/to/custom-package/src/main.rs",
1511 Some((
1512 Some(TargetInfo {
1513 package_name: "my-custom-package".into(),
1514 target_name: "my-custom-bin".into(),
1515 required_features: vec!["foo".to_owned(), "bar".to_owned()],
1516 target_kind: TargetKind::Example,
1517 }),
1518 Arc::from("/path/to/custom-package".as_ref()),
1519 )),
1520 ),
1521 (
1522 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"}]}"#,
1523 "/path/to/custom-package/src/main.rs",
1524 Some((
1525 Some(TargetInfo {
1526 package_name: "my-custom-package".into(),
1527 target_name: "my-custom-bin".into(),
1528 required_features: vec![],
1529 target_kind: TargetKind::Example,
1530 }),
1531 Arc::from("/path/to/custom-package".as_ref()),
1532 )),
1533 ),
1534 (
1535 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"}]}"#,
1536 "/path/to/custom-package/src/main.rs",
1537 Some((None, Arc::from("/path/to/custom-package".as_ref()))),
1538 ),
1539 ] {
1540 let metadata: CargoMetadata = serde_json::from_str(input).context(input).unwrap();
1541
1542 let absolute_path = Path::new(absolute_path);
1543
1544 assert_eq!(target_info_from_metadata(metadata, absolute_path), expected);
1545 }
1546 }
1547
1548 #[test]
1549 fn test_rust_test_fragment() {
1550 #[track_caller]
1551 fn check(
1552 variables: impl IntoIterator<Item = (VariableName, &'static str)>,
1553 path: &str,
1554 expected: &str,
1555 ) {
1556 let path = Path::new(path);
1557 let found = test_fragment(
1558 &TaskVariables::from_iter(variables.into_iter().map(|(k, v)| (k, v.to_owned()))),
1559 path,
1560 path.file_stem().unwrap().to_str().unwrap(),
1561 );
1562 assert_eq!(expected, found);
1563 }
1564
1565 check([], "/project/src/lib.rs", "--lib");
1566 check([], "/project/src/foo/mod.rs", "foo");
1567 check(
1568 [
1569 (RUST_BIN_KIND_TASK_VARIABLE.clone(), "bin"),
1570 (RUST_BIN_NAME_TASK_VARIABLE, "x"),
1571 ],
1572 "/project/src/main.rs",
1573 "--bin=x",
1574 );
1575 check([], "/project/src/main.rs", "--");
1576 }
1577}