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