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
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 binary = LanguageServerBinary {
518 path: server_path.clone(),
519 env: None,
520 arguments: Default::default(),
521 };
522
523 let metadata_path = destination_path.with_extension("metadata");
524 let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
525 .await
526 .ok();
527 if let Some(metadata) = metadata {
528 let validity_check = async || {
529 delegate
530 .try_exec(LanguageServerBinary {
531 path: server_path.clone(),
532 arguments: vec!["--version".into()],
533 env: None,
534 })
535 .await
536 .inspect_err(|err| {
537 log::warn!("Unable to run {server_path:?} asset, redownloading: {err:#}",)
538 })
539 };
540 if let (Some(actual_digest), Some(expected_digest)) =
541 (&metadata.digest, &expected_digest)
542 {
543 if actual_digest == expected_digest {
544 if validity_check().await.is_ok() {
545 return Ok(binary);
546 }
547 } else {
548 log::info!(
549 "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
550 );
551 }
552 } else if validity_check().await.is_ok() {
553 return Ok(binary);
554 }
555 }
556
557 download_server_binary(
558 &*delegate.http_client(),
559 &url,
560 expected_digest.as_deref(),
561 &destination_path,
562 Self::GITHUB_ASSET_KIND,
563 )
564 .await?;
565 make_file_executable(&server_path).await?;
566 remove_matching(&container_dir, |path| path != destination_path).await;
567 GithubBinaryMetadata::write_to_file(
568 &GithubBinaryMetadata {
569 metadata_version: 1,
570 digest: expected_digest,
571 },
572 &metadata_path,
573 )
574 .await?;
575
576 Ok(LanguageServerBinary {
577 path: server_path,
578 env: None,
579 arguments: Default::default(),
580 })
581 }
582
583 async fn cached_server_binary(
584 &self,
585 container_dir: PathBuf,
586 _: &dyn LspAdapterDelegate,
587 ) -> Option<LanguageServerBinary> {
588 get_cached_server_binary(container_dir).await
589 }
590}
591
592pub(crate) struct RustContextProvider;
593
594const RUST_PACKAGE_TASK_VARIABLE: VariableName =
595 VariableName::Custom(Cow::Borrowed("RUST_PACKAGE"));
596
597/// The bin name corresponding to the current file in Cargo.toml
598const RUST_BIN_NAME_TASK_VARIABLE: VariableName =
599 VariableName::Custom(Cow::Borrowed("RUST_BIN_NAME"));
600
601/// The bin kind (bin/example) corresponding to the current file in Cargo.toml
602const RUST_BIN_KIND_TASK_VARIABLE: VariableName =
603 VariableName::Custom(Cow::Borrowed("RUST_BIN_KIND"));
604
605/// The flag to list required features for executing a bin, if any
606const RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE: VariableName =
607 VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES_FLAG"));
608
609/// The list of required features for executing a bin, if any
610const RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE: VariableName =
611 VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES"));
612
613const RUST_TEST_FRAGMENT_TASK_VARIABLE: VariableName =
614 VariableName::Custom(Cow::Borrowed("RUST_TEST_FRAGMENT"));
615
616const RUST_DOC_TEST_NAME_TASK_VARIABLE: VariableName =
617 VariableName::Custom(Cow::Borrowed("RUST_DOC_TEST_NAME"));
618
619const RUST_TEST_NAME_TASK_VARIABLE: VariableName =
620 VariableName::Custom(Cow::Borrowed("RUST_TEST_NAME"));
621
622const RUST_MANIFEST_DIRNAME_TASK_VARIABLE: VariableName =
623 VariableName::Custom(Cow::Borrowed("RUST_MANIFEST_DIRNAME"));
624
625impl ContextProvider for RustContextProvider {
626 fn build_context(
627 &self,
628 task_variables: &TaskVariables,
629 location: ContextLocation<'_>,
630 project_env: Option<HashMap<String, String>>,
631 _: Arc<dyn LanguageToolchainStore>,
632 cx: &mut gpui::App,
633 ) -> Task<Result<TaskVariables>> {
634 let local_abs_path = location
635 .file_location
636 .buffer
637 .read(cx)
638 .file()
639 .and_then(|file| Some(file.as_local()?.abs_path(cx)));
640
641 let mut variables = TaskVariables::default();
642
643 if let (Some(path), Some(stem)) = (&local_abs_path, task_variables.get(&VariableName::Stem))
644 {
645 let fragment = test_fragment(&variables, path, stem);
646 variables.insert(RUST_TEST_FRAGMENT_TASK_VARIABLE, fragment);
647 };
648 if let Some(test_name) =
649 task_variables.get(&VariableName::Custom(Cow::Borrowed("_test_name")))
650 {
651 variables.insert(RUST_TEST_NAME_TASK_VARIABLE, test_name.into());
652 }
653 if let Some(doc_test_name) =
654 task_variables.get(&VariableName::Custom(Cow::Borrowed("_doc_test_name")))
655 {
656 variables.insert(RUST_DOC_TEST_NAME_TASK_VARIABLE, doc_test_name.into());
657 }
658 cx.background_spawn(async move {
659 if let Some(path) = local_abs_path
660 .as_deref()
661 .and_then(|local_abs_path| local_abs_path.parent())
662 && let Some(package_name) =
663 human_readable_package_name(path, project_env.as_ref()).await
664 {
665 variables.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name);
666 }
667 if let Some(path) = local_abs_path.as_ref()
668 && let Some((target, manifest_path)) =
669 target_info_from_abs_path(path, project_env.as_ref()).await
670 {
671 if let Some(target) = target {
672 variables.extend(TaskVariables::from_iter([
673 (RUST_PACKAGE_TASK_VARIABLE.clone(), target.package_name),
674 (RUST_BIN_NAME_TASK_VARIABLE.clone(), target.target_name),
675 (
676 RUST_BIN_KIND_TASK_VARIABLE.clone(),
677 target.target_kind.to_string(),
678 ),
679 ]));
680 if target.required_features.is_empty() {
681 variables.insert(RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE, "".into());
682 variables.insert(RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE, "".into());
683 } else {
684 variables.insert(
685 RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.clone(),
686 "--features".to_string(),
687 );
688 variables.insert(
689 RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.clone(),
690 target.required_features.join(","),
691 );
692 }
693 }
694 variables.extend(TaskVariables::from_iter([(
695 RUST_MANIFEST_DIRNAME_TASK_VARIABLE.clone(),
696 manifest_path.to_string_lossy().into_owned(),
697 )]));
698 }
699 Ok(variables)
700 })
701 }
702
703 fn associated_tasks(
704 &self,
705 file: Option<Arc<dyn language::File>>,
706 cx: &App,
707 ) -> Task<Option<TaskTemplates>> {
708 const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
709 const CUSTOM_TARGET_DIR: &str = "RUST_TARGET_DIR";
710
711 let language_sets = language_settings(Some("Rust".into()), file.as_ref(), cx);
712 let package_to_run = language_sets
713 .tasks
714 .variables
715 .get(DEFAULT_RUN_NAME_STR)
716 .cloned();
717 let custom_target_dir = language_sets
718 .tasks
719 .variables
720 .get(CUSTOM_TARGET_DIR)
721 .cloned();
722 let run_task_args = if let Some(package_to_run) = package_to_run {
723 vec!["run".into(), "-p".into(), package_to_run]
724 } else {
725 vec!["run".into()]
726 };
727 let mut task_templates = vec![
728 TaskTemplate {
729 label: format!(
730 "Check (package: {})",
731 RUST_PACKAGE_TASK_VARIABLE.template_value(),
732 ),
733 command: "cargo".into(),
734 args: vec![
735 "check".into(),
736 "-p".into(),
737 RUST_PACKAGE_TASK_VARIABLE.template_value(),
738 ],
739 cwd: Some("$ZED_DIRNAME".to_owned()),
740 ..TaskTemplate::default()
741 },
742 TaskTemplate {
743 label: "Check all targets (workspace)".into(),
744 command: "cargo".into(),
745 args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
746 cwd: Some("$ZED_DIRNAME".to_owned()),
747 ..TaskTemplate::default()
748 },
749 TaskTemplate {
750 label: format!(
751 "Test '{}' (package: {})",
752 RUST_TEST_NAME_TASK_VARIABLE.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 "--nocapture".into(),
762 "--include-ignored".into(),
763 RUST_TEST_NAME_TASK_VARIABLE.template_value(),
764 ],
765 tags: vec!["rust-test".to_owned()],
766 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
767 ..TaskTemplate::default()
768 },
769 TaskTemplate {
770 label: format!(
771 "Doc test '{}' (package: {})",
772 RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
773 RUST_PACKAGE_TASK_VARIABLE.template_value(),
774 ),
775 command: "cargo".into(),
776 args: vec![
777 "test".into(),
778 "--doc".into(),
779 "-p".into(),
780 RUST_PACKAGE_TASK_VARIABLE.template_value(),
781 "--".into(),
782 "--nocapture".into(),
783 "--include-ignored".into(),
784 RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
785 ],
786 tags: vec!["rust-doc-test".to_owned()],
787 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
788 ..TaskTemplate::default()
789 },
790 TaskTemplate {
791 label: format!(
792 "Test mod '{}' (package: {})",
793 VariableName::Stem.template_value(),
794 RUST_PACKAGE_TASK_VARIABLE.template_value(),
795 ),
796 command: "cargo".into(),
797 args: vec![
798 "test".into(),
799 "-p".into(),
800 RUST_PACKAGE_TASK_VARIABLE.template_value(),
801 "--".into(),
802 RUST_TEST_FRAGMENT_TASK_VARIABLE.template_value(),
803 ],
804 tags: vec!["rust-mod-test".to_owned()],
805 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
806 ..TaskTemplate::default()
807 },
808 TaskTemplate {
809 label: format!(
810 "Run {} {} (package: {})",
811 RUST_BIN_KIND_TASK_VARIABLE.template_value(),
812 RUST_BIN_NAME_TASK_VARIABLE.template_value(),
813 RUST_PACKAGE_TASK_VARIABLE.template_value(),
814 ),
815 command: "cargo".into(),
816 args: vec![
817 "run".into(),
818 "-p".into(),
819 RUST_PACKAGE_TASK_VARIABLE.template_value(),
820 format!("--{}", RUST_BIN_KIND_TASK_VARIABLE.template_value()),
821 RUST_BIN_NAME_TASK_VARIABLE.template_value(),
822 RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.template_value(),
823 RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.template_value(),
824 ],
825 cwd: Some("$ZED_DIRNAME".to_owned()),
826 tags: vec!["rust-main".to_owned()],
827 ..TaskTemplate::default()
828 },
829 TaskTemplate {
830 label: format!(
831 "Test (package: {})",
832 RUST_PACKAGE_TASK_VARIABLE.template_value()
833 ),
834 command: "cargo".into(),
835 args: vec![
836 "test".into(),
837 "-p".into(),
838 RUST_PACKAGE_TASK_VARIABLE.template_value(),
839 ],
840 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
841 ..TaskTemplate::default()
842 },
843 TaskTemplate {
844 label: "Run".into(),
845 command: "cargo".into(),
846 args: run_task_args,
847 cwd: Some("$ZED_DIRNAME".to_owned()),
848 ..TaskTemplate::default()
849 },
850 TaskTemplate {
851 label: "Clean".into(),
852 command: "cargo".into(),
853 args: vec!["clean".into()],
854 cwd: Some("$ZED_DIRNAME".to_owned()),
855 ..TaskTemplate::default()
856 },
857 ];
858
859 if let Some(custom_target_dir) = custom_target_dir {
860 task_templates = task_templates
861 .into_iter()
862 .map(|mut task_template| {
863 let mut args = task_template.args.split_off(1);
864 task_template.args.append(&mut vec![
865 "--target-dir".to_string(),
866 custom_target_dir.clone(),
867 ]);
868 task_template.args.append(&mut args);
869
870 task_template
871 })
872 .collect();
873 }
874
875 Task::ready(Some(TaskTemplates(task_templates)))
876 }
877
878 fn lsp_task_source(&self) -> Option<LanguageServerName> {
879 Some(SERVER_NAME)
880 }
881}
882
883/// Part of the data structure of Cargo metadata
884#[derive(Debug, serde::Deserialize)]
885struct CargoMetadata {
886 packages: Vec<CargoPackage>,
887}
888
889#[derive(Debug, serde::Deserialize)]
890struct CargoPackage {
891 id: String,
892 targets: Vec<CargoTarget>,
893 manifest_path: Arc<Path>,
894}
895
896#[derive(Debug, serde::Deserialize)]
897struct CargoTarget {
898 name: String,
899 kind: Vec<String>,
900 src_path: String,
901 #[serde(rename = "required-features", default)]
902 required_features: Vec<String>,
903}
904
905#[derive(Debug, PartialEq)]
906enum TargetKind {
907 Bin,
908 Example,
909}
910
911impl Display for TargetKind {
912 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
913 match self {
914 TargetKind::Bin => write!(f, "bin"),
915 TargetKind::Example => write!(f, "example"),
916 }
917 }
918}
919
920impl TryFrom<&str> for TargetKind {
921 type Error = ();
922 fn try_from(value: &str) -> Result<Self, ()> {
923 match value {
924 "bin" => Ok(Self::Bin),
925 "example" => Ok(Self::Example),
926 _ => Err(()),
927 }
928 }
929}
930/// Which package and binary target are we in?
931#[derive(Debug, PartialEq)]
932struct TargetInfo {
933 package_name: String,
934 target_name: String,
935 target_kind: TargetKind,
936 required_features: Vec<String>,
937}
938
939async fn target_info_from_abs_path(
940 abs_path: &Path,
941 project_env: Option<&HashMap<String, String>>,
942) -> Option<(Option<TargetInfo>, Arc<Path>)> {
943 let mut command = util::command::new_smol_command("cargo");
944 if let Some(envs) = project_env {
945 command.envs(envs);
946 }
947 let output = command
948 .current_dir(abs_path.parent()?)
949 .arg("metadata")
950 .arg("--no-deps")
951 .arg("--format-version")
952 .arg("1")
953 .output()
954 .await
955 .log_err()?
956 .stdout;
957
958 let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
959 target_info_from_metadata(metadata, abs_path)
960}
961
962fn target_info_from_metadata(
963 metadata: CargoMetadata,
964 abs_path: &Path,
965) -> Option<(Option<TargetInfo>, Arc<Path>)> {
966 let mut manifest_path = None;
967 for package in metadata.packages {
968 let Some(manifest_dir_path) = package.manifest_path.parent() else {
969 continue;
970 };
971
972 let Some(path_from_manifest_dir) = abs_path.strip_prefix(manifest_dir_path).ok() else {
973 continue;
974 };
975 let candidate_path_length = path_from_manifest_dir.components().count();
976 // Pick the most specific manifest path
977 if let Some((path, current_length)) = &mut manifest_path {
978 if candidate_path_length > *current_length {
979 *path = Arc::from(manifest_dir_path);
980 *current_length = candidate_path_length;
981 }
982 } else {
983 manifest_path = Some((Arc::from(manifest_dir_path), candidate_path_length));
984 };
985
986 for target in package.targets {
987 let Some(bin_kind) = target
988 .kind
989 .iter()
990 .find_map(|kind| TargetKind::try_from(kind.as_ref()).ok())
991 else {
992 continue;
993 };
994 let target_path = PathBuf::from(target.src_path);
995 if target_path == abs_path {
996 return manifest_path.map(|(path, _)| {
997 (
998 package_name_from_pkgid(&package.id).map(|package_name| TargetInfo {
999 package_name: package_name.to_owned(),
1000 target_name: target.name,
1001 required_features: target.required_features,
1002 target_kind: bin_kind,
1003 }),
1004 path,
1005 )
1006 });
1007 }
1008 }
1009 }
1010
1011 manifest_path.map(|(path, _)| (None, path))
1012}
1013
1014async fn human_readable_package_name(
1015 package_directory: &Path,
1016 project_env: Option<&HashMap<String, String>>,
1017) -> Option<String> {
1018 let mut command = util::command::new_smol_command("cargo");
1019 if let Some(envs) = project_env {
1020 command.envs(envs);
1021 }
1022 let pkgid = String::from_utf8(
1023 command
1024 .current_dir(package_directory)
1025 .arg("pkgid")
1026 .output()
1027 .await
1028 .log_err()?
1029 .stdout,
1030 )
1031 .ok()?;
1032 Some(package_name_from_pkgid(&pkgid)?.to_owned())
1033}
1034
1035// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
1036// Output example in the root of Zed project:
1037// ```sh
1038// ❯ cargo pkgid zed
1039// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
1040// ```
1041// Another variant, if a project has a custom package name or hyphen in the name:
1042// ```
1043// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
1044// ```
1045//
1046// Extracts the package name from the output according to the spec:
1047// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
1048fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
1049 fn split_off_suffix(input: &str, suffix_start: char) -> &str {
1050 match input.rsplit_once(suffix_start) {
1051 Some((without_suffix, _)) => without_suffix,
1052 None => input,
1053 }
1054 }
1055
1056 let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
1057 let package_name = match version_suffix.rsplit_once('@') {
1058 Some((custom_package_name, _version)) => custom_package_name,
1059 None => {
1060 let host_and_path = split_off_suffix(version_prefix, '?');
1061 let (_, package_name) = host_and_path.rsplit_once('/')?;
1062 package_name
1063 }
1064 };
1065 Some(package_name)
1066}
1067
1068async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
1069 maybe!(async {
1070 let mut last = None;
1071 let mut entries = fs::read_dir(&container_dir).await?;
1072 while let Some(entry) = entries.next().await {
1073 let path = entry?.path();
1074 if path.extension().is_some_and(|ext| ext == "metadata") {
1075 continue;
1076 }
1077 last = Some(path);
1078 }
1079
1080 let path = last.context("no cached binary")?;
1081 let path = match RustLspAdapter::GITHUB_ASSET_KIND {
1082 AssetKind::TarGz | AssetKind::Gz => path, // Tar and gzip extract in place.
1083 AssetKind::Zip => path.join("rust-analyzer.exe"), // zip contains a .exe
1084 };
1085
1086 anyhow::Ok(LanguageServerBinary {
1087 path,
1088 env: None,
1089 arguments: Default::default(),
1090 })
1091 })
1092 .await
1093 .log_err()
1094}
1095
1096fn test_fragment(variables: &TaskVariables, path: &Path, stem: &str) -> String {
1097 let fragment = if stem == "lib" {
1098 // This isn't quite right---it runs the tests for the entire library, rather than
1099 // just for the top-level `mod tests`. But we don't really have the means here to
1100 // filter out just that module.
1101 Some("--lib".to_owned())
1102 } else if stem == "mod" {
1103 maybe!({ Some(path.parent()?.file_name()?.to_string_lossy().into_owned()) })
1104 } else if stem == "main" {
1105 if let (Some(bin_name), Some(bin_kind)) = (
1106 variables.get(&RUST_BIN_NAME_TASK_VARIABLE),
1107 variables.get(&RUST_BIN_KIND_TASK_VARIABLE),
1108 ) {
1109 Some(format!("--{bin_kind}={bin_name}"))
1110 } else {
1111 None
1112 }
1113 } else {
1114 Some(stem.to_owned())
1115 };
1116 fragment.unwrap_or_else(|| "--".to_owned())
1117}
1118
1119#[cfg(test)]
1120mod tests {
1121 use std::num::NonZeroU32;
1122
1123 use super::*;
1124 use crate::language;
1125 use gpui::{BorrowAppContext, Hsla, TestAppContext};
1126 use lsp::CompletionItemLabelDetails;
1127 use settings::SettingsStore;
1128 use theme::SyntaxTheme;
1129 use util::path;
1130
1131 #[gpui::test]
1132 async fn test_process_rust_diagnostics() {
1133 let mut params = lsp::PublishDiagnosticsParams {
1134 uri: lsp::Uri::from_file_path(path!("/a")).unwrap(),
1135 version: None,
1136 diagnostics: vec![
1137 // no newlines
1138 lsp::Diagnostic {
1139 message: "use of moved value `a`".to_string(),
1140 ..Default::default()
1141 },
1142 // newline at the end of a code span
1143 lsp::Diagnostic {
1144 message: "consider importing this struct: `use b::c;\n`".to_string(),
1145 ..Default::default()
1146 },
1147 // code span starting right after a newline
1148 lsp::Diagnostic {
1149 message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1150 .to_string(),
1151 ..Default::default()
1152 },
1153 ],
1154 };
1155 RustLspAdapter.process_diagnostics(&mut params, LanguageServerId(0), None);
1156
1157 assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
1158
1159 // remove trailing newline from code span
1160 assert_eq!(
1161 params.diagnostics[1].message,
1162 "consider importing this struct: `use b::c;`"
1163 );
1164
1165 // do not remove newline before the start of code span
1166 assert_eq!(
1167 params.diagnostics[2].message,
1168 "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1169 );
1170 }
1171
1172 #[gpui::test]
1173 async fn test_rust_label_for_completion() {
1174 let adapter = Arc::new(RustLspAdapter);
1175 let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1176 let grammar = language.grammar().unwrap();
1177 let theme = SyntaxTheme::new_test([
1178 ("type", Hsla::default()),
1179 ("keyword", Hsla::default()),
1180 ("function", Hsla::default()),
1181 ("property", Hsla::default()),
1182 ]);
1183
1184 language.set_theme(&theme);
1185
1186 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1187 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1188 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1189 let highlight_field = grammar.highlight_id_for_name("property").unwrap();
1190
1191 assert_eq!(
1192 adapter
1193 .label_for_completion(
1194 &lsp::CompletionItem {
1195 kind: Some(lsp::CompletionItemKind::FUNCTION),
1196 label: "hello(…)".to_string(),
1197 label_details: Some(CompletionItemLabelDetails {
1198 detail: Some("(use crate::foo)".into()),
1199 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string())
1200 }),
1201 ..Default::default()
1202 },
1203 &language
1204 )
1205 .await,
1206 Some(CodeLabel::new(
1207 "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1208 0..5,
1209 vec![
1210 (0..5, highlight_function),
1211 (7..10, highlight_keyword),
1212 (11..17, highlight_type),
1213 (18..19, highlight_type),
1214 (25..28, highlight_type),
1215 (29..30, highlight_type),
1216 ],
1217 ))
1218 );
1219 assert_eq!(
1220 adapter
1221 .label_for_completion(
1222 &lsp::CompletionItem {
1223 kind: Some(lsp::CompletionItemKind::FUNCTION),
1224 label: "hello(…)".to_string(),
1225 label_details: Some(CompletionItemLabelDetails {
1226 detail: Some("(use crate::foo)".into()),
1227 description: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
1228 }),
1229 ..Default::default()
1230 },
1231 &language
1232 )
1233 .await,
1234 Some(CodeLabel::new(
1235 "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1236 0..5,
1237 vec![
1238 (0..5, highlight_function),
1239 (7..10, highlight_keyword),
1240 (11..17, highlight_type),
1241 (18..19, highlight_type),
1242 (25..28, highlight_type),
1243 (29..30, highlight_type),
1244 ],
1245 ))
1246 );
1247 assert_eq!(
1248 adapter
1249 .label_for_completion(
1250 &lsp::CompletionItem {
1251 kind: Some(lsp::CompletionItemKind::FIELD),
1252 label: "len".to_string(),
1253 detail: Some("usize".to_string()),
1254 ..Default::default()
1255 },
1256 &language
1257 )
1258 .await,
1259 Some(CodeLabel::new(
1260 "len: usize".to_string(),
1261 0..3,
1262 vec![(0..3, highlight_field), (5..10, highlight_type),],
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
1277 ..Default::default()
1278 },
1279 &language
1280 )
1281 .await,
1282 Some(CodeLabel::new(
1283 "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1284 0..5,
1285 vec![
1286 (0..5, highlight_function),
1287 (7..10, highlight_keyword),
1288 (11..17, highlight_type),
1289 (18..19, highlight_type),
1290 (25..28, highlight_type),
1291 (29..30, highlight_type),
1292 ],
1293 ))
1294 );
1295
1296 assert_eq!(
1297 adapter
1298 .label_for_completion(
1299 &lsp::CompletionItem {
1300 kind: Some(lsp::CompletionItemKind::FUNCTION),
1301 label: "hello".to_string(),
1302 label_details: Some(CompletionItemLabelDetails {
1303 detail: Some("(use crate::foo)".to_string()),
1304 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1305 }),
1306 ..Default::default()
1307 },
1308 &language
1309 )
1310 .await,
1311 Some(CodeLabel::new(
1312 "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1313 0..5,
1314 vec![
1315 (0..5, highlight_function),
1316 (7..10, highlight_keyword),
1317 (11..17, highlight_type),
1318 (18..19, highlight_type),
1319 (25..28, highlight_type),
1320 (29..30, highlight_type),
1321 ],
1322 ))
1323 );
1324
1325 assert_eq!(
1326 adapter
1327 .label_for_completion(
1328 &lsp::CompletionItem {
1329 kind: Some(lsp::CompletionItemKind::METHOD),
1330 label: "await.as_deref_mut()".to_string(),
1331 filter_text: Some("as_deref_mut".to_string()),
1332 label_details: Some(CompletionItemLabelDetails {
1333 detail: None,
1334 description: Some("fn(&mut self) -> IterMut<'_, T>".to_string()),
1335 }),
1336 ..Default::default()
1337 },
1338 &language
1339 )
1340 .await,
1341 Some(CodeLabel::new(
1342 "await.as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1343 6..18,
1344 vec![
1345 (6..18, HighlightId(2)),
1346 (20..23, HighlightId(1)),
1347 (33..40, HighlightId(0)),
1348 (45..46, HighlightId(0))
1349 ],
1350 ))
1351 );
1352
1353 assert_eq!(
1354 adapter
1355 .label_for_completion(
1356 &lsp::CompletionItem {
1357 kind: Some(lsp::CompletionItemKind::METHOD),
1358 label: "as_deref_mut()".to_string(),
1359 filter_text: Some("as_deref_mut".to_string()),
1360 label_details: Some(CompletionItemLabelDetails {
1361 detail: None,
1362 description: Some(
1363 "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string()
1364 ),
1365 }),
1366 ..Default::default()
1367 },
1368 &language
1369 )
1370 .await,
1371 Some(CodeLabel::new(
1372 "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1373 7..19,
1374 vec![
1375 (0..3, HighlightId(1)),
1376 (4..6, HighlightId(1)),
1377 (7..19, HighlightId(2)),
1378 (21..24, HighlightId(1)),
1379 (34..41, HighlightId(0)),
1380 (46..47, HighlightId(0))
1381 ],
1382 ))
1383 );
1384
1385 assert_eq!(
1386 adapter
1387 .label_for_completion(
1388 &lsp::CompletionItem {
1389 kind: Some(lsp::CompletionItemKind::FIELD),
1390 label: "inner_value".to_string(),
1391 filter_text: Some("value".to_string()),
1392 detail: Some("String".to_string()),
1393 ..Default::default()
1394 },
1395 &language,
1396 )
1397 .await,
1398 Some(CodeLabel::new(
1399 "inner_value: String".to_string(),
1400 6..11,
1401 vec![(0..11, HighlightId(3)), (13..19, HighlightId(0))],
1402 ))
1403 );
1404 }
1405
1406 #[gpui::test]
1407 async fn test_rust_label_for_symbol() {
1408 let adapter = Arc::new(RustLspAdapter);
1409 let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1410 let grammar = language.grammar().unwrap();
1411 let theme = SyntaxTheme::new_test([
1412 ("type", Hsla::default()),
1413 ("keyword", Hsla::default()),
1414 ("function", Hsla::default()),
1415 ("property", Hsla::default()),
1416 ]);
1417
1418 language.set_theme(&theme);
1419
1420 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1421 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1422 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1423
1424 assert_eq!(
1425 adapter
1426 .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
1427 .await,
1428 Some(CodeLabel::new(
1429 "fn hello".to_string(),
1430 3..8,
1431 vec![(0..2, highlight_keyword), (3..8, highlight_function)],
1432 ))
1433 );
1434
1435 assert_eq!(
1436 adapter
1437 .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
1438 .await,
1439 Some(CodeLabel::new(
1440 "type World".to_string(),
1441 5..10,
1442 vec![(0..4, highlight_keyword), (5..10, highlight_type)],
1443 ))
1444 );
1445 }
1446
1447 #[gpui::test]
1448 async fn test_rust_autoindent(cx: &mut TestAppContext) {
1449 // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
1450 cx.update(|cx| {
1451 let test_settings = SettingsStore::test(cx);
1452 cx.set_global(test_settings);
1453 cx.update_global::<SettingsStore, _>(|store, cx| {
1454 store.update_user_settings(cx, |s| {
1455 s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
1456 });
1457 });
1458 });
1459
1460 let language = crate::language("rust", tree_sitter_rust::LANGUAGE.into());
1461
1462 cx.new(|cx| {
1463 let mut buffer = Buffer::local("", cx).with_language(language, cx);
1464
1465 // indent between braces
1466 buffer.set_text("fn a() {}", cx);
1467 let ix = buffer.len() - 1;
1468 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1469 assert_eq!(buffer.text(), "fn a() {\n \n}");
1470
1471 // indent between braces, even after empty lines
1472 buffer.set_text("fn a() {\n\n\n}", cx);
1473 let ix = buffer.len() - 2;
1474 buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1475 assert_eq!(buffer.text(), "fn a() {\n\n\n \n}");
1476
1477 // indent a line that continues a field expression
1478 buffer.set_text("fn a() {\n \n}", cx);
1479 let ix = buffer.len() - 2;
1480 buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
1481 assert_eq!(buffer.text(), "fn a() {\n b\n .c\n}");
1482
1483 // indent further lines that continue the field expression, even after empty lines
1484 let ix = buffer.len() - 2;
1485 buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
1486 assert_eq!(buffer.text(), "fn a() {\n b\n .c\n \n .d\n}");
1487
1488 // dedent the line after the field expression
1489 let ix = buffer.len() - 2;
1490 buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
1491 assert_eq!(
1492 buffer.text(),
1493 "fn a() {\n b\n .c\n \n .d;\n e\n}"
1494 );
1495
1496 // indent inside a struct within a call
1497 buffer.set_text("const a: B = c(D {});", cx);
1498 let ix = buffer.len() - 3;
1499 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1500 assert_eq!(buffer.text(), "const a: B = c(D {\n \n});");
1501
1502 // indent further inside a nested call
1503 let ix = buffer.len() - 4;
1504 buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
1505 assert_eq!(buffer.text(), "const a: B = c(D {\n e: f(\n \n )\n});");
1506
1507 // keep that indent after an empty line
1508 let ix = buffer.len() - 8;
1509 buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1510 assert_eq!(
1511 buffer.text(),
1512 "const a: B = c(D {\n e: f(\n \n \n )\n});"
1513 );
1514
1515 buffer
1516 });
1517 }
1518
1519 #[test]
1520 fn test_package_name_from_pkgid() {
1521 for (input, expected) in [
1522 (
1523 "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
1524 "zed",
1525 ),
1526 (
1527 "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
1528 "my-custom-package",
1529 ),
1530 ] {
1531 assert_eq!(package_name_from_pkgid(input), Some(expected));
1532 }
1533 }
1534
1535 #[test]
1536 fn test_target_info_from_metadata() {
1537 for (input, absolute_path, expected) in [
1538 (
1539 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"}]}]}"#,
1540 "/path/to/zed/src/main.rs",
1541 Some((
1542 Some(TargetInfo {
1543 package_name: "zed".into(),
1544 target_name: "zed".into(),
1545 required_features: Vec::new(),
1546 target_kind: TargetKind::Bin,
1547 }),
1548 Arc::from("/path/to/zed".as_ref()),
1549 )),
1550 ),
1551 (
1552 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"}]}]}"#,
1553 "/path/to/custom-package/src/main.rs",
1554 Some((
1555 Some(TargetInfo {
1556 package_name: "my-custom-package".into(),
1557 target_name: "my-custom-bin".into(),
1558 required_features: Vec::new(),
1559 target_kind: TargetKind::Bin,
1560 }),
1561 Arc::from("/path/to/custom-package".as_ref()),
1562 )),
1563 ),
1564 (
1565 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"}]}"#,
1566 "/path/to/custom-package/src/main.rs",
1567 Some((
1568 Some(TargetInfo {
1569 package_name: "my-custom-package".into(),
1570 target_name: "my-custom-bin".into(),
1571 required_features: Vec::new(),
1572 target_kind: TargetKind::Example,
1573 }),
1574 Arc::from("/path/to/custom-package".as_ref()),
1575 )),
1576 ),
1577 (
1578 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"]}]}]}"#,
1579 "/path/to/custom-package/src/main.rs",
1580 Some((
1581 Some(TargetInfo {
1582 package_name: "my-custom-package".into(),
1583 target_name: "my-custom-bin".into(),
1584 required_features: vec!["foo".to_owned(), "bar".to_owned()],
1585 target_kind: TargetKind::Example,
1586 }),
1587 Arc::from("/path/to/custom-package".as_ref()),
1588 )),
1589 ),
1590 (
1591 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"}]}"#,
1592 "/path/to/custom-package/src/main.rs",
1593 Some((
1594 Some(TargetInfo {
1595 package_name: "my-custom-package".into(),
1596 target_name: "my-custom-bin".into(),
1597 required_features: vec![],
1598 target_kind: TargetKind::Example,
1599 }),
1600 Arc::from("/path/to/custom-package".as_ref()),
1601 )),
1602 ),
1603 (
1604 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"}]}"#,
1605 "/path/to/custom-package/src/main.rs",
1606 Some((None, Arc::from("/path/to/custom-package".as_ref()))),
1607 ),
1608 ] {
1609 let metadata: CargoMetadata = serde_json::from_str(input).context(input).unwrap();
1610
1611 let absolute_path = Path::new(absolute_path);
1612
1613 assert_eq!(target_info_from_metadata(metadata, absolute_path), expected);
1614 }
1615 }
1616
1617 #[test]
1618 fn test_rust_test_fragment() {
1619 #[track_caller]
1620 fn check(
1621 variables: impl IntoIterator<Item = (VariableName, &'static str)>,
1622 path: &str,
1623 expected: &str,
1624 ) {
1625 let path = Path::new(path);
1626 let found = test_fragment(
1627 &TaskVariables::from_iter(variables.into_iter().map(|(k, v)| (k, v.to_owned()))),
1628 path,
1629 path.file_stem().unwrap().to_str().unwrap(),
1630 );
1631 assert_eq!(expected, found);
1632 }
1633
1634 check([], "/project/src/lib.rs", "--lib");
1635 check([], "/project/src/foo/mod.rs", "foo");
1636 check(
1637 [
1638 (RUST_BIN_KIND_TASK_VARIABLE.clone(), "bin"),
1639 (RUST_BIN_NAME_TASK_VARIABLE, "x"),
1640 ],
1641 "/project/src/main.rs",
1642 "--bin=x",
1643 );
1644 check([], "/project/src/main.rs", "--");
1645 }
1646}