1use anyhow::{Context as _, Result};
2use async_trait::async_trait;
3use collections::HashMap;
4use futures::StreamExt;
5use futures::lock::OwnedMutexGuard;
6use gpui::{App, AppContext, AsyncApp, SharedString, Task};
7use http_client::github::AssetKind;
8use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
9use http_client::github_download::{GithubBinaryMetadata, download_server_binary};
10pub use language::*;
11use lsp::{InitializeParams, LanguageServerBinary, LanguageServerBinaryOptions};
12use project::lsp_store::rust_analyzer_ext::CARGO_DIAGNOSTICS_SOURCE_NAME;
13use project::project_settings::ProjectSettings;
14use regex::Regex;
15use serde_json::json;
16use settings::Settings as _;
17use smallvec::SmallVec;
18use smol::fs::{self};
19use std::cmp::Reverse;
20use std::fmt::Display;
21use std::ops::Range;
22use std::process::Stdio;
23use std::{
24 borrow::Cow,
25 path::{Path, PathBuf},
26 sync::{Arc, LazyLock},
27};
28use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
29use util::fs::{make_file_executable, remove_matching};
30use util::merge_json_value_into;
31use util::rel_path::RelPath;
32use util::{ResultExt, maybe};
33
34use crate::language_settings::language_settings;
35
36pub struct RustLspAdapter;
37
38#[cfg(target_os = "macos")]
39impl RustLspAdapter {
40 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
41 const ARCH_SERVER_NAME: &str = "apple-darwin";
42}
43
44#[cfg(target_os = "linux")]
45impl RustLspAdapter {
46 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
47 const ARCH_SERVER_NAME: &str = "unknown-linux";
48}
49
50#[cfg(target_os = "freebsd")]
51impl RustLspAdapter {
52 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
53 const ARCH_SERVER_NAME: &str = "unknown-freebsd";
54}
55
56#[cfg(target_os = "windows")]
57impl RustLspAdapter {
58 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
59 const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
60}
61
62const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer");
63
64#[cfg(target_os = "linux")]
65enum LibcType {
66 Gnu,
67 Musl,
68}
69
70impl RustLspAdapter {
71 fn convert_rust_analyzer_schema(raw_schema: &serde_json::Value) -> serde_json::Value {
72 let Some(schema_array) = raw_schema.as_array() else {
73 return raw_schema.clone();
74 };
75
76 let mut root_properties = serde_json::Map::new();
77
78 for item in schema_array {
79 if let Some(props) = item.get("properties").and_then(|p| p.as_object()) {
80 for (key, value) in props {
81 let parts: Vec<&str> = key.split('.').collect();
82
83 if parts.is_empty() {
84 continue;
85 }
86
87 let parts_to_process = if parts.first() == Some(&"rust-analyzer") {
88 &parts[1..]
89 } else {
90 &parts[..]
91 };
92
93 if parts_to_process.is_empty() {
94 continue;
95 }
96
97 let mut current = &mut root_properties;
98
99 for (i, part) in parts_to_process.iter().enumerate() {
100 let is_last = i == parts_to_process.len() - 1;
101
102 if is_last {
103 current.insert(part.to_string(), value.clone());
104 } else {
105 let next_current = current
106 .entry(part.to_string())
107 .or_insert_with(|| {
108 serde_json::json!({
109 "type": "object",
110 "properties": {}
111 })
112 })
113 .as_object_mut()
114 .expect("should be an object")
115 .entry("properties")
116 .or_insert_with(|| serde_json::json!({}))
117 .as_object_mut()
118 .expect("properties should be an object");
119
120 current = next_current;
121 }
122 }
123 }
124 }
125 }
126
127 serde_json::json!({
128 "type": "object",
129 "properties": root_properties
130 })
131 }
132
133 #[cfg(target_os = "linux")]
134 async fn determine_libc_type() -> LibcType {
135 use futures::pin_mut;
136
137 async fn from_ldd_version() -> Option<LibcType> {
138 use util::command::new_smol_command;
139
140 let ldd_output = new_smol_command("ldd")
141 .arg("--version")
142 .output()
143 .await
144 .ok()?;
145 let ldd_version = String::from_utf8_lossy(&ldd_output.stdout);
146
147 if ldd_version.contains("GNU libc") || ldd_version.contains("GLIBC") {
148 Some(LibcType::Gnu)
149 } else if ldd_version.contains("musl") {
150 Some(LibcType::Musl)
151 } else {
152 None
153 }
154 }
155
156 if let Some(libc_type) = from_ldd_version().await {
157 return libc_type;
158 }
159
160 let Ok(dir_entries) = smol::fs::read_dir("/lib").await else {
161 // defaulting to gnu because nix doesn't have /lib files due to not following FHS
162 return LibcType::Gnu;
163 };
164 let dir_entries = dir_entries.filter_map(async move |e| e.ok());
165 pin_mut!(dir_entries);
166
167 let mut has_musl = false;
168 let mut has_gnu = false;
169
170 while let Some(entry) = dir_entries.next().await {
171 let file_name = entry.file_name();
172 let file_name = file_name.to_string_lossy();
173 if file_name.starts_with("ld-musl-") {
174 has_musl = true;
175 } else if file_name.starts_with("ld-linux-") {
176 has_gnu = true;
177 }
178 }
179
180 match (has_musl, has_gnu) {
181 (true, _) => LibcType::Musl,
182 (_, true) => LibcType::Gnu,
183 _ => LibcType::Gnu,
184 }
185 }
186
187 #[cfg(target_os = "linux")]
188 async fn build_arch_server_name_linux() -> String {
189 let libc = match Self::determine_libc_type().await {
190 LibcType::Musl => "musl",
191 LibcType::Gnu => "gnu",
192 };
193
194 format!("{}-{}", Self::ARCH_SERVER_NAME, libc)
195 }
196
197 async fn build_asset_name() -> String {
198 let extension = match Self::GITHUB_ASSET_KIND {
199 AssetKind::TarGz => "tar.gz",
200 AssetKind::Gz => "gz",
201 AssetKind::Zip => "zip",
202 };
203
204 #[cfg(target_os = "linux")]
205 let arch_server_name = Self::build_arch_server_name_linux().await;
206 #[cfg(not(target_os = "linux"))]
207 let arch_server_name = Self::ARCH_SERVER_NAME.to_string();
208
209 format!(
210 "{}-{}-{}.{}",
211 SERVER_NAME,
212 std::env::consts::ARCH,
213 &arch_server_name,
214 extension
215 )
216 }
217}
218
219pub(crate) struct CargoManifestProvider;
220
221impl ManifestProvider for CargoManifestProvider {
222 fn name(&self) -> ManifestName {
223 SharedString::new_static("Cargo.toml").into()
224 }
225
226 fn search(
227 &self,
228 ManifestQuery {
229 path,
230 depth,
231 delegate,
232 }: ManifestQuery,
233 ) -> Option<Arc<RelPath>> {
234 let mut outermost_cargo_toml = None;
235 for path in path.ancestors().take(depth) {
236 let p = path.join(RelPath::unix("Cargo.toml").unwrap());
237 if delegate.exists(&p, Some(false)) {
238 outermost_cargo_toml = Some(Arc::from(path));
239 }
240 }
241
242 outermost_cargo_toml
243 }
244}
245
246#[async_trait(?Send)]
247impl LspAdapter for RustLspAdapter {
248 fn name(&self) -> LanguageServerName {
249 SERVER_NAME
250 }
251
252 fn disk_based_diagnostic_sources(&self) -> Vec<String> {
253 vec![CARGO_DIAGNOSTICS_SOURCE_NAME.to_owned()]
254 }
255
256 fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
257 Some("rust-analyzer/flycheck".into())
258 }
259
260 fn process_diagnostics(
261 &self,
262 params: &mut lsp::PublishDiagnosticsParams,
263 _: LanguageServerId,
264 _: Option<&'_ Buffer>,
265 ) {
266 static REGEX: LazyLock<Regex> =
267 LazyLock::new(|| Regex::new(r"(?m)`([^`]+)\n`$").expect("Failed to create REGEX"));
268
269 for diagnostic in &mut params.diagnostics {
270 for message in diagnostic
271 .related_information
272 .iter_mut()
273 .flatten()
274 .map(|info| &mut info.message)
275 .chain([&mut diagnostic.message])
276 {
277 if let Cow::Owned(sanitized) = REGEX.replace_all(message, "`$1`") {
278 *message = sanitized;
279 }
280 }
281 }
282 }
283
284 fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
285 static REGEX: LazyLock<Regex> =
286 LazyLock::new(|| Regex::new(r"(?m)\n *").expect("Failed to create REGEX"));
287 Some(REGEX.replace_all(message, "\n\n").to_string())
288 }
289
290 async fn label_for_completion(
291 &self,
292 completion: &lsp::CompletionItem,
293 language: &Arc<Language>,
294 ) -> Option<CodeLabel> {
295 // rust-analyzer calls these detail left and detail right in terms of where it expects things to be rendered
296 // this usually contains signatures of the thing to be completed
297 let detail_right = completion
298 .label_details
299 .as_ref()
300 .and_then(|detail| detail.description.as_ref())
301 .or(completion.detail.as_ref())
302 .map(|detail| detail.trim());
303 // this tends to contain alias and import information
304 let mut detail_left = completion
305 .label_details
306 .as_ref()
307 .and_then(|detail| detail.detail.as_deref());
308 let mk_label = |text: String, filter_range: &dyn Fn() -> Range<usize>, runs| {
309 let filter_range = completion
310 .filter_text
311 .as_deref()
312 .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
313 .or_else(|| {
314 text.find(&completion.label)
315 .map(|ix| ix..ix + completion.label.len())
316 })
317 .unwrap_or_else(filter_range);
318
319 CodeLabel::new(text, filter_range, runs)
320 };
321 let mut label = match (detail_right, completion.kind) {
322 (Some(signature), Some(lsp::CompletionItemKind::FIELD)) => {
323 let name = &completion.label;
324 let text = format!("{name}: {signature}");
325 let prefix = "struct S { ";
326 let source = Rope::from_iter([prefix, &text, " }"]);
327 let runs =
328 language.highlight_text(&source, prefix.len()..prefix.len() + text.len());
329 mk_label(text, &|| 0..completion.label.len(), runs)
330 }
331 (
332 Some(signature),
333 Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE),
334 ) if completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) => {
335 let name = &completion.label;
336 let text = format!("{name}: {signature}",);
337 let prefix = "let ";
338 let source = Rope::from_iter([prefix, &text, " = ();"]);
339 let runs =
340 language.highlight_text(&source, prefix.len()..prefix.len() + text.len());
341 mk_label(text, &|| 0..completion.label.len(), runs)
342 }
343 (
344 function_signature,
345 Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD),
346 ) => {
347 const FUNCTION_PREFIXES: [&str; 6] = [
348 "async fn",
349 "async unsafe fn",
350 "const fn",
351 "const unsafe fn",
352 "unsafe fn",
353 "fn",
354 ];
355 let fn_prefixed = FUNCTION_PREFIXES.iter().find_map(|&prefix| {
356 function_signature?
357 .strip_prefix(prefix)
358 .map(|suffix| (prefix, suffix))
359 });
360 let label = if let Some(label) = completion
361 .label
362 .strip_suffix("(…)")
363 .or_else(|| completion.label.strip_suffix("()"))
364 {
365 label
366 } else {
367 &completion.label
368 };
369
370 static FULL_SIGNATURE_REGEX: LazyLock<Regex> =
371 LazyLock::new(|| Regex::new(r"fn (.?+)\(").expect("Failed to create REGEX"));
372 if let Some((function_signature, match_)) = function_signature
373 .filter(|it| it.contains(&label))
374 .and_then(|it| Some((it, FULL_SIGNATURE_REGEX.find(it)?)))
375 {
376 let source = Rope::from(function_signature);
377 let runs = language.highlight_text(&source, 0..function_signature.len());
378 mk_label(
379 function_signature.to_owned(),
380 &|| match_.range().start - 3..match_.range().end - 1,
381 runs,
382 )
383 } else if let Some((prefix, suffix)) = fn_prefixed {
384 let text = format!("{label}{suffix}");
385 let source = Rope::from_iter([prefix, " ", &text, " {}"]);
386 let run_start = prefix.len() + 1;
387 let runs = language.highlight_text(&source, run_start..run_start + text.len());
388 mk_label(text, &|| 0..label.len(), runs)
389 } else if completion
390 .detail
391 .as_ref()
392 .is_some_and(|detail| detail.starts_with("macro_rules! "))
393 {
394 let text = completion.label.clone();
395 let len = text.len();
396 let source = Rope::from(text.as_str());
397 let runs = language.highlight_text(&source, 0..len);
398 mk_label(text, &|| 0..completion.label.len(), runs)
399 } else if detail_left.is_none() {
400 return None;
401 } else {
402 mk_label(
403 completion.label.clone(),
404 &|| 0..completion.label.len(),
405 vec![],
406 )
407 }
408 }
409 (_, kind) => {
410 let mut label;
411 let mut runs = vec![];
412
413 if completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET)
414 && let Some(
415 lsp::CompletionTextEdit::InsertAndReplace(lsp::InsertReplaceEdit {
416 new_text,
417 ..
418 })
419 | lsp::CompletionTextEdit::Edit(lsp::TextEdit { new_text, .. }),
420 ) = completion.text_edit.as_ref()
421 && let Ok(mut snippet) = snippet::Snippet::parse(new_text)
422 && snippet.tabstops.len() > 1
423 {
424 label = String::new();
425
426 // we never display the final tabstop
427 snippet.tabstops.remove(snippet.tabstops.len() - 1);
428
429 let mut text_pos = 0;
430
431 let mut all_stop_ranges = snippet
432 .tabstops
433 .into_iter()
434 .flat_map(|stop| stop.ranges)
435 .collect::<SmallVec<[_; 8]>>();
436 all_stop_ranges.sort_unstable_by_key(|a| (a.start, Reverse(a.end)));
437
438 for range in &all_stop_ranges {
439 let start_pos = range.start as usize;
440 let end_pos = range.end as usize;
441
442 label.push_str(&snippet.text[text_pos..start_pos]);
443
444 if start_pos == end_pos {
445 let caret_start = label.len();
446 label.push('…');
447 runs.push((caret_start..label.len(), HighlightId::TABSTOP_INSERT_ID));
448 } else {
449 let label_start = label.len();
450 label.push_str(&snippet.text[start_pos..end_pos]);
451 let label_end = label.len();
452 runs.push((label_start..label_end, HighlightId::TABSTOP_REPLACE_ID));
453 }
454
455 text_pos = end_pos;
456 }
457
458 label.push_str(&snippet.text[text_pos..]);
459
460 if detail_left.is_some_and(|detail_left| detail_left == new_text) {
461 // We only include the left detail if it isn't the snippet again
462 detail_left.take();
463 }
464
465 runs.extend(language.highlight_text(&Rope::from(&label), 0..label.len()));
466 } else {
467 let highlight_name = kind.and_then(|kind| match kind {
468 lsp::CompletionItemKind::STRUCT
469 | lsp::CompletionItemKind::INTERFACE
470 | lsp::CompletionItemKind::ENUM => Some("type"),
471 lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
472 lsp::CompletionItemKind::KEYWORD => Some("keyword"),
473 lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
474 Some("constant")
475 }
476 _ => None,
477 });
478
479 label = completion.label.clone();
480
481 if let Some(highlight_name) = highlight_name {
482 let highlight_id =
483 language.grammar()?.highlight_id_for_name(highlight_name)?;
484 runs.push((
485 0..label.rfind('(').unwrap_or(completion.label.len()),
486 highlight_id,
487 ));
488 } else if detail_left.is_none()
489 && kind != Some(lsp::CompletionItemKind::SNIPPET)
490 {
491 return None;
492 }
493 }
494
495 let label_len = label.len();
496
497 mk_label(label, &|| 0..label_len, runs)
498 }
499 };
500
501 if let Some(detail_left) = detail_left {
502 label.text.push(' ');
503 if !detail_left.starts_with('(') {
504 label.text.push('(');
505 }
506 label.text.push_str(detail_left);
507 if !detail_left.ends_with(')') {
508 label.text.push(')');
509 }
510 }
511
512 Some(label)
513 }
514
515 async fn initialization_options_schema(
516 self: Arc<Self>,
517 delegate: &Arc<dyn LspAdapterDelegate>,
518 cached_binary: OwnedMutexGuard<Option<(bool, LanguageServerBinary)>>,
519 cx: &mut AsyncApp,
520 ) -> Option<serde_json::Value> {
521 let binary = self
522 .get_language_server_command(
523 delegate.clone(),
524 None,
525 LanguageServerBinaryOptions {
526 allow_path_lookup: true,
527 allow_binary_download: false,
528 pre_release: false,
529 },
530 cached_binary,
531 cx.clone(),
532 )
533 .await
534 .0
535 .ok()?;
536
537 let mut command = util::command::new_smol_command(&binary.path);
538 command
539 .arg("--print-config-schema")
540 .stdout(Stdio::piped())
541 .stderr(Stdio::piped());
542 let cmd = command
543 .spawn()
544 .map_err(|e| log::debug!("failed to spawn command {command:?}: {e}"))
545 .ok()?;
546 let output = cmd
547 .output()
548 .await
549 .map_err(|e| log::debug!("failed to execute command {command:?}: {e}"))
550 .ok()?;
551 if !output.status.success() {
552 return None;
553 }
554
555 let raw_schema: serde_json::Value = serde_json::from_slice(output.stdout.as_slice())
556 .map_err(|e| log::debug!("failed to parse rust-analyzer's JSON schema output: {e}"))
557 .ok()?;
558
559 // Convert rust-analyzer's array-based schema format to nested JSON Schema
560 let converted_schema = Self::convert_rust_analyzer_schema(&raw_schema);
561 Some(converted_schema)
562 }
563
564 async fn label_for_symbol(
565 &self,
566 name: &str,
567 kind: lsp::SymbolKind,
568 language: &Arc<Language>,
569 ) -> Option<CodeLabel> {
570 let (prefix, suffix) = match kind {
571 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => ("fn ", "();"),
572 lsp::SymbolKind::STRUCT => ("struct ", ";"),
573 lsp::SymbolKind::ENUM => ("enum ", "{}"),
574 lsp::SymbolKind::INTERFACE => ("trait ", "{}"),
575 lsp::SymbolKind::CONSTANT => ("const ", ":()=();"),
576 lsp::SymbolKind::MODULE => ("mod ", ";"),
577 lsp::SymbolKind::PACKAGE => ("extern crate ", ";"),
578 lsp::SymbolKind::TYPE_PARAMETER => ("type ", "=();"),
579 lsp::SymbolKind::ENUM_MEMBER => {
580 let prefix = "enum E {";
581 return Some(CodeLabel::new(
582 name.to_string(),
583 0..name.len(),
584 language.highlight_text(
585 &Rope::from_iter([prefix, name, "}"]),
586 prefix.len()..prefix.len() + name.len(),
587 ),
588 ));
589 }
590 _ => return None,
591 };
592
593 let filter_range = prefix.len()..prefix.len() + name.len();
594 let display_range = 0..filter_range.end;
595 Some(CodeLabel::new(
596 format!("{prefix}{name}"),
597 filter_range,
598 language.highlight_text(&Rope::from_iter([prefix, name, suffix]), display_range),
599 ))
600 }
601
602 fn prepare_initialize_params(
603 &self,
604 mut original: InitializeParams,
605 cx: &App,
606 ) -> Result<InitializeParams> {
607 let enable_lsp_tasks = ProjectSettings::get_global(cx)
608 .lsp
609 .get(&SERVER_NAME)
610 .is_some_and(|s| s.enable_lsp_tasks);
611 if enable_lsp_tasks {
612 let experimental = json!({
613 "runnables": {
614 "kinds": [ "cargo", "shell" ],
615 },
616 });
617 if let Some(original_experimental) = &mut original.capabilities.experimental {
618 merge_json_value_into(experimental, original_experimental);
619 } else {
620 original.capabilities.experimental = Some(experimental);
621 }
622 }
623
624 Ok(original)
625 }
626}
627
628impl LspInstaller for RustLspAdapter {
629 type BinaryVersion = GitHubLspBinaryVersion;
630 async fn check_if_user_installed(
631 &self,
632 delegate: &dyn LspAdapterDelegate,
633 _: Option<Toolchain>,
634 _: &AsyncApp,
635 ) -> Option<LanguageServerBinary> {
636 let path = delegate.which("rust-analyzer".as_ref()).await?;
637 let env = delegate.shell_env().await;
638
639 // It is surprisingly common for ~/.cargo/bin/rust-analyzer to be a symlink to
640 // /usr/bin/rust-analyzer that fails when you run it; so we need to test it.
641 log::debug!("found rust-analyzer in PATH. trying to run `rust-analyzer --help`");
642 let result = delegate
643 .try_exec(LanguageServerBinary {
644 path: path.clone(),
645 arguments: vec!["--help".into()],
646 env: Some(env.clone()),
647 })
648 .await;
649 if let Err(err) = result {
650 log::debug!(
651 "failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {}",
652 path,
653 err
654 );
655 return None;
656 }
657
658 Some(LanguageServerBinary {
659 path,
660 env: Some(env),
661 arguments: vec![],
662 })
663 }
664
665 async fn fetch_latest_server_version(
666 &self,
667 delegate: &dyn LspAdapterDelegate,
668 pre_release: bool,
669 _: &mut AsyncApp,
670 ) -> Result<GitHubLspBinaryVersion> {
671 let release = latest_github_release(
672 "rust-lang/rust-analyzer",
673 true,
674 pre_release,
675 delegate.http_client(),
676 )
677 .await?;
678 let asset_name = Self::build_asset_name().await;
679 let asset = release
680 .assets
681 .into_iter()
682 .find(|asset| asset.name == asset_name)
683 .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
684 Ok(GitHubLspBinaryVersion {
685 name: release.tag_name,
686 url: asset.browser_download_url,
687 digest: asset.digest,
688 })
689 }
690
691 async fn fetch_server_binary(
692 &self,
693 version: GitHubLspBinaryVersion,
694 container_dir: PathBuf,
695 delegate: &dyn LspAdapterDelegate,
696 ) -> Result<LanguageServerBinary> {
697 let GitHubLspBinaryVersion {
698 name,
699 url,
700 digest: expected_digest,
701 } = version;
702 let destination_path = container_dir.join(format!("rust-analyzer-{name}"));
703 let server_path = match Self::GITHUB_ASSET_KIND {
704 AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place.
705 AssetKind::Zip => destination_path.clone().join("rust-analyzer.exe"), // zip contains a .exe
706 };
707
708 let binary = LanguageServerBinary {
709 path: server_path.clone(),
710 env: None,
711 arguments: Default::default(),
712 };
713
714 let metadata_path = destination_path.with_extension("metadata");
715 let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
716 .await
717 .ok();
718 if let Some(metadata) = metadata {
719 let validity_check = async || {
720 delegate
721 .try_exec(LanguageServerBinary {
722 path: server_path.clone(),
723 arguments: vec!["--version".into()],
724 env: None,
725 })
726 .await
727 .inspect_err(|err| {
728 log::warn!("Unable to run {server_path:?} asset, redownloading: {err:#}",)
729 })
730 };
731 if let (Some(actual_digest), Some(expected_digest)) =
732 (&metadata.digest, &expected_digest)
733 {
734 if actual_digest == expected_digest {
735 if validity_check().await.is_ok() {
736 return Ok(binary);
737 }
738 } else {
739 log::info!(
740 "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
741 );
742 }
743 } else if validity_check().await.is_ok() {
744 return Ok(binary);
745 }
746 }
747
748 download_server_binary(
749 &*delegate.http_client(),
750 &url,
751 expected_digest.as_deref(),
752 &destination_path,
753 Self::GITHUB_ASSET_KIND,
754 )
755 .await?;
756 make_file_executable(&server_path).await?;
757 remove_matching(&container_dir, |path| path != destination_path).await;
758 GithubBinaryMetadata::write_to_file(
759 &GithubBinaryMetadata {
760 metadata_version: 1,
761 digest: expected_digest,
762 },
763 &metadata_path,
764 )
765 .await?;
766
767 Ok(LanguageServerBinary {
768 path: server_path,
769 env: None,
770 arguments: Default::default(),
771 })
772 }
773
774 async fn cached_server_binary(
775 &self,
776 container_dir: PathBuf,
777 _: &dyn LspAdapterDelegate,
778 ) -> Option<LanguageServerBinary> {
779 get_cached_server_binary(container_dir).await
780 }
781}
782
783pub(crate) struct RustContextProvider;
784
785const RUST_PACKAGE_TASK_VARIABLE: VariableName =
786 VariableName::Custom(Cow::Borrowed("RUST_PACKAGE"));
787
788/// The bin name corresponding to the current file in Cargo.toml
789const RUST_BIN_NAME_TASK_VARIABLE: VariableName =
790 VariableName::Custom(Cow::Borrowed("RUST_BIN_NAME"));
791
792/// The bin kind (bin/example) corresponding to the current file in Cargo.toml
793const RUST_BIN_KIND_TASK_VARIABLE: VariableName =
794 VariableName::Custom(Cow::Borrowed("RUST_BIN_KIND"));
795
796/// The flag to list required features for executing a bin, if any
797const RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE: VariableName =
798 VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES_FLAG"));
799
800/// The list of required features for executing a bin, if any
801const RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE: VariableName =
802 VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES"));
803
804const RUST_TEST_FRAGMENT_TASK_VARIABLE: VariableName =
805 VariableName::Custom(Cow::Borrowed("RUST_TEST_FRAGMENT"));
806
807const RUST_DOC_TEST_NAME_TASK_VARIABLE: VariableName =
808 VariableName::Custom(Cow::Borrowed("RUST_DOC_TEST_NAME"));
809
810const RUST_TEST_NAME_TASK_VARIABLE: VariableName =
811 VariableName::Custom(Cow::Borrowed("RUST_TEST_NAME"));
812
813const RUST_MANIFEST_DIRNAME_TASK_VARIABLE: VariableName =
814 VariableName::Custom(Cow::Borrowed("RUST_MANIFEST_DIRNAME"));
815
816impl ContextProvider for RustContextProvider {
817 fn build_context(
818 &self,
819 task_variables: &TaskVariables,
820 location: ContextLocation<'_>,
821 project_env: Option<HashMap<String, String>>,
822 _: Arc<dyn LanguageToolchainStore>,
823 cx: &mut gpui::App,
824 ) -> Task<Result<TaskVariables>> {
825 let local_abs_path = location
826 .file_location
827 .buffer
828 .read(cx)
829 .file()
830 .and_then(|file| Some(file.as_local()?.abs_path(cx)));
831
832 let mut variables = TaskVariables::default();
833
834 if let (Some(path), Some(stem)) = (&local_abs_path, task_variables.get(&VariableName::Stem))
835 {
836 let fragment = test_fragment(&variables, path, stem);
837 variables.insert(RUST_TEST_FRAGMENT_TASK_VARIABLE, fragment);
838 };
839 if let Some(test_name) =
840 task_variables.get(&VariableName::Custom(Cow::Borrowed("_test_name")))
841 {
842 variables.insert(RUST_TEST_NAME_TASK_VARIABLE, test_name.into());
843 }
844 if let Some(doc_test_name) =
845 task_variables.get(&VariableName::Custom(Cow::Borrowed("_doc_test_name")))
846 {
847 variables.insert(RUST_DOC_TEST_NAME_TASK_VARIABLE, doc_test_name.into());
848 }
849 cx.background_spawn(async move {
850 if let Some(path) = local_abs_path
851 .as_deref()
852 .and_then(|local_abs_path| local_abs_path.parent())
853 && let Some(package_name) =
854 human_readable_package_name(path, project_env.as_ref()).await
855 {
856 variables.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name);
857 }
858 if let Some(path) = local_abs_path.as_ref()
859 && let Some((target, manifest_path)) =
860 target_info_from_abs_path(path, project_env.as_ref()).await
861 {
862 if let Some(target) = target {
863 variables.extend(TaskVariables::from_iter([
864 (RUST_PACKAGE_TASK_VARIABLE.clone(), target.package_name),
865 (RUST_BIN_NAME_TASK_VARIABLE.clone(), target.target_name),
866 (
867 RUST_BIN_KIND_TASK_VARIABLE.clone(),
868 target.target_kind.to_string(),
869 ),
870 ]));
871 if target.required_features.is_empty() {
872 variables.insert(RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE, "".into());
873 variables.insert(RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE, "".into());
874 } else {
875 variables.insert(
876 RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.clone(),
877 "--features".to_string(),
878 );
879 variables.insert(
880 RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.clone(),
881 target.required_features.join(","),
882 );
883 }
884 }
885 variables.extend(TaskVariables::from_iter([(
886 RUST_MANIFEST_DIRNAME_TASK_VARIABLE.clone(),
887 manifest_path.to_string_lossy().into_owned(),
888 )]));
889 }
890 Ok(variables)
891 })
892 }
893
894 fn associated_tasks(
895 &self,
896 file: Option<Arc<dyn language::File>>,
897 cx: &App,
898 ) -> Task<Option<TaskTemplates>> {
899 const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
900 const CUSTOM_TARGET_DIR: &str = "RUST_TARGET_DIR";
901
902 let language_sets = language_settings(Some("Rust".into()), file.as_ref(), cx);
903 let package_to_run = language_sets
904 .tasks
905 .variables
906 .get(DEFAULT_RUN_NAME_STR)
907 .cloned();
908 let custom_target_dir = language_sets
909 .tasks
910 .variables
911 .get(CUSTOM_TARGET_DIR)
912 .cloned();
913 let run_task_args = if let Some(package_to_run) = package_to_run {
914 vec!["run".into(), "-p".into(), package_to_run]
915 } else {
916 vec!["run".into()]
917 };
918 let mut task_templates = vec![
919 TaskTemplate {
920 label: format!(
921 "Check (package: {})",
922 RUST_PACKAGE_TASK_VARIABLE.template_value(),
923 ),
924 command: "cargo".into(),
925 args: vec![
926 "check".into(),
927 "-p".into(),
928 RUST_PACKAGE_TASK_VARIABLE.template_value(),
929 ],
930 cwd: Some("$ZED_DIRNAME".to_owned()),
931 ..TaskTemplate::default()
932 },
933 TaskTemplate {
934 label: "Check all targets (workspace)".into(),
935 command: "cargo".into(),
936 args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
937 cwd: Some("$ZED_DIRNAME".to_owned()),
938 ..TaskTemplate::default()
939 },
940 TaskTemplate {
941 label: format!(
942 "Test '{}' (package: {})",
943 RUST_TEST_NAME_TASK_VARIABLE.template_value(),
944 RUST_PACKAGE_TASK_VARIABLE.template_value(),
945 ),
946 command: "cargo".into(),
947 args: vec![
948 "test".into(),
949 "-p".into(),
950 RUST_PACKAGE_TASK_VARIABLE.template_value(),
951 "--".into(),
952 "--nocapture".into(),
953 "--include-ignored".into(),
954 RUST_TEST_NAME_TASK_VARIABLE.template_value(),
955 ],
956 tags: vec!["rust-test".to_owned()],
957 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
958 ..TaskTemplate::default()
959 },
960 TaskTemplate {
961 label: format!(
962 "Doc test '{}' (package: {})",
963 RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
964 RUST_PACKAGE_TASK_VARIABLE.template_value(),
965 ),
966 command: "cargo".into(),
967 args: vec![
968 "test".into(),
969 "--doc".into(),
970 "-p".into(),
971 RUST_PACKAGE_TASK_VARIABLE.template_value(),
972 "--".into(),
973 "--nocapture".into(),
974 "--include-ignored".into(),
975 RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
976 ],
977 tags: vec!["rust-doc-test".to_owned()],
978 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
979 ..TaskTemplate::default()
980 },
981 TaskTemplate {
982 label: format!(
983 "Test mod '{}' (package: {})",
984 VariableName::Stem.template_value(),
985 RUST_PACKAGE_TASK_VARIABLE.template_value(),
986 ),
987 command: "cargo".into(),
988 args: vec![
989 "test".into(),
990 "-p".into(),
991 RUST_PACKAGE_TASK_VARIABLE.template_value(),
992 "--".into(),
993 RUST_TEST_FRAGMENT_TASK_VARIABLE.template_value(),
994 ],
995 tags: vec!["rust-mod-test".to_owned()],
996 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
997 ..TaskTemplate::default()
998 },
999 TaskTemplate {
1000 label: format!(
1001 "Run {} {} (package: {})",
1002 RUST_BIN_KIND_TASK_VARIABLE.template_value(),
1003 RUST_BIN_NAME_TASK_VARIABLE.template_value(),
1004 RUST_PACKAGE_TASK_VARIABLE.template_value(),
1005 ),
1006 command: "cargo".into(),
1007 args: vec![
1008 "run".into(),
1009 "-p".into(),
1010 RUST_PACKAGE_TASK_VARIABLE.template_value(),
1011 format!("--{}", RUST_BIN_KIND_TASK_VARIABLE.template_value()),
1012 RUST_BIN_NAME_TASK_VARIABLE.template_value(),
1013 RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.template_value(),
1014 RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.template_value(),
1015 ],
1016 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
1017 tags: vec!["rust-main".to_owned()],
1018 ..TaskTemplate::default()
1019 },
1020 TaskTemplate {
1021 label: format!(
1022 "Test (package: {})",
1023 RUST_PACKAGE_TASK_VARIABLE.template_value()
1024 ),
1025 command: "cargo".into(),
1026 args: vec![
1027 "test".into(),
1028 "-p".into(),
1029 RUST_PACKAGE_TASK_VARIABLE.template_value(),
1030 ],
1031 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
1032 ..TaskTemplate::default()
1033 },
1034 TaskTemplate {
1035 label: "Run".into(),
1036 command: "cargo".into(),
1037 args: run_task_args,
1038 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
1039 ..TaskTemplate::default()
1040 },
1041 TaskTemplate {
1042 label: "Clean".into(),
1043 command: "cargo".into(),
1044 args: vec!["clean".into()],
1045 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
1046 ..TaskTemplate::default()
1047 },
1048 ];
1049
1050 if let Some(custom_target_dir) = custom_target_dir {
1051 task_templates = task_templates
1052 .into_iter()
1053 .map(|mut task_template| {
1054 let mut args = task_template.args.split_off(1);
1055 task_template.args.append(&mut vec![
1056 "--target-dir".to_string(),
1057 custom_target_dir.clone(),
1058 ]);
1059 task_template.args.append(&mut args);
1060
1061 task_template
1062 })
1063 .collect();
1064 }
1065
1066 Task::ready(Some(TaskTemplates(task_templates)))
1067 }
1068
1069 fn lsp_task_source(&self) -> Option<LanguageServerName> {
1070 Some(SERVER_NAME)
1071 }
1072}
1073
1074/// Part of the data structure of Cargo metadata
1075#[derive(Debug, serde::Deserialize)]
1076struct CargoMetadata {
1077 packages: Vec<CargoPackage>,
1078}
1079
1080#[derive(Debug, serde::Deserialize)]
1081struct CargoPackage {
1082 id: String,
1083 targets: Vec<CargoTarget>,
1084 manifest_path: Arc<Path>,
1085}
1086
1087#[derive(Debug, serde::Deserialize)]
1088struct CargoTarget {
1089 name: String,
1090 kind: Vec<String>,
1091 src_path: String,
1092 #[serde(rename = "required-features", default)]
1093 required_features: Vec<String>,
1094}
1095
1096#[derive(Debug, PartialEq)]
1097enum TargetKind {
1098 Bin,
1099 Example,
1100}
1101
1102impl Display for TargetKind {
1103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1104 match self {
1105 TargetKind::Bin => write!(f, "bin"),
1106 TargetKind::Example => write!(f, "example"),
1107 }
1108 }
1109}
1110
1111impl TryFrom<&str> for TargetKind {
1112 type Error = ();
1113 fn try_from(value: &str) -> Result<Self, ()> {
1114 match value {
1115 "bin" => Ok(Self::Bin),
1116 "example" => Ok(Self::Example),
1117 _ => Err(()),
1118 }
1119 }
1120}
1121/// Which package and binary target are we in?
1122#[derive(Debug, PartialEq)]
1123struct TargetInfo {
1124 package_name: String,
1125 target_name: String,
1126 target_kind: TargetKind,
1127 required_features: Vec<String>,
1128}
1129
1130async fn target_info_from_abs_path(
1131 abs_path: &Path,
1132 project_env: Option<&HashMap<String, String>>,
1133) -> Option<(Option<TargetInfo>, Arc<Path>)> {
1134 let mut command = util::command::new_smol_command("cargo");
1135 if let Some(envs) = project_env {
1136 command.envs(envs);
1137 }
1138 let output = command
1139 .current_dir(abs_path.parent()?)
1140 .arg("metadata")
1141 .arg("--no-deps")
1142 .arg("--format-version")
1143 .arg("1")
1144 .output()
1145 .await
1146 .log_err()?
1147 .stdout;
1148
1149 let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
1150 target_info_from_metadata(metadata, abs_path)
1151}
1152
1153fn target_info_from_metadata(
1154 metadata: CargoMetadata,
1155 abs_path: &Path,
1156) -> Option<(Option<TargetInfo>, Arc<Path>)> {
1157 let mut manifest_path = None;
1158 for package in metadata.packages {
1159 let Some(manifest_dir_path) = package.manifest_path.parent() else {
1160 continue;
1161 };
1162
1163 let Some(path_from_manifest_dir) = abs_path.strip_prefix(manifest_dir_path).ok() else {
1164 continue;
1165 };
1166 let candidate_path_length = path_from_manifest_dir.components().count();
1167 // Pick the most specific manifest path
1168 if let Some((path, current_length)) = &mut manifest_path {
1169 if candidate_path_length > *current_length {
1170 *path = Arc::from(manifest_dir_path);
1171 *current_length = candidate_path_length;
1172 }
1173 } else {
1174 manifest_path = Some((Arc::from(manifest_dir_path), candidate_path_length));
1175 };
1176
1177 for target in package.targets {
1178 let Some(bin_kind) = target
1179 .kind
1180 .iter()
1181 .find_map(|kind| TargetKind::try_from(kind.as_ref()).ok())
1182 else {
1183 continue;
1184 };
1185 let target_path = PathBuf::from(target.src_path);
1186 if target_path == abs_path {
1187 return manifest_path.map(|(path, _)| {
1188 (
1189 package_name_from_pkgid(&package.id).map(|package_name| TargetInfo {
1190 package_name: package_name.to_owned(),
1191 target_name: target.name,
1192 required_features: target.required_features,
1193 target_kind: bin_kind,
1194 }),
1195 path,
1196 )
1197 });
1198 }
1199 }
1200 }
1201
1202 manifest_path.map(|(path, _)| (None, path))
1203}
1204
1205async fn human_readable_package_name(
1206 package_directory: &Path,
1207 project_env: Option<&HashMap<String, String>>,
1208) -> Option<String> {
1209 let mut command = util::command::new_smol_command("cargo");
1210 if let Some(envs) = project_env {
1211 command.envs(envs);
1212 }
1213 let pkgid = String::from_utf8(
1214 command
1215 .current_dir(package_directory)
1216 .arg("pkgid")
1217 .output()
1218 .await
1219 .log_err()?
1220 .stdout,
1221 )
1222 .ok()?;
1223 Some(package_name_from_pkgid(&pkgid)?.to_owned())
1224}
1225
1226// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
1227// Output example in the root of Zed project:
1228// ```sh
1229// ❯ cargo pkgid zed
1230// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
1231// ```
1232// Another variant, if a project has a custom package name or hyphen in the name:
1233// ```
1234// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
1235// ```
1236//
1237// Extracts the package name from the output according to the spec:
1238// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
1239fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
1240 fn split_off_suffix(input: &str, suffix_start: char) -> &str {
1241 match input.rsplit_once(suffix_start) {
1242 Some((without_suffix, _)) => without_suffix,
1243 None => input,
1244 }
1245 }
1246
1247 let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
1248 let package_name = match version_suffix.rsplit_once('@') {
1249 Some((custom_package_name, _version)) => custom_package_name,
1250 None => {
1251 let host_and_path = split_off_suffix(version_prefix, '?');
1252 let (_, package_name) = host_and_path.rsplit_once('/')?;
1253 package_name
1254 }
1255 };
1256 Some(package_name)
1257}
1258
1259async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
1260 let binary_result = maybe!(async {
1261 let mut last = None;
1262 let mut entries = fs::read_dir(&container_dir)
1263 .await
1264 .with_context(|| format!("listing {container_dir:?}"))?;
1265 while let Some(entry) = entries.next().await {
1266 let path = entry?.path();
1267 if path.extension().is_some_and(|ext| ext == "metadata") {
1268 continue;
1269 }
1270 last = Some(path);
1271 }
1272
1273 let path = match last {
1274 Some(last) => last,
1275 None => return Ok(None),
1276 };
1277 let path = match RustLspAdapter::GITHUB_ASSET_KIND {
1278 AssetKind::TarGz | AssetKind::Gz => path, // Tar and gzip extract in place.
1279 AssetKind::Zip => path.join("rust-analyzer.exe"), // zip contains a .exe
1280 };
1281
1282 anyhow::Ok(Some(LanguageServerBinary {
1283 path,
1284 env: None,
1285 arguments: Vec::new(),
1286 }))
1287 })
1288 .await;
1289
1290 match binary_result {
1291 Ok(Some(binary)) => Some(binary),
1292 Ok(None) => {
1293 log::info!("No cached rust-analyzer binary found");
1294 None
1295 }
1296 Err(e) => {
1297 log::error!("Failed to look up cached rust-analyzer binary: {e:#}");
1298 None
1299 }
1300 }
1301}
1302
1303fn test_fragment(variables: &TaskVariables, path: &Path, stem: &str) -> String {
1304 let fragment = if stem == "lib" {
1305 // This isn't quite right---it runs the tests for the entire library, rather than
1306 // just for the top-level `mod tests`. But we don't really have the means here to
1307 // filter out just that module.
1308 Some("--lib".to_owned())
1309 } else if stem == "mod" {
1310 maybe!({ Some(path.parent()?.file_name()?.to_string_lossy().into_owned()) })
1311 } else if stem == "main" {
1312 if let (Some(bin_name), Some(bin_kind)) = (
1313 variables.get(&RUST_BIN_NAME_TASK_VARIABLE),
1314 variables.get(&RUST_BIN_KIND_TASK_VARIABLE),
1315 ) {
1316 Some(format!("--{bin_kind}={bin_name}"))
1317 } else {
1318 None
1319 }
1320 } else {
1321 Some(stem.to_owned())
1322 };
1323 fragment.unwrap_or_else(|| "--".to_owned())
1324}
1325
1326#[cfg(test)]
1327mod tests {
1328 use std::num::NonZeroU32;
1329
1330 use super::*;
1331 use crate::language;
1332 use gpui::{BorrowAppContext, Hsla, TestAppContext};
1333 use lsp::CompletionItemLabelDetails;
1334 use settings::SettingsStore;
1335 use theme::SyntaxTheme;
1336 use util::path;
1337
1338 #[gpui::test]
1339 async fn test_process_rust_diagnostics() {
1340 let mut params = lsp::PublishDiagnosticsParams {
1341 uri: lsp::Uri::from_file_path(path!("/a")).unwrap(),
1342 version: None,
1343 diagnostics: vec![
1344 // no newlines
1345 lsp::Diagnostic {
1346 message: "use of moved value `a`".to_string(),
1347 ..Default::default()
1348 },
1349 // newline at the end of a code span
1350 lsp::Diagnostic {
1351 message: "consider importing this struct: `use b::c;\n`".to_string(),
1352 ..Default::default()
1353 },
1354 // code span starting right after a newline
1355 lsp::Diagnostic {
1356 message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1357 .to_string(),
1358 ..Default::default()
1359 },
1360 ],
1361 };
1362 RustLspAdapter.process_diagnostics(&mut params, LanguageServerId(0), None);
1363
1364 assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
1365
1366 // remove trailing newline from code span
1367 assert_eq!(
1368 params.diagnostics[1].message,
1369 "consider importing this struct: `use b::c;`"
1370 );
1371
1372 // do not remove newline before the start of code span
1373 assert_eq!(
1374 params.diagnostics[2].message,
1375 "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1376 );
1377 }
1378
1379 #[gpui::test]
1380 async fn test_rust_label_for_completion() {
1381 let adapter = Arc::new(RustLspAdapter);
1382 let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1383 let grammar = language.grammar().unwrap();
1384 let theme = SyntaxTheme::new_test([
1385 ("type", Hsla::default()),
1386 ("keyword", Hsla::default()),
1387 ("function", Hsla::default()),
1388 ("property", Hsla::default()),
1389 ]);
1390
1391 language.set_theme(&theme);
1392
1393 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1394 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1395 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1396 let highlight_field = grammar.highlight_id_for_name("property").unwrap();
1397
1398 assert_eq!(
1399 adapter
1400 .label_for_completion(
1401 &lsp::CompletionItem {
1402 kind: Some(lsp::CompletionItemKind::FUNCTION),
1403 label: "hello(…)".to_string(),
1404 label_details: Some(CompletionItemLabelDetails {
1405 detail: Some("(use crate::foo)".into()),
1406 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string())
1407 }),
1408 ..Default::default()
1409 },
1410 &language
1411 )
1412 .await,
1413 Some(CodeLabel::new(
1414 "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1415 0..5,
1416 vec![
1417 (0..5, highlight_function),
1418 (7..10, highlight_keyword),
1419 (11..17, highlight_type),
1420 (18..19, highlight_type),
1421 (25..28, highlight_type),
1422 (29..30, highlight_type),
1423 ],
1424 ))
1425 );
1426 assert_eq!(
1427 adapter
1428 .label_for_completion(
1429 &lsp::CompletionItem {
1430 kind: Some(lsp::CompletionItemKind::FUNCTION),
1431 label: "hello(…)".to_string(),
1432 label_details: Some(CompletionItemLabelDetails {
1433 detail: Some("(use crate::foo)".into()),
1434 description: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
1435 }),
1436 ..Default::default()
1437 },
1438 &language
1439 )
1440 .await,
1441 Some(CodeLabel::new(
1442 "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1443 0..5,
1444 vec![
1445 (0..5, highlight_function),
1446 (7..10, highlight_keyword),
1447 (11..17, highlight_type),
1448 (18..19, highlight_type),
1449 (25..28, highlight_type),
1450 (29..30, highlight_type),
1451 ],
1452 ))
1453 );
1454 assert_eq!(
1455 adapter
1456 .label_for_completion(
1457 &lsp::CompletionItem {
1458 kind: Some(lsp::CompletionItemKind::FIELD),
1459 label: "len".to_string(),
1460 detail: Some("usize".to_string()),
1461 ..Default::default()
1462 },
1463 &language
1464 )
1465 .await,
1466 Some(CodeLabel::new(
1467 "len: usize".to_string(),
1468 0..3,
1469 vec![(0..3, highlight_field), (5..10, highlight_type),],
1470 ))
1471 );
1472
1473 assert_eq!(
1474 adapter
1475 .label_for_completion(
1476 &lsp::CompletionItem {
1477 kind: Some(lsp::CompletionItemKind::FUNCTION),
1478 label: "hello(…)".to_string(),
1479 label_details: Some(CompletionItemLabelDetails {
1480 detail: Some("(use crate::foo)".to_string()),
1481 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1482 }),
1483
1484 ..Default::default()
1485 },
1486 &language
1487 )
1488 .await,
1489 Some(CodeLabel::new(
1490 "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1491 0..5,
1492 vec![
1493 (0..5, highlight_function),
1494 (7..10, highlight_keyword),
1495 (11..17, highlight_type),
1496 (18..19, highlight_type),
1497 (25..28, highlight_type),
1498 (29..30, highlight_type),
1499 ],
1500 ))
1501 );
1502
1503 assert_eq!(
1504 adapter
1505 .label_for_completion(
1506 &lsp::CompletionItem {
1507 kind: Some(lsp::CompletionItemKind::FUNCTION),
1508 label: "hello".to_string(),
1509 label_details: Some(CompletionItemLabelDetails {
1510 detail: Some("(use crate::foo)".to_string()),
1511 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1512 }),
1513 ..Default::default()
1514 },
1515 &language
1516 )
1517 .await,
1518 Some(CodeLabel::new(
1519 "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1520 0..5,
1521 vec![
1522 (0..5, highlight_function),
1523 (7..10, highlight_keyword),
1524 (11..17, highlight_type),
1525 (18..19, highlight_type),
1526 (25..28, highlight_type),
1527 (29..30, highlight_type),
1528 ],
1529 ))
1530 );
1531
1532 assert_eq!(
1533 adapter
1534 .label_for_completion(
1535 &lsp::CompletionItem {
1536 kind: Some(lsp::CompletionItemKind::METHOD),
1537 label: "await.as_deref_mut()".to_string(),
1538 filter_text: Some("as_deref_mut".to_string()),
1539 label_details: Some(CompletionItemLabelDetails {
1540 detail: None,
1541 description: Some("fn(&mut self) -> IterMut<'_, T>".to_string()),
1542 }),
1543 ..Default::default()
1544 },
1545 &language
1546 )
1547 .await,
1548 Some(CodeLabel::new(
1549 "await.as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1550 6..18,
1551 vec![
1552 (6..18, HighlightId(2)),
1553 (20..23, HighlightId(1)),
1554 (33..40, HighlightId(0)),
1555 (45..46, HighlightId(0))
1556 ],
1557 ))
1558 );
1559
1560 assert_eq!(
1561 adapter
1562 .label_for_completion(
1563 &lsp::CompletionItem {
1564 kind: Some(lsp::CompletionItemKind::METHOD),
1565 label: "as_deref_mut()".to_string(),
1566 filter_text: Some("as_deref_mut".to_string()),
1567 label_details: Some(CompletionItemLabelDetails {
1568 detail: None,
1569 description: Some(
1570 "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string()
1571 ),
1572 }),
1573 ..Default::default()
1574 },
1575 &language
1576 )
1577 .await,
1578 Some(CodeLabel::new(
1579 "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1580 7..19,
1581 vec![
1582 (0..3, HighlightId(1)),
1583 (4..6, HighlightId(1)),
1584 (7..19, HighlightId(2)),
1585 (21..24, HighlightId(1)),
1586 (34..41, HighlightId(0)),
1587 (46..47, HighlightId(0))
1588 ],
1589 ))
1590 );
1591
1592 assert_eq!(
1593 adapter
1594 .label_for_completion(
1595 &lsp::CompletionItem {
1596 kind: Some(lsp::CompletionItemKind::FIELD),
1597 label: "inner_value".to_string(),
1598 filter_text: Some("value".to_string()),
1599 detail: Some("String".to_string()),
1600 ..Default::default()
1601 },
1602 &language,
1603 )
1604 .await,
1605 Some(CodeLabel::new(
1606 "inner_value: String".to_string(),
1607 6..11,
1608 vec![(0..11, HighlightId(3)), (13..19, HighlightId(0))],
1609 ))
1610 );
1611
1612 // Snippet with insert tabstop (empty placeholder)
1613 assert_eq!(
1614 adapter
1615 .label_for_completion(
1616 &lsp::CompletionItem {
1617 kind: Some(lsp::CompletionItemKind::SNIPPET),
1618 label: "println!".to_string(),
1619 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1620 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1621 range: lsp::Range::default(),
1622 new_text: "println!(\"$1\", $2)$0".to_string(),
1623 })),
1624 ..Default::default()
1625 },
1626 &language,
1627 )
1628 .await,
1629 Some(CodeLabel::new(
1630 "println!(\"…\", …)".to_string(),
1631 0..8,
1632 vec![
1633 (10..13, HighlightId::TABSTOP_INSERT_ID),
1634 (16..19, HighlightId::TABSTOP_INSERT_ID),
1635 (0..7, HighlightId(2)),
1636 (7..8, HighlightId(2)),
1637 ],
1638 ))
1639 );
1640
1641 // Snippet with replace tabstop (placeholder with default text)
1642 assert_eq!(
1643 adapter
1644 .label_for_completion(
1645 &lsp::CompletionItem {
1646 kind: Some(lsp::CompletionItemKind::SNIPPET),
1647 label: "vec!".to_string(),
1648 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1649 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1650 range: lsp::Range::default(),
1651 new_text: "vec![${1:elem}]$0".to_string(),
1652 })),
1653 ..Default::default()
1654 },
1655 &language,
1656 )
1657 .await,
1658 Some(CodeLabel::new(
1659 "vec![elem]".to_string(),
1660 0..4,
1661 vec![
1662 (5..9, HighlightId::TABSTOP_REPLACE_ID),
1663 (0..3, HighlightId(2)),
1664 (3..4, HighlightId(2)),
1665 ],
1666 ))
1667 );
1668
1669 // Snippet with tabstop appearing more than once
1670 assert_eq!(
1671 adapter
1672 .label_for_completion(
1673 &lsp::CompletionItem {
1674 kind: Some(lsp::CompletionItemKind::SNIPPET),
1675 label: "if let".to_string(),
1676 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1677 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1678 range: lsp::Range::default(),
1679 new_text: "if let ${1:pat} = $1 {\n $0\n}".to_string(),
1680 })),
1681 ..Default::default()
1682 },
1683 &language,
1684 )
1685 .await,
1686 Some(CodeLabel::new(
1687 "if let pat = … {\n \n}".to_string(),
1688 0..6,
1689 vec![
1690 (7..10, HighlightId::TABSTOP_REPLACE_ID),
1691 (13..16, HighlightId::TABSTOP_INSERT_ID),
1692 (0..2, HighlightId(1)),
1693 (3..6, HighlightId(1)),
1694 ],
1695 ))
1696 );
1697
1698 // Snippet with tabstops not in left-to-right order
1699 assert_eq!(
1700 adapter
1701 .label_for_completion(
1702 &lsp::CompletionItem {
1703 kind: Some(lsp::CompletionItemKind::SNIPPET),
1704 label: "for".to_string(),
1705 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1706 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1707 range: lsp::Range::default(),
1708 new_text: "for ${2:item} in ${1:iter} {\n $0\n}".to_string(),
1709 })),
1710 ..Default::default()
1711 },
1712 &language,
1713 )
1714 .await,
1715 Some(CodeLabel::new(
1716 "for item in iter {\n \n}".to_string(),
1717 0..3,
1718 vec![
1719 (4..8, HighlightId::TABSTOP_REPLACE_ID),
1720 (12..16, HighlightId::TABSTOP_REPLACE_ID),
1721 (0..3, HighlightId(1)),
1722 (9..11, HighlightId(1)),
1723 ],
1724 ))
1725 );
1726
1727 // Postfix completion without actual tabstops (only implicit final $0)
1728 // The label should use completion.label so it can be filtered by "ref"
1729 let ref_completion = adapter
1730 .label_for_completion(
1731 &lsp::CompletionItem {
1732 kind: Some(lsp::CompletionItemKind::SNIPPET),
1733 label: "ref".to_string(),
1734 filter_text: Some("ref".to_string()),
1735 label_details: Some(CompletionItemLabelDetails {
1736 detail: None,
1737 description: Some("&expr".to_string()),
1738 }),
1739 detail: Some("&expr".to_string()),
1740 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1741 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1742 range: lsp::Range::default(),
1743 new_text: "&String::new()".to_string(),
1744 })),
1745 ..Default::default()
1746 },
1747 &language,
1748 )
1749 .await;
1750 assert!(
1751 ref_completion.is_some(),
1752 "ref postfix completion should have a label"
1753 );
1754 let ref_label = ref_completion.unwrap();
1755 let filter_text = &ref_label.text[ref_label.filter_range.clone()];
1756 assert!(
1757 filter_text.contains("ref"),
1758 "filter range text '{filter_text}' should contain 'ref' for filtering to work",
1759 );
1760
1761 // Test for correct range calculation with mixed empty and non-empty tabstops.(See https://github.com/zed-industries/zed/issues/44825)
1762 let res = adapter
1763 .label_for_completion(
1764 &lsp::CompletionItem {
1765 kind: Some(lsp::CompletionItemKind::STRUCT),
1766 label: "Particles".to_string(),
1767 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1768 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1769 range: lsp::Range::default(),
1770 new_text: "Particles { pos_x: $1, pos_y: $2, vel_x: $3, vel_y: $4, acc_x: ${5:()}, acc_y: ${6:()}, mass: $7 }$0".to_string(),
1771 })),
1772 ..Default::default()
1773 },
1774 &language,
1775 )
1776 .await
1777 .unwrap();
1778
1779 assert_eq!(
1780 res,
1781 CodeLabel::new(
1782 "Particles { pos_x: …, pos_y: …, vel_x: …, vel_y: …, acc_x: (), acc_y: (), mass: … }".to_string(),
1783 0..9,
1784 vec![
1785 (19..22, HighlightId::TABSTOP_INSERT_ID),
1786 (31..34, HighlightId::TABSTOP_INSERT_ID),
1787 (43..46, HighlightId::TABSTOP_INSERT_ID),
1788 (55..58, HighlightId::TABSTOP_INSERT_ID),
1789 (67..69, HighlightId::TABSTOP_REPLACE_ID),
1790 (78..80, HighlightId::TABSTOP_REPLACE_ID),
1791 (88..91, HighlightId::TABSTOP_INSERT_ID),
1792 (0..9, highlight_type),
1793 (60..65, highlight_field),
1794 (71..76, highlight_field),
1795 ],
1796 )
1797 );
1798 }
1799
1800 #[gpui::test]
1801 async fn test_rust_label_for_symbol() {
1802 let adapter = Arc::new(RustLspAdapter);
1803 let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1804 let grammar = language.grammar().unwrap();
1805 let theme = SyntaxTheme::new_test([
1806 ("type", Hsla::default()),
1807 ("keyword", Hsla::default()),
1808 ("function", Hsla::default()),
1809 ("property", Hsla::default()),
1810 ]);
1811
1812 language.set_theme(&theme);
1813
1814 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1815 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1816 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1817
1818 assert_eq!(
1819 adapter
1820 .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
1821 .await,
1822 Some(CodeLabel::new(
1823 "fn hello".to_string(),
1824 3..8,
1825 vec![(0..2, highlight_keyword), (3..8, highlight_function)],
1826 ))
1827 );
1828
1829 assert_eq!(
1830 adapter
1831 .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
1832 .await,
1833 Some(CodeLabel::new(
1834 "type World".to_string(),
1835 5..10,
1836 vec![(0..4, highlight_keyword), (5..10, highlight_type)],
1837 ))
1838 );
1839
1840 assert_eq!(
1841 adapter
1842 .label_for_symbol("zed", lsp::SymbolKind::PACKAGE, &language)
1843 .await,
1844 Some(CodeLabel::new(
1845 "extern crate zed".to_string(),
1846 13..16,
1847 vec![(0..6, highlight_keyword), (7..12, highlight_keyword),],
1848 ))
1849 );
1850
1851 assert_eq!(
1852 adapter
1853 .label_for_symbol("Variant", lsp::SymbolKind::ENUM_MEMBER, &language)
1854 .await,
1855 Some(CodeLabel::new(
1856 "Variant".to_string(),
1857 0..7,
1858 vec![(0..7, highlight_type)],
1859 ))
1860 );
1861 }
1862
1863 #[gpui::test]
1864 async fn test_rust_autoindent(cx: &mut TestAppContext) {
1865 // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
1866 cx.update(|cx| {
1867 let test_settings = SettingsStore::test(cx);
1868 cx.set_global(test_settings);
1869 cx.update_global::<SettingsStore, _>(|store, cx| {
1870 store.update_user_settings(cx, |s| {
1871 s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
1872 });
1873 });
1874 });
1875
1876 let language = crate::language("rust", tree_sitter_rust::LANGUAGE.into());
1877
1878 cx.new(|cx| {
1879 let mut buffer = Buffer::local("", cx).with_language(language, cx);
1880
1881 // indent between braces
1882 buffer.set_text("fn a() {}", cx);
1883 let ix = buffer.len() - 1;
1884 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1885 assert_eq!(buffer.text(), "fn a() {\n \n}");
1886
1887 // indent between braces, even after empty lines
1888 buffer.set_text("fn a() {\n\n\n}", cx);
1889 let ix = buffer.len() - 2;
1890 buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1891 assert_eq!(buffer.text(), "fn a() {\n\n\n \n}");
1892
1893 // indent a line that continues a field expression
1894 buffer.set_text("fn a() {\n \n}", cx);
1895 let ix = buffer.len() - 2;
1896 buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
1897 assert_eq!(buffer.text(), "fn a() {\n b\n .c\n}");
1898
1899 // indent further lines that continue the field expression, even after empty lines
1900 let ix = buffer.len() - 2;
1901 buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
1902 assert_eq!(buffer.text(), "fn a() {\n b\n .c\n \n .d\n}");
1903
1904 // dedent the line after the field expression
1905 let ix = buffer.len() - 2;
1906 buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
1907 assert_eq!(
1908 buffer.text(),
1909 "fn a() {\n b\n .c\n \n .d;\n e\n}"
1910 );
1911
1912 // indent inside a struct within a call
1913 buffer.set_text("const a: B = c(D {});", cx);
1914 let ix = buffer.len() - 3;
1915 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1916 assert_eq!(buffer.text(), "const a: B = c(D {\n \n});");
1917
1918 // indent further inside a nested call
1919 let ix = buffer.len() - 4;
1920 buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
1921 assert_eq!(buffer.text(), "const a: B = c(D {\n e: f(\n \n )\n});");
1922
1923 // keep that indent after an empty line
1924 let ix = buffer.len() - 8;
1925 buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1926 assert_eq!(
1927 buffer.text(),
1928 "const a: B = c(D {\n e: f(\n \n \n )\n});"
1929 );
1930
1931 buffer
1932 });
1933 }
1934
1935 #[test]
1936 fn test_package_name_from_pkgid() {
1937 for (input, expected) in [
1938 (
1939 "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
1940 "zed",
1941 ),
1942 (
1943 "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
1944 "my-custom-package",
1945 ),
1946 ] {
1947 assert_eq!(package_name_from_pkgid(input), Some(expected));
1948 }
1949 }
1950
1951 #[test]
1952 fn test_target_info_from_metadata() {
1953 for (input, absolute_path, expected) in [
1954 (
1955 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"}]}]}"#,
1956 "/path/to/zed/src/main.rs",
1957 Some((
1958 Some(TargetInfo {
1959 package_name: "zed".into(),
1960 target_name: "zed".into(),
1961 required_features: Vec::new(),
1962 target_kind: TargetKind::Bin,
1963 }),
1964 Arc::from("/path/to/zed".as_ref()),
1965 )),
1966 ),
1967 (
1968 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"}]}]}"#,
1969 "/path/to/custom-package/src/main.rs",
1970 Some((
1971 Some(TargetInfo {
1972 package_name: "my-custom-package".into(),
1973 target_name: "my-custom-bin".into(),
1974 required_features: Vec::new(),
1975 target_kind: TargetKind::Bin,
1976 }),
1977 Arc::from("/path/to/custom-package".as_ref()),
1978 )),
1979 ),
1980 (
1981 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"}]}"#,
1982 "/path/to/custom-package/src/main.rs",
1983 Some((
1984 Some(TargetInfo {
1985 package_name: "my-custom-package".into(),
1986 target_name: "my-custom-bin".into(),
1987 required_features: Vec::new(),
1988 target_kind: TargetKind::Example,
1989 }),
1990 Arc::from("/path/to/custom-package".as_ref()),
1991 )),
1992 ),
1993 (
1994 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"]}]}]}"#,
1995 "/path/to/custom-package/src/main.rs",
1996 Some((
1997 Some(TargetInfo {
1998 package_name: "my-custom-package".into(),
1999 target_name: "my-custom-bin".into(),
2000 required_features: vec!["foo".to_owned(), "bar".to_owned()],
2001 target_kind: TargetKind::Example,
2002 }),
2003 Arc::from("/path/to/custom-package".as_ref()),
2004 )),
2005 ),
2006 (
2007 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"}]}"#,
2008 "/path/to/custom-package/src/main.rs",
2009 Some((
2010 Some(TargetInfo {
2011 package_name: "my-custom-package".into(),
2012 target_name: "my-custom-bin".into(),
2013 required_features: vec![],
2014 target_kind: TargetKind::Example,
2015 }),
2016 Arc::from("/path/to/custom-package".as_ref()),
2017 )),
2018 ),
2019 (
2020 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"}]}"#,
2021 "/path/to/custom-package/src/main.rs",
2022 Some((None, Arc::from("/path/to/custom-package".as_ref()))),
2023 ),
2024 ] {
2025 let metadata: CargoMetadata = serde_json::from_str(input).context(input).unwrap();
2026
2027 let absolute_path = Path::new(absolute_path);
2028
2029 assert_eq!(target_info_from_metadata(metadata, absolute_path), expected);
2030 }
2031 }
2032
2033 #[test]
2034 fn test_rust_test_fragment() {
2035 #[track_caller]
2036 fn check(
2037 variables: impl IntoIterator<Item = (VariableName, &'static str)>,
2038 path: &str,
2039 expected: &str,
2040 ) {
2041 let path = Path::new(path);
2042 let found = test_fragment(
2043 &TaskVariables::from_iter(variables.into_iter().map(|(k, v)| (k, v.to_owned()))),
2044 path,
2045 path.file_stem().unwrap().to_str().unwrap(),
2046 );
2047 assert_eq!(expected, found);
2048 }
2049
2050 check([], "/project/src/lib.rs", "--lib");
2051 check([], "/project/src/foo/mod.rs", "foo");
2052 check(
2053 [
2054 (RUST_BIN_KIND_TASK_VARIABLE.clone(), "bin"),
2055 (RUST_BIN_NAME_TASK_VARIABLE, "x"),
2056 ],
2057 "/project/src/main.rs",
2058 "--bin=x",
2059 );
2060 check([], "/project/src/main.rs", "--");
2061 }
2062
2063 #[test]
2064 fn test_convert_rust_analyzer_schema() {
2065 let raw_schema = serde_json::json!([
2066 {
2067 "title": "Assist",
2068 "properties": {
2069 "rust-analyzer.assist.emitMustUse": {
2070 "markdownDescription": "Insert #[must_use] when generating `as_` methods for enum variants.",
2071 "default": false,
2072 "type": "boolean"
2073 }
2074 }
2075 },
2076 {
2077 "title": "Assist",
2078 "properties": {
2079 "rust-analyzer.assist.expressionFillDefault": {
2080 "markdownDescription": "Placeholder expression to use for missing expressions in assists.",
2081 "default": "todo",
2082 "type": "string"
2083 }
2084 }
2085 },
2086 {
2087 "title": "Cache Priming",
2088 "properties": {
2089 "rust-analyzer.cachePriming.enable": {
2090 "markdownDescription": "Warm up caches on project load.",
2091 "default": true,
2092 "type": "boolean"
2093 }
2094 }
2095 }
2096 ]);
2097
2098 let converted = RustLspAdapter::convert_rust_analyzer_schema(&raw_schema);
2099
2100 assert_eq!(
2101 converted.get("type").and_then(|v| v.as_str()),
2102 Some("object")
2103 );
2104
2105 let properties = converted
2106 .pointer("/properties")
2107 .expect("should have properties")
2108 .as_object()
2109 .expect("properties should be object");
2110
2111 assert!(properties.contains_key("assist"));
2112 assert!(properties.contains_key("cachePriming"));
2113 assert!(!properties.contains_key("rust-analyzer"));
2114
2115 let assist_props = properties
2116 .get("assist")
2117 .expect("should have assist")
2118 .pointer("/properties")
2119 .expect("assist should have properties")
2120 .as_object()
2121 .expect("assist properties should be object");
2122
2123 assert!(assist_props.contains_key("emitMustUse"));
2124 assert!(assist_props.contains_key("expressionFillDefault"));
2125
2126 let emit_must_use = assist_props
2127 .get("emitMustUse")
2128 .expect("should have emitMustUse");
2129 assert_eq!(
2130 emit_must_use.get("type").and_then(|v| v.as_str()),
2131 Some("boolean")
2132 );
2133 assert_eq!(
2134 emit_must_use.get("default").and_then(|v| v.as_bool()),
2135 Some(false)
2136 );
2137
2138 let cache_priming_props = properties
2139 .get("cachePriming")
2140 .expect("should have cachePriming")
2141 .pointer("/properties")
2142 .expect("cachePriming should have properties")
2143 .as_object()
2144 .expect("cachePriming properties should be object");
2145
2146 assert!(cache_priming_props.contains_key("enable"));
2147 }
2148}