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