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