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