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::TYPE_PARAMETER => ("type ", " {}"),
578 _ => return None,
579 };
580
581 let filter_range = prefix.len()..prefix.len() + name.len();
582 let display_range = 0..filter_range.end;
583 Some(CodeLabel::new(
584 format!("{prefix}{name}"),
585 filter_range,
586 language.highlight_text(&Rope::from_iter([prefix, name, suffix]), display_range),
587 ))
588 }
589
590 fn prepare_initialize_params(
591 &self,
592 mut original: InitializeParams,
593 cx: &App,
594 ) -> Result<InitializeParams> {
595 let enable_lsp_tasks = ProjectSettings::get_global(cx)
596 .lsp
597 .get(&SERVER_NAME)
598 .is_some_and(|s| s.enable_lsp_tasks);
599 if enable_lsp_tasks {
600 let experimental = json!({
601 "runnables": {
602 "kinds": [ "cargo", "shell" ],
603 },
604 });
605 if let Some(original_experimental) = &mut original.capabilities.experimental {
606 merge_json_value_into(experimental, original_experimental);
607 } else {
608 original.capabilities.experimental = Some(experimental);
609 }
610 }
611
612 Ok(original)
613 }
614}
615
616impl LspInstaller for RustLspAdapter {
617 type BinaryVersion = GitHubLspBinaryVersion;
618 async fn check_if_user_installed(
619 &self,
620 delegate: &dyn LspAdapterDelegate,
621 _: Option<Toolchain>,
622 _: &AsyncApp,
623 ) -> Option<LanguageServerBinary> {
624 let path = delegate.which("rust-analyzer".as_ref()).await?;
625 let env = delegate.shell_env().await;
626
627 // It is surprisingly common for ~/.cargo/bin/rust-analyzer to be a symlink to
628 // /usr/bin/rust-analyzer that fails when you run it; so we need to test it.
629 log::debug!("found rust-analyzer in PATH. trying to run `rust-analyzer --help`");
630 let result = delegate
631 .try_exec(LanguageServerBinary {
632 path: path.clone(),
633 arguments: vec!["--help".into()],
634 env: Some(env.clone()),
635 })
636 .await;
637 if let Err(err) = result {
638 log::debug!(
639 "failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {}",
640 path,
641 err
642 );
643 return None;
644 }
645
646 Some(LanguageServerBinary {
647 path,
648 env: Some(env),
649 arguments: vec![],
650 })
651 }
652
653 async fn fetch_latest_server_version(
654 &self,
655 delegate: &dyn LspAdapterDelegate,
656 pre_release: bool,
657 _: &mut AsyncApp,
658 ) -> Result<GitHubLspBinaryVersion> {
659 let release = latest_github_release(
660 "rust-lang/rust-analyzer",
661 true,
662 pre_release,
663 delegate.http_client(),
664 )
665 .await?;
666 let asset_name = Self::build_asset_name().await;
667 let asset = release
668 .assets
669 .into_iter()
670 .find(|asset| asset.name == asset_name)
671 .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
672 Ok(GitHubLspBinaryVersion {
673 name: release.tag_name,
674 url: asset.browser_download_url,
675 digest: asset.digest,
676 })
677 }
678
679 async fn fetch_server_binary(
680 &self,
681 version: GitHubLspBinaryVersion,
682 container_dir: PathBuf,
683 delegate: &dyn LspAdapterDelegate,
684 ) -> Result<LanguageServerBinary> {
685 let GitHubLspBinaryVersion {
686 name,
687 url,
688 digest: expected_digest,
689 } = version;
690 let destination_path = container_dir.join(format!("rust-analyzer-{name}"));
691 let server_path = match Self::GITHUB_ASSET_KIND {
692 AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place.
693 AssetKind::Zip => destination_path.clone().join("rust-analyzer.exe"), // zip contains a .exe
694 };
695
696 let binary = LanguageServerBinary {
697 path: server_path.clone(),
698 env: None,
699 arguments: Default::default(),
700 };
701
702 let metadata_path = destination_path.with_extension("metadata");
703 let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
704 .await
705 .ok();
706 if let Some(metadata) = metadata {
707 let validity_check = async || {
708 delegate
709 .try_exec(LanguageServerBinary {
710 path: server_path.clone(),
711 arguments: vec!["--version".into()],
712 env: None,
713 })
714 .await
715 .inspect_err(|err| {
716 log::warn!("Unable to run {server_path:?} asset, redownloading: {err:#}",)
717 })
718 };
719 if let (Some(actual_digest), Some(expected_digest)) =
720 (&metadata.digest, &expected_digest)
721 {
722 if actual_digest == expected_digest {
723 if validity_check().await.is_ok() {
724 return Ok(binary);
725 }
726 } else {
727 log::info!(
728 "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
729 );
730 }
731 } else if validity_check().await.is_ok() {
732 return Ok(binary);
733 }
734 }
735
736 download_server_binary(
737 &*delegate.http_client(),
738 &url,
739 expected_digest.as_deref(),
740 &destination_path,
741 Self::GITHUB_ASSET_KIND,
742 )
743 .await?;
744 make_file_executable(&server_path).await?;
745 remove_matching(&container_dir, |path| path != destination_path).await;
746 GithubBinaryMetadata::write_to_file(
747 &GithubBinaryMetadata {
748 metadata_version: 1,
749 digest: expected_digest,
750 },
751 &metadata_path,
752 )
753 .await?;
754
755 Ok(LanguageServerBinary {
756 path: server_path,
757 env: None,
758 arguments: Default::default(),
759 })
760 }
761
762 async fn cached_server_binary(
763 &self,
764 container_dir: PathBuf,
765 _: &dyn LspAdapterDelegate,
766 ) -> Option<LanguageServerBinary> {
767 get_cached_server_binary(container_dir).await
768 }
769}
770
771pub(crate) struct RustContextProvider;
772
773const RUST_PACKAGE_TASK_VARIABLE: VariableName =
774 VariableName::Custom(Cow::Borrowed("RUST_PACKAGE"));
775
776/// The bin name corresponding to the current file in Cargo.toml
777const RUST_BIN_NAME_TASK_VARIABLE: VariableName =
778 VariableName::Custom(Cow::Borrowed("RUST_BIN_NAME"));
779
780/// The bin kind (bin/example) corresponding to the current file in Cargo.toml
781const RUST_BIN_KIND_TASK_VARIABLE: VariableName =
782 VariableName::Custom(Cow::Borrowed("RUST_BIN_KIND"));
783
784/// The flag to list required features for executing a bin, if any
785const RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE: VariableName =
786 VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES_FLAG"));
787
788/// The list of required features for executing a bin, if any
789const RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE: VariableName =
790 VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES"));
791
792const RUST_TEST_FRAGMENT_TASK_VARIABLE: VariableName =
793 VariableName::Custom(Cow::Borrowed("RUST_TEST_FRAGMENT"));
794
795const RUST_DOC_TEST_NAME_TASK_VARIABLE: VariableName =
796 VariableName::Custom(Cow::Borrowed("RUST_DOC_TEST_NAME"));
797
798const RUST_TEST_NAME_TASK_VARIABLE: VariableName =
799 VariableName::Custom(Cow::Borrowed("RUST_TEST_NAME"));
800
801const RUST_MANIFEST_DIRNAME_TASK_VARIABLE: VariableName =
802 VariableName::Custom(Cow::Borrowed("RUST_MANIFEST_DIRNAME"));
803
804impl ContextProvider for RustContextProvider {
805 fn build_context(
806 &self,
807 task_variables: &TaskVariables,
808 location: ContextLocation<'_>,
809 project_env: Option<HashMap<String, String>>,
810 _: Arc<dyn LanguageToolchainStore>,
811 cx: &mut gpui::App,
812 ) -> Task<Result<TaskVariables>> {
813 let local_abs_path = location
814 .file_location
815 .buffer
816 .read(cx)
817 .file()
818 .and_then(|file| Some(file.as_local()?.abs_path(cx)));
819
820 let mut variables = TaskVariables::default();
821
822 if let (Some(path), Some(stem)) = (&local_abs_path, task_variables.get(&VariableName::Stem))
823 {
824 let fragment = test_fragment(&variables, path, stem);
825 variables.insert(RUST_TEST_FRAGMENT_TASK_VARIABLE, fragment);
826 };
827 if let Some(test_name) =
828 task_variables.get(&VariableName::Custom(Cow::Borrowed("_test_name")))
829 {
830 variables.insert(RUST_TEST_NAME_TASK_VARIABLE, test_name.into());
831 }
832 if let Some(doc_test_name) =
833 task_variables.get(&VariableName::Custom(Cow::Borrowed("_doc_test_name")))
834 {
835 variables.insert(RUST_DOC_TEST_NAME_TASK_VARIABLE, doc_test_name.into());
836 }
837 cx.background_spawn(async move {
838 if let Some(path) = local_abs_path
839 .as_deref()
840 .and_then(|local_abs_path| local_abs_path.parent())
841 && let Some(package_name) =
842 human_readable_package_name(path, project_env.as_ref()).await
843 {
844 variables.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name);
845 }
846 if let Some(path) = local_abs_path.as_ref()
847 && let Some((target, manifest_path)) =
848 target_info_from_abs_path(path, project_env.as_ref()).await
849 {
850 if let Some(target) = target {
851 variables.extend(TaskVariables::from_iter([
852 (RUST_PACKAGE_TASK_VARIABLE.clone(), target.package_name),
853 (RUST_BIN_NAME_TASK_VARIABLE.clone(), target.target_name),
854 (
855 RUST_BIN_KIND_TASK_VARIABLE.clone(),
856 target.target_kind.to_string(),
857 ),
858 ]));
859 if target.required_features.is_empty() {
860 variables.insert(RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE, "".into());
861 variables.insert(RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE, "".into());
862 } else {
863 variables.insert(
864 RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.clone(),
865 "--features".to_string(),
866 );
867 variables.insert(
868 RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.clone(),
869 target.required_features.join(","),
870 );
871 }
872 }
873 variables.extend(TaskVariables::from_iter([(
874 RUST_MANIFEST_DIRNAME_TASK_VARIABLE.clone(),
875 manifest_path.to_string_lossy().into_owned(),
876 )]));
877 }
878 Ok(variables)
879 })
880 }
881
882 fn associated_tasks(
883 &self,
884 file: Option<Arc<dyn language::File>>,
885 cx: &App,
886 ) -> Task<Option<TaskTemplates>> {
887 const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
888 const CUSTOM_TARGET_DIR: &str = "RUST_TARGET_DIR";
889
890 let language_sets = language_settings(Some("Rust".into()), file.as_ref(), cx);
891 let package_to_run = language_sets
892 .tasks
893 .variables
894 .get(DEFAULT_RUN_NAME_STR)
895 .cloned();
896 let custom_target_dir = language_sets
897 .tasks
898 .variables
899 .get(CUSTOM_TARGET_DIR)
900 .cloned();
901 let run_task_args = if let Some(package_to_run) = package_to_run {
902 vec!["run".into(), "-p".into(), package_to_run]
903 } else {
904 vec!["run".into()]
905 };
906 let mut task_templates = vec![
907 TaskTemplate {
908 label: format!(
909 "Check (package: {})",
910 RUST_PACKAGE_TASK_VARIABLE.template_value(),
911 ),
912 command: "cargo".into(),
913 args: vec![
914 "check".into(),
915 "-p".into(),
916 RUST_PACKAGE_TASK_VARIABLE.template_value(),
917 ],
918 cwd: Some("$ZED_DIRNAME".to_owned()),
919 ..TaskTemplate::default()
920 },
921 TaskTemplate {
922 label: "Check all targets (workspace)".into(),
923 command: "cargo".into(),
924 args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
925 cwd: Some("$ZED_DIRNAME".to_owned()),
926 ..TaskTemplate::default()
927 },
928 TaskTemplate {
929 label: format!(
930 "Test '{}' (package: {})",
931 RUST_TEST_NAME_TASK_VARIABLE.template_value(),
932 RUST_PACKAGE_TASK_VARIABLE.template_value(),
933 ),
934 command: "cargo".into(),
935 args: vec![
936 "test".into(),
937 "-p".into(),
938 RUST_PACKAGE_TASK_VARIABLE.template_value(),
939 "--".into(),
940 "--nocapture".into(),
941 "--include-ignored".into(),
942 RUST_TEST_NAME_TASK_VARIABLE.template_value(),
943 ],
944 tags: vec!["rust-test".to_owned()],
945 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
946 ..TaskTemplate::default()
947 },
948 TaskTemplate {
949 label: format!(
950 "Doc test '{}' (package: {})",
951 RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
952 RUST_PACKAGE_TASK_VARIABLE.template_value(),
953 ),
954 command: "cargo".into(),
955 args: vec![
956 "test".into(),
957 "--doc".into(),
958 "-p".into(),
959 RUST_PACKAGE_TASK_VARIABLE.template_value(),
960 "--".into(),
961 "--nocapture".into(),
962 "--include-ignored".into(),
963 RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
964 ],
965 tags: vec!["rust-doc-test".to_owned()],
966 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
967 ..TaskTemplate::default()
968 },
969 TaskTemplate {
970 label: format!(
971 "Test mod '{}' (package: {})",
972 VariableName::Stem.template_value(),
973 RUST_PACKAGE_TASK_VARIABLE.template_value(),
974 ),
975 command: "cargo".into(),
976 args: vec![
977 "test".into(),
978 "-p".into(),
979 RUST_PACKAGE_TASK_VARIABLE.template_value(),
980 "--".into(),
981 RUST_TEST_FRAGMENT_TASK_VARIABLE.template_value(),
982 ],
983 tags: vec!["rust-mod-test".to_owned()],
984 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
985 ..TaskTemplate::default()
986 },
987 TaskTemplate {
988 label: format!(
989 "Run {} {} (package: {})",
990 RUST_BIN_KIND_TASK_VARIABLE.template_value(),
991 RUST_BIN_NAME_TASK_VARIABLE.template_value(),
992 RUST_PACKAGE_TASK_VARIABLE.template_value(),
993 ),
994 command: "cargo".into(),
995 args: vec![
996 "run".into(),
997 "-p".into(),
998 RUST_PACKAGE_TASK_VARIABLE.template_value(),
999 format!("--{}", RUST_BIN_KIND_TASK_VARIABLE.template_value()),
1000 RUST_BIN_NAME_TASK_VARIABLE.template_value(),
1001 RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.template_value(),
1002 RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.template_value(),
1003 ],
1004 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
1005 tags: vec!["rust-main".to_owned()],
1006 ..TaskTemplate::default()
1007 },
1008 TaskTemplate {
1009 label: format!(
1010 "Test (package: {})",
1011 RUST_PACKAGE_TASK_VARIABLE.template_value()
1012 ),
1013 command: "cargo".into(),
1014 args: vec![
1015 "test".into(),
1016 "-p".into(),
1017 RUST_PACKAGE_TASK_VARIABLE.template_value(),
1018 ],
1019 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
1020 ..TaskTemplate::default()
1021 },
1022 TaskTemplate {
1023 label: "Run".into(),
1024 command: "cargo".into(),
1025 args: run_task_args,
1026 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
1027 ..TaskTemplate::default()
1028 },
1029 TaskTemplate {
1030 label: "Clean".into(),
1031 command: "cargo".into(),
1032 args: vec!["clean".into()],
1033 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
1034 ..TaskTemplate::default()
1035 },
1036 ];
1037
1038 if let Some(custom_target_dir) = custom_target_dir {
1039 task_templates = task_templates
1040 .into_iter()
1041 .map(|mut task_template| {
1042 let mut args = task_template.args.split_off(1);
1043 task_template.args.append(&mut vec![
1044 "--target-dir".to_string(),
1045 custom_target_dir.clone(),
1046 ]);
1047 task_template.args.append(&mut args);
1048
1049 task_template
1050 })
1051 .collect();
1052 }
1053
1054 Task::ready(Some(TaskTemplates(task_templates)))
1055 }
1056
1057 fn lsp_task_source(&self) -> Option<LanguageServerName> {
1058 Some(SERVER_NAME)
1059 }
1060}
1061
1062/// Part of the data structure of Cargo metadata
1063#[derive(Debug, serde::Deserialize)]
1064struct CargoMetadata {
1065 packages: Vec<CargoPackage>,
1066}
1067
1068#[derive(Debug, serde::Deserialize)]
1069struct CargoPackage {
1070 id: String,
1071 targets: Vec<CargoTarget>,
1072 manifest_path: Arc<Path>,
1073}
1074
1075#[derive(Debug, serde::Deserialize)]
1076struct CargoTarget {
1077 name: String,
1078 kind: Vec<String>,
1079 src_path: String,
1080 #[serde(rename = "required-features", default)]
1081 required_features: Vec<String>,
1082}
1083
1084#[derive(Debug, PartialEq)]
1085enum TargetKind {
1086 Bin,
1087 Example,
1088}
1089
1090impl Display for TargetKind {
1091 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1092 match self {
1093 TargetKind::Bin => write!(f, "bin"),
1094 TargetKind::Example => write!(f, "example"),
1095 }
1096 }
1097}
1098
1099impl TryFrom<&str> for TargetKind {
1100 type Error = ();
1101 fn try_from(value: &str) -> Result<Self, ()> {
1102 match value {
1103 "bin" => Ok(Self::Bin),
1104 "example" => Ok(Self::Example),
1105 _ => Err(()),
1106 }
1107 }
1108}
1109/// Which package and binary target are we in?
1110#[derive(Debug, PartialEq)]
1111struct TargetInfo {
1112 package_name: String,
1113 target_name: String,
1114 target_kind: TargetKind,
1115 required_features: Vec<String>,
1116}
1117
1118async fn target_info_from_abs_path(
1119 abs_path: &Path,
1120 project_env: Option<&HashMap<String, String>>,
1121) -> Option<(Option<TargetInfo>, Arc<Path>)> {
1122 let mut command = util::command::new_smol_command("cargo");
1123 if let Some(envs) = project_env {
1124 command.envs(envs);
1125 }
1126 let output = command
1127 .current_dir(abs_path.parent()?)
1128 .arg("metadata")
1129 .arg("--no-deps")
1130 .arg("--format-version")
1131 .arg("1")
1132 .output()
1133 .await
1134 .log_err()?
1135 .stdout;
1136
1137 let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
1138 target_info_from_metadata(metadata, abs_path)
1139}
1140
1141fn target_info_from_metadata(
1142 metadata: CargoMetadata,
1143 abs_path: &Path,
1144) -> Option<(Option<TargetInfo>, Arc<Path>)> {
1145 let mut manifest_path = None;
1146 for package in metadata.packages {
1147 let Some(manifest_dir_path) = package.manifest_path.parent() else {
1148 continue;
1149 };
1150
1151 let Some(path_from_manifest_dir) = abs_path.strip_prefix(manifest_dir_path).ok() else {
1152 continue;
1153 };
1154 let candidate_path_length = path_from_manifest_dir.components().count();
1155 // Pick the most specific manifest path
1156 if let Some((path, current_length)) = &mut manifest_path {
1157 if candidate_path_length > *current_length {
1158 *path = Arc::from(manifest_dir_path);
1159 *current_length = candidate_path_length;
1160 }
1161 } else {
1162 manifest_path = Some((Arc::from(manifest_dir_path), candidate_path_length));
1163 };
1164
1165 for target in package.targets {
1166 let Some(bin_kind) = target
1167 .kind
1168 .iter()
1169 .find_map(|kind| TargetKind::try_from(kind.as_ref()).ok())
1170 else {
1171 continue;
1172 };
1173 let target_path = PathBuf::from(target.src_path);
1174 if target_path == abs_path {
1175 return manifest_path.map(|(path, _)| {
1176 (
1177 package_name_from_pkgid(&package.id).map(|package_name| TargetInfo {
1178 package_name: package_name.to_owned(),
1179 target_name: target.name,
1180 required_features: target.required_features,
1181 target_kind: bin_kind,
1182 }),
1183 path,
1184 )
1185 });
1186 }
1187 }
1188 }
1189
1190 manifest_path.map(|(path, _)| (None, path))
1191}
1192
1193async fn human_readable_package_name(
1194 package_directory: &Path,
1195 project_env: Option<&HashMap<String, String>>,
1196) -> Option<String> {
1197 let mut command = util::command::new_smol_command("cargo");
1198 if let Some(envs) = project_env {
1199 command.envs(envs);
1200 }
1201 let pkgid = String::from_utf8(
1202 command
1203 .current_dir(package_directory)
1204 .arg("pkgid")
1205 .output()
1206 .await
1207 .log_err()?
1208 .stdout,
1209 )
1210 .ok()?;
1211 Some(package_name_from_pkgid(&pkgid)?.to_owned())
1212}
1213
1214// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
1215// Output example in the root of Zed project:
1216// ```sh
1217// ❯ cargo pkgid zed
1218// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
1219// ```
1220// Another variant, if a project has a custom package name or hyphen in the name:
1221// ```
1222// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
1223// ```
1224//
1225// Extracts the package name from the output according to the spec:
1226// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
1227fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
1228 fn split_off_suffix(input: &str, suffix_start: char) -> &str {
1229 match input.rsplit_once(suffix_start) {
1230 Some((without_suffix, _)) => without_suffix,
1231 None => input,
1232 }
1233 }
1234
1235 let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
1236 let package_name = match version_suffix.rsplit_once('@') {
1237 Some((custom_package_name, _version)) => custom_package_name,
1238 None => {
1239 let host_and_path = split_off_suffix(version_prefix, '?');
1240 let (_, package_name) = host_and_path.rsplit_once('/')?;
1241 package_name
1242 }
1243 };
1244 Some(package_name)
1245}
1246
1247async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
1248 let binary_result = maybe!(async {
1249 let mut last = None;
1250 let mut entries = fs::read_dir(&container_dir)
1251 .await
1252 .with_context(|| format!("listing {container_dir:?}"))?;
1253 while let Some(entry) = entries.next().await {
1254 let path = entry?.path();
1255 if path.extension().is_some_and(|ext| ext == "metadata") {
1256 continue;
1257 }
1258 last = Some(path);
1259 }
1260
1261 let path = match last {
1262 Some(last) => last,
1263 None => return Ok(None),
1264 };
1265 let path = match RustLspAdapter::GITHUB_ASSET_KIND {
1266 AssetKind::TarGz | AssetKind::Gz => path, // Tar and gzip extract in place.
1267 AssetKind::Zip => path.join("rust-analyzer.exe"), // zip contains a .exe
1268 };
1269
1270 anyhow::Ok(Some(LanguageServerBinary {
1271 path,
1272 env: None,
1273 arguments: Vec::new(),
1274 }))
1275 })
1276 .await;
1277
1278 match binary_result {
1279 Ok(Some(binary)) => Some(binary),
1280 Ok(None) => {
1281 log::info!("No cached rust-analyzer binary found");
1282 None
1283 }
1284 Err(e) => {
1285 log::error!("Failed to look up cached rust-analyzer binary: {e:#}");
1286 None
1287 }
1288 }
1289}
1290
1291fn test_fragment(variables: &TaskVariables, path: &Path, stem: &str) -> String {
1292 let fragment = if stem == "lib" {
1293 // This isn't quite right---it runs the tests for the entire library, rather than
1294 // just for the top-level `mod tests`. But we don't really have the means here to
1295 // filter out just that module.
1296 Some("--lib".to_owned())
1297 } else if stem == "mod" {
1298 maybe!({ Some(path.parent()?.file_name()?.to_string_lossy().into_owned()) })
1299 } else if stem == "main" {
1300 if let (Some(bin_name), Some(bin_kind)) = (
1301 variables.get(&RUST_BIN_NAME_TASK_VARIABLE),
1302 variables.get(&RUST_BIN_KIND_TASK_VARIABLE),
1303 ) {
1304 Some(format!("--{bin_kind}={bin_name}"))
1305 } else {
1306 None
1307 }
1308 } else {
1309 Some(stem.to_owned())
1310 };
1311 fragment.unwrap_or_else(|| "--".to_owned())
1312}
1313
1314#[cfg(test)]
1315mod tests {
1316 use std::num::NonZeroU32;
1317
1318 use super::*;
1319 use crate::language;
1320 use gpui::{BorrowAppContext, Hsla, TestAppContext};
1321 use lsp::CompletionItemLabelDetails;
1322 use settings::SettingsStore;
1323 use theme::SyntaxTheme;
1324 use util::path;
1325
1326 #[gpui::test]
1327 async fn test_process_rust_diagnostics() {
1328 let mut params = lsp::PublishDiagnosticsParams {
1329 uri: lsp::Uri::from_file_path(path!("/a")).unwrap(),
1330 version: None,
1331 diagnostics: vec![
1332 // no newlines
1333 lsp::Diagnostic {
1334 message: "use of moved value `a`".to_string(),
1335 ..Default::default()
1336 },
1337 // newline at the end of a code span
1338 lsp::Diagnostic {
1339 message: "consider importing this struct: `use b::c;\n`".to_string(),
1340 ..Default::default()
1341 },
1342 // code span starting right after a newline
1343 lsp::Diagnostic {
1344 message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1345 .to_string(),
1346 ..Default::default()
1347 },
1348 ],
1349 };
1350 RustLspAdapter.process_diagnostics(&mut params, LanguageServerId(0), None);
1351
1352 assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
1353
1354 // remove trailing newline from code span
1355 assert_eq!(
1356 params.diagnostics[1].message,
1357 "consider importing this struct: `use b::c;`"
1358 );
1359
1360 // do not remove newline before the start of code span
1361 assert_eq!(
1362 params.diagnostics[2].message,
1363 "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1364 );
1365 }
1366
1367 #[gpui::test]
1368 async fn test_rust_label_for_completion() {
1369 let adapter = Arc::new(RustLspAdapter);
1370 let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1371 let grammar = language.grammar().unwrap();
1372 let theme = SyntaxTheme::new_test([
1373 ("type", Hsla::default()),
1374 ("keyword", Hsla::default()),
1375 ("function", Hsla::default()),
1376 ("property", Hsla::default()),
1377 ]);
1378
1379 language.set_theme(&theme);
1380
1381 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1382 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1383 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1384 let highlight_field = grammar.highlight_id_for_name("property").unwrap();
1385
1386 assert_eq!(
1387 adapter
1388 .label_for_completion(
1389 &lsp::CompletionItem {
1390 kind: Some(lsp::CompletionItemKind::FUNCTION),
1391 label: "hello(…)".to_string(),
1392 label_details: Some(CompletionItemLabelDetails {
1393 detail: Some("(use crate::foo)".into()),
1394 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string())
1395 }),
1396 ..Default::default()
1397 },
1398 &language
1399 )
1400 .await,
1401 Some(CodeLabel::new(
1402 "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1403 0..5,
1404 vec![
1405 (0..5, highlight_function),
1406 (7..10, highlight_keyword),
1407 (11..17, highlight_type),
1408 (18..19, highlight_type),
1409 (25..28, highlight_type),
1410 (29..30, highlight_type),
1411 ],
1412 ))
1413 );
1414 assert_eq!(
1415 adapter
1416 .label_for_completion(
1417 &lsp::CompletionItem {
1418 kind: Some(lsp::CompletionItemKind::FUNCTION),
1419 label: "hello(…)".to_string(),
1420 label_details: Some(CompletionItemLabelDetails {
1421 detail: Some("(use crate::foo)".into()),
1422 description: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
1423 }),
1424 ..Default::default()
1425 },
1426 &language
1427 )
1428 .await,
1429 Some(CodeLabel::new(
1430 "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1431 0..5,
1432 vec![
1433 (0..5, highlight_function),
1434 (7..10, highlight_keyword),
1435 (11..17, highlight_type),
1436 (18..19, highlight_type),
1437 (25..28, highlight_type),
1438 (29..30, highlight_type),
1439 ],
1440 ))
1441 );
1442 assert_eq!(
1443 adapter
1444 .label_for_completion(
1445 &lsp::CompletionItem {
1446 kind: Some(lsp::CompletionItemKind::FIELD),
1447 label: "len".to_string(),
1448 detail: Some("usize".to_string()),
1449 ..Default::default()
1450 },
1451 &language
1452 )
1453 .await,
1454 Some(CodeLabel::new(
1455 "len: usize".to_string(),
1456 0..3,
1457 vec![(0..3, highlight_field), (5..10, highlight_type),],
1458 ))
1459 );
1460
1461 assert_eq!(
1462 adapter
1463 .label_for_completion(
1464 &lsp::CompletionItem {
1465 kind: Some(lsp::CompletionItemKind::FUNCTION),
1466 label: "hello(…)".to_string(),
1467 label_details: Some(CompletionItemLabelDetails {
1468 detail: Some("(use crate::foo)".to_string()),
1469 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1470 }),
1471
1472 ..Default::default()
1473 },
1474 &language
1475 )
1476 .await,
1477 Some(CodeLabel::new(
1478 "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1479 0..5,
1480 vec![
1481 (0..5, highlight_function),
1482 (7..10, highlight_keyword),
1483 (11..17, highlight_type),
1484 (18..19, highlight_type),
1485 (25..28, highlight_type),
1486 (29..30, highlight_type),
1487 ],
1488 ))
1489 );
1490
1491 assert_eq!(
1492 adapter
1493 .label_for_completion(
1494 &lsp::CompletionItem {
1495 kind: Some(lsp::CompletionItemKind::FUNCTION),
1496 label: "hello".to_string(),
1497 label_details: Some(CompletionItemLabelDetails {
1498 detail: Some("(use crate::foo)".to_string()),
1499 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1500 }),
1501 ..Default::default()
1502 },
1503 &language
1504 )
1505 .await,
1506 Some(CodeLabel::new(
1507 "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1508 0..5,
1509 vec![
1510 (0..5, highlight_function),
1511 (7..10, highlight_keyword),
1512 (11..17, highlight_type),
1513 (18..19, highlight_type),
1514 (25..28, highlight_type),
1515 (29..30, highlight_type),
1516 ],
1517 ))
1518 );
1519
1520 assert_eq!(
1521 adapter
1522 .label_for_completion(
1523 &lsp::CompletionItem {
1524 kind: Some(lsp::CompletionItemKind::METHOD),
1525 label: "await.as_deref_mut()".to_string(),
1526 filter_text: Some("as_deref_mut".to_string()),
1527 label_details: Some(CompletionItemLabelDetails {
1528 detail: None,
1529 description: Some("fn(&mut self) -> IterMut<'_, T>".to_string()),
1530 }),
1531 ..Default::default()
1532 },
1533 &language
1534 )
1535 .await,
1536 Some(CodeLabel::new(
1537 "await.as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1538 6..18,
1539 vec![
1540 (6..18, HighlightId(2)),
1541 (20..23, HighlightId(1)),
1542 (33..40, HighlightId(0)),
1543 (45..46, HighlightId(0))
1544 ],
1545 ))
1546 );
1547
1548 assert_eq!(
1549 adapter
1550 .label_for_completion(
1551 &lsp::CompletionItem {
1552 kind: Some(lsp::CompletionItemKind::METHOD),
1553 label: "as_deref_mut()".to_string(),
1554 filter_text: Some("as_deref_mut".to_string()),
1555 label_details: Some(CompletionItemLabelDetails {
1556 detail: None,
1557 description: Some(
1558 "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string()
1559 ),
1560 }),
1561 ..Default::default()
1562 },
1563 &language
1564 )
1565 .await,
1566 Some(CodeLabel::new(
1567 "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1568 7..19,
1569 vec![
1570 (0..3, HighlightId(1)),
1571 (4..6, HighlightId(1)),
1572 (7..19, HighlightId(2)),
1573 (21..24, HighlightId(1)),
1574 (34..41, HighlightId(0)),
1575 (46..47, HighlightId(0))
1576 ],
1577 ))
1578 );
1579
1580 assert_eq!(
1581 adapter
1582 .label_for_completion(
1583 &lsp::CompletionItem {
1584 kind: Some(lsp::CompletionItemKind::FIELD),
1585 label: "inner_value".to_string(),
1586 filter_text: Some("value".to_string()),
1587 detail: Some("String".to_string()),
1588 ..Default::default()
1589 },
1590 &language,
1591 )
1592 .await,
1593 Some(CodeLabel::new(
1594 "inner_value: String".to_string(),
1595 6..11,
1596 vec![(0..11, HighlightId(3)), (13..19, HighlightId(0))],
1597 ))
1598 );
1599
1600 // Snippet with insert tabstop (empty placeholder)
1601 assert_eq!(
1602 adapter
1603 .label_for_completion(
1604 &lsp::CompletionItem {
1605 kind: Some(lsp::CompletionItemKind::SNIPPET),
1606 label: "println!".to_string(),
1607 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1608 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1609 range: lsp::Range::default(),
1610 new_text: "println!(\"$1\", $2)$0".to_string(),
1611 })),
1612 ..Default::default()
1613 },
1614 &language,
1615 )
1616 .await,
1617 Some(CodeLabel::new(
1618 "println!(\"…\", …)".to_string(),
1619 0..8,
1620 vec![
1621 (10..13, HighlightId::TABSTOP_INSERT_ID),
1622 (16..19, HighlightId::TABSTOP_INSERT_ID),
1623 (0..7, HighlightId(2)),
1624 (7..8, HighlightId(2)),
1625 ],
1626 ))
1627 );
1628
1629 // Snippet with replace tabstop (placeholder with default text)
1630 assert_eq!(
1631 adapter
1632 .label_for_completion(
1633 &lsp::CompletionItem {
1634 kind: Some(lsp::CompletionItemKind::SNIPPET),
1635 label: "vec!".to_string(),
1636 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1637 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1638 range: lsp::Range::default(),
1639 new_text: "vec![${1:elem}]$0".to_string(),
1640 })),
1641 ..Default::default()
1642 },
1643 &language,
1644 )
1645 .await,
1646 Some(CodeLabel::new(
1647 "vec![elem]".to_string(),
1648 0..4,
1649 vec![
1650 (5..9, HighlightId::TABSTOP_REPLACE_ID),
1651 (0..3, HighlightId(2)),
1652 (3..4, HighlightId(2)),
1653 ],
1654 ))
1655 );
1656
1657 // Snippet with tabstop appearing more than once
1658 assert_eq!(
1659 adapter
1660 .label_for_completion(
1661 &lsp::CompletionItem {
1662 kind: Some(lsp::CompletionItemKind::SNIPPET),
1663 label: "if let".to_string(),
1664 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1665 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1666 range: lsp::Range::default(),
1667 new_text: "if let ${1:pat} = $1 {\n $0\n}".to_string(),
1668 })),
1669 ..Default::default()
1670 },
1671 &language,
1672 )
1673 .await,
1674 Some(CodeLabel::new(
1675 "if let pat = … {\n \n}".to_string(),
1676 0..6,
1677 vec![
1678 (7..10, HighlightId::TABSTOP_REPLACE_ID),
1679 (13..16, HighlightId::TABSTOP_INSERT_ID),
1680 (0..2, HighlightId(1)),
1681 (3..6, HighlightId(1)),
1682 ],
1683 ))
1684 );
1685
1686 // Snippet with tabstops not in left-to-right order
1687 assert_eq!(
1688 adapter
1689 .label_for_completion(
1690 &lsp::CompletionItem {
1691 kind: Some(lsp::CompletionItemKind::SNIPPET),
1692 label: "for".to_string(),
1693 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1694 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1695 range: lsp::Range::default(),
1696 new_text: "for ${2:item} in ${1:iter} {\n $0\n}".to_string(),
1697 })),
1698 ..Default::default()
1699 },
1700 &language,
1701 )
1702 .await,
1703 Some(CodeLabel::new(
1704 "for item in iter {\n \n}".to_string(),
1705 0..3,
1706 vec![
1707 (4..8, HighlightId::TABSTOP_REPLACE_ID),
1708 (12..16, HighlightId::TABSTOP_REPLACE_ID),
1709 (0..3, HighlightId(1)),
1710 (9..11, HighlightId(1)),
1711 ],
1712 ))
1713 );
1714
1715 // Postfix completion without actual tabstops (only implicit final $0)
1716 // The label should use completion.label so it can be filtered by "ref"
1717 let ref_completion = adapter
1718 .label_for_completion(
1719 &lsp::CompletionItem {
1720 kind: Some(lsp::CompletionItemKind::SNIPPET),
1721 label: "ref".to_string(),
1722 filter_text: Some("ref".to_string()),
1723 label_details: Some(CompletionItemLabelDetails {
1724 detail: None,
1725 description: Some("&expr".to_string()),
1726 }),
1727 detail: Some("&expr".to_string()),
1728 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1729 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1730 range: lsp::Range::default(),
1731 new_text: "&String::new()".to_string(),
1732 })),
1733 ..Default::default()
1734 },
1735 &language,
1736 )
1737 .await;
1738 assert!(
1739 ref_completion.is_some(),
1740 "ref postfix completion should have a label"
1741 );
1742 let ref_label = ref_completion.unwrap();
1743 let filter_text = &ref_label.text[ref_label.filter_range.clone()];
1744 assert!(
1745 filter_text.contains("ref"),
1746 "filter range text '{filter_text}' should contain 'ref' for filtering to work",
1747 );
1748
1749 // Test for correct range calculation with mixed empty and non-empty tabstops.(See https://github.com/zed-industries/zed/issues/44825)
1750 let res = adapter
1751 .label_for_completion(
1752 &lsp::CompletionItem {
1753 kind: Some(lsp::CompletionItemKind::STRUCT),
1754 label: "Particles".to_string(),
1755 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1756 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1757 range: lsp::Range::default(),
1758 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(),
1759 })),
1760 ..Default::default()
1761 },
1762 &language,
1763 )
1764 .await
1765 .unwrap();
1766
1767 assert_eq!(
1768 res,
1769 CodeLabel::new(
1770 "Particles { pos_x: …, pos_y: …, vel_x: …, vel_y: …, acc_x: (), acc_y: (), mass: … }".to_string(),
1771 0..9,
1772 vec![
1773 (19..22, HighlightId::TABSTOP_INSERT_ID),
1774 (31..34, HighlightId::TABSTOP_INSERT_ID),
1775 (43..46, HighlightId::TABSTOP_INSERT_ID),
1776 (55..58, HighlightId::TABSTOP_INSERT_ID),
1777 (67..69, HighlightId::TABSTOP_REPLACE_ID),
1778 (78..80, HighlightId::TABSTOP_REPLACE_ID),
1779 (88..91, HighlightId::TABSTOP_INSERT_ID),
1780 (0..9, highlight_type),
1781 (60..65, highlight_field),
1782 (71..76, highlight_field),
1783 ],
1784 )
1785 );
1786 }
1787
1788 #[gpui::test]
1789 async fn test_rust_label_for_symbol() {
1790 let adapter = Arc::new(RustLspAdapter);
1791 let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1792 let grammar = language.grammar().unwrap();
1793 let theme = SyntaxTheme::new_test([
1794 ("type", Hsla::default()),
1795 ("keyword", Hsla::default()),
1796 ("function", Hsla::default()),
1797 ("property", Hsla::default()),
1798 ]);
1799
1800 language.set_theme(&theme);
1801
1802 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1803 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1804 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1805
1806 assert_eq!(
1807 adapter
1808 .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
1809 .await,
1810 Some(CodeLabel::new(
1811 "fn hello".to_string(),
1812 3..8,
1813 vec![(0..2, highlight_keyword), (3..8, highlight_function)],
1814 ))
1815 );
1816
1817 assert_eq!(
1818 adapter
1819 .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
1820 .await,
1821 Some(CodeLabel::new(
1822 "type World".to_string(),
1823 5..10,
1824 vec![(0..4, highlight_keyword), (5..10, highlight_type)],
1825 ))
1826 );
1827 }
1828
1829 #[gpui::test]
1830 async fn test_rust_autoindent(cx: &mut TestAppContext) {
1831 // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
1832 cx.update(|cx| {
1833 let test_settings = SettingsStore::test(cx);
1834 cx.set_global(test_settings);
1835 cx.update_global::<SettingsStore, _>(|store, cx| {
1836 store.update_user_settings(cx, |s| {
1837 s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
1838 });
1839 });
1840 });
1841
1842 let language = crate::language("rust", tree_sitter_rust::LANGUAGE.into());
1843
1844 cx.new(|cx| {
1845 let mut buffer = Buffer::local("", cx).with_language(language, cx);
1846
1847 // indent between braces
1848 buffer.set_text("fn a() {}", cx);
1849 let ix = buffer.len() - 1;
1850 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1851 assert_eq!(buffer.text(), "fn a() {\n \n}");
1852
1853 // indent between braces, even after empty lines
1854 buffer.set_text("fn a() {\n\n\n}", cx);
1855 let ix = buffer.len() - 2;
1856 buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1857 assert_eq!(buffer.text(), "fn a() {\n\n\n \n}");
1858
1859 // indent a line that continues a field expression
1860 buffer.set_text("fn a() {\n \n}", cx);
1861 let ix = buffer.len() - 2;
1862 buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
1863 assert_eq!(buffer.text(), "fn a() {\n b\n .c\n}");
1864
1865 // indent further lines that continue the field expression, even after empty lines
1866 let ix = buffer.len() - 2;
1867 buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
1868 assert_eq!(buffer.text(), "fn a() {\n b\n .c\n \n .d\n}");
1869
1870 // dedent the line after the field expression
1871 let ix = buffer.len() - 2;
1872 buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
1873 assert_eq!(
1874 buffer.text(),
1875 "fn a() {\n b\n .c\n \n .d;\n e\n}"
1876 );
1877
1878 // indent inside a struct within a call
1879 buffer.set_text("const a: B = c(D {});", cx);
1880 let ix = buffer.len() - 3;
1881 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1882 assert_eq!(buffer.text(), "const a: B = c(D {\n \n});");
1883
1884 // indent further inside a nested call
1885 let ix = buffer.len() - 4;
1886 buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
1887 assert_eq!(buffer.text(), "const a: B = c(D {\n e: f(\n \n )\n});");
1888
1889 // keep that indent after an empty line
1890 let ix = buffer.len() - 8;
1891 buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1892 assert_eq!(
1893 buffer.text(),
1894 "const a: B = c(D {\n e: f(\n \n \n )\n});"
1895 );
1896
1897 buffer
1898 });
1899 }
1900
1901 #[test]
1902 fn test_package_name_from_pkgid() {
1903 for (input, expected) in [
1904 (
1905 "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
1906 "zed",
1907 ),
1908 (
1909 "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
1910 "my-custom-package",
1911 ),
1912 ] {
1913 assert_eq!(package_name_from_pkgid(input), Some(expected));
1914 }
1915 }
1916
1917 #[test]
1918 fn test_target_info_from_metadata() {
1919 for (input, absolute_path, expected) in [
1920 (
1921 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"}]}]}"#,
1922 "/path/to/zed/src/main.rs",
1923 Some((
1924 Some(TargetInfo {
1925 package_name: "zed".into(),
1926 target_name: "zed".into(),
1927 required_features: Vec::new(),
1928 target_kind: TargetKind::Bin,
1929 }),
1930 Arc::from("/path/to/zed".as_ref()),
1931 )),
1932 ),
1933 (
1934 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"}]}]}"#,
1935 "/path/to/custom-package/src/main.rs",
1936 Some((
1937 Some(TargetInfo {
1938 package_name: "my-custom-package".into(),
1939 target_name: "my-custom-bin".into(),
1940 required_features: Vec::new(),
1941 target_kind: TargetKind::Bin,
1942 }),
1943 Arc::from("/path/to/custom-package".as_ref()),
1944 )),
1945 ),
1946 (
1947 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"}]}"#,
1948 "/path/to/custom-package/src/main.rs",
1949 Some((
1950 Some(TargetInfo {
1951 package_name: "my-custom-package".into(),
1952 target_name: "my-custom-bin".into(),
1953 required_features: Vec::new(),
1954 target_kind: TargetKind::Example,
1955 }),
1956 Arc::from("/path/to/custom-package".as_ref()),
1957 )),
1958 ),
1959 (
1960 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"]}]}]}"#,
1961 "/path/to/custom-package/src/main.rs",
1962 Some((
1963 Some(TargetInfo {
1964 package_name: "my-custom-package".into(),
1965 target_name: "my-custom-bin".into(),
1966 required_features: vec!["foo".to_owned(), "bar".to_owned()],
1967 target_kind: TargetKind::Example,
1968 }),
1969 Arc::from("/path/to/custom-package".as_ref()),
1970 )),
1971 ),
1972 (
1973 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"}]}"#,
1974 "/path/to/custom-package/src/main.rs",
1975 Some((
1976 Some(TargetInfo {
1977 package_name: "my-custom-package".into(),
1978 target_name: "my-custom-bin".into(),
1979 required_features: vec![],
1980 target_kind: TargetKind::Example,
1981 }),
1982 Arc::from("/path/to/custom-package".as_ref()),
1983 )),
1984 ),
1985 (
1986 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"}]}"#,
1987 "/path/to/custom-package/src/main.rs",
1988 Some((None, Arc::from("/path/to/custom-package".as_ref()))),
1989 ),
1990 ] {
1991 let metadata: CargoMetadata = serde_json::from_str(input).context(input).unwrap();
1992
1993 let absolute_path = Path::new(absolute_path);
1994
1995 assert_eq!(target_info_from_metadata(metadata, absolute_path), expected);
1996 }
1997 }
1998
1999 #[test]
2000 fn test_rust_test_fragment() {
2001 #[track_caller]
2002 fn check(
2003 variables: impl IntoIterator<Item = (VariableName, &'static str)>,
2004 path: &str,
2005 expected: &str,
2006 ) {
2007 let path = Path::new(path);
2008 let found = test_fragment(
2009 &TaskVariables::from_iter(variables.into_iter().map(|(k, v)| (k, v.to_owned()))),
2010 path,
2011 path.file_stem().unwrap().to_str().unwrap(),
2012 );
2013 assert_eq!(expected, found);
2014 }
2015
2016 check([], "/project/src/lib.rs", "--lib");
2017 check([], "/project/src/foo/mod.rs", "foo");
2018 check(
2019 [
2020 (RUST_BIN_KIND_TASK_VARIABLE.clone(), "bin"),
2021 (RUST_BIN_NAME_TASK_VARIABLE, "x"),
2022 ],
2023 "/project/src/main.rs",
2024 "--bin=x",
2025 );
2026 check([], "/project/src/main.rs", "--");
2027 }
2028
2029 #[test]
2030 fn test_convert_rust_analyzer_schema() {
2031 let raw_schema = serde_json::json!([
2032 {
2033 "title": "Assist",
2034 "properties": {
2035 "rust-analyzer.assist.emitMustUse": {
2036 "markdownDescription": "Insert #[must_use] when generating `as_` methods for enum variants.",
2037 "default": false,
2038 "type": "boolean"
2039 }
2040 }
2041 },
2042 {
2043 "title": "Assist",
2044 "properties": {
2045 "rust-analyzer.assist.expressionFillDefault": {
2046 "markdownDescription": "Placeholder expression to use for missing expressions in assists.",
2047 "default": "todo",
2048 "type": "string"
2049 }
2050 }
2051 },
2052 {
2053 "title": "Cache Priming",
2054 "properties": {
2055 "rust-analyzer.cachePriming.enable": {
2056 "markdownDescription": "Warm up caches on project load.",
2057 "default": true,
2058 "type": "boolean"
2059 }
2060 }
2061 }
2062 ]);
2063
2064 let converted = RustLspAdapter::convert_rust_analyzer_schema(&raw_schema);
2065
2066 assert_eq!(
2067 converted.get("type").and_then(|v| v.as_str()),
2068 Some("object")
2069 );
2070
2071 let properties = converted
2072 .pointer("/properties")
2073 .expect("should have properties")
2074 .as_object()
2075 .expect("properties should be object");
2076
2077 assert!(properties.contains_key("assist"));
2078 assert!(properties.contains_key("cachePriming"));
2079 assert!(!properties.contains_key("rust-analyzer"));
2080
2081 let assist_props = properties
2082 .get("assist")
2083 .expect("should have assist")
2084 .pointer("/properties")
2085 .expect("assist should have properties")
2086 .as_object()
2087 .expect("assist properties should be object");
2088
2089 assert!(assist_props.contains_key("emitMustUse"));
2090 assert!(assist_props.contains_key("expressionFillDefault"));
2091
2092 let emit_must_use = assist_props
2093 .get("emitMustUse")
2094 .expect("should have emitMustUse");
2095 assert_eq!(
2096 emit_must_use.get("type").and_then(|v| v.as_str()),
2097 Some("boolean")
2098 );
2099 assert_eq!(
2100 emit_must_use.get("default").and_then(|v| v.as_bool()),
2101 Some(false)
2102 );
2103
2104 let cache_priming_props = properties
2105 .get("cachePriming")
2106 .expect("should have cachePriming")
2107 .pointer("/properties")
2108 .expect("cachePriming should have properties")
2109 .as_object()
2110 .expect("cachePriming properties should be object");
2111
2112 assert!(cache_priming_props.contains_key("enable"));
2113 }
2114}