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