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