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::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 buffer: Option<Entity<Buffer>>,
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 = LanguageName::new_static("Rust");
909 let settings = LanguageSettings::resolve(buffer.map(|b| b.read(cx)), Some(&language), cx);
910 let package_to_run = settings.tasks.variables.get(DEFAULT_RUN_NAME_STR).cloned();
911 let custom_target_dir = settings.tasks.variables.get(CUSTOM_TARGET_DIR).cloned();
912 let run_task_args = if let Some(package_to_run) = package_to_run {
913 vec!["run".into(), "-p".into(), package_to_run]
914 } else {
915 vec!["run".into()]
916 };
917 let mut task_templates = vec![
918 TaskTemplate {
919 label: format!(
920 "Check (package: {})",
921 RUST_PACKAGE_TASK_VARIABLE.template_value(),
922 ),
923 command: "cargo".into(),
924 args: vec![
925 "check".into(),
926 "-p".into(),
927 RUST_PACKAGE_TASK_VARIABLE.template_value(),
928 ],
929 cwd: Some("$ZED_DIRNAME".to_owned()),
930 ..TaskTemplate::default()
931 },
932 TaskTemplate {
933 label: "Check all targets (workspace)".into(),
934 command: "cargo".into(),
935 args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
936 cwd: Some("$ZED_DIRNAME".to_owned()),
937 ..TaskTemplate::default()
938 },
939 TaskTemplate {
940 label: format!(
941 "Test '{}' (package: {})",
942 RUST_TEST_NAME_TASK_VARIABLE.template_value(),
943 RUST_PACKAGE_TASK_VARIABLE.template_value(),
944 ),
945 command: "cargo".into(),
946 args: vec![
947 "test".into(),
948 "-p".into(),
949 RUST_PACKAGE_TASK_VARIABLE.template_value(),
950 "--".into(),
951 "--nocapture".into(),
952 "--include-ignored".into(),
953 RUST_TEST_NAME_TASK_VARIABLE.template_value(),
954 ],
955 tags: vec!["rust-test".to_owned()],
956 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
957 ..TaskTemplate::default()
958 },
959 TaskTemplate {
960 label: format!(
961 "Doc test '{}' (package: {})",
962 RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
963 RUST_PACKAGE_TASK_VARIABLE.template_value(),
964 ),
965 command: "cargo".into(),
966 args: vec![
967 "test".into(),
968 "--doc".into(),
969 "-p".into(),
970 RUST_PACKAGE_TASK_VARIABLE.template_value(),
971 "--".into(),
972 "--nocapture".into(),
973 "--include-ignored".into(),
974 RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
975 ],
976 tags: vec!["rust-doc-test".to_owned()],
977 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
978 ..TaskTemplate::default()
979 },
980 TaskTemplate {
981 label: format!(
982 "Test mod '{}' (package: {})",
983 VariableName::Stem.template_value(),
984 RUST_PACKAGE_TASK_VARIABLE.template_value(),
985 ),
986 command: "cargo".into(),
987 args: vec![
988 "test".into(),
989 "-p".into(),
990 RUST_PACKAGE_TASK_VARIABLE.template_value(),
991 "--".into(),
992 RUST_TEST_FRAGMENT_TASK_VARIABLE.template_value(),
993 ],
994 tags: vec!["rust-mod-test".to_owned()],
995 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
996 ..TaskTemplate::default()
997 },
998 TaskTemplate {
999 label: format!(
1000 "Run {} {} (package: {})",
1001 RUST_BIN_KIND_TASK_VARIABLE.template_value(),
1002 RUST_BIN_NAME_TASK_VARIABLE.template_value(),
1003 RUST_PACKAGE_TASK_VARIABLE.template_value(),
1004 ),
1005 command: "cargo".into(),
1006 args: vec![
1007 "run".into(),
1008 "-p".into(),
1009 RUST_PACKAGE_TASK_VARIABLE.template_value(),
1010 format!("--{}", RUST_BIN_KIND_TASK_VARIABLE.template_value()),
1011 RUST_BIN_NAME_TASK_VARIABLE.template_value(),
1012 RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.template_value(),
1013 RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.template_value(),
1014 ],
1015 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
1016 tags: vec!["rust-main".to_owned()],
1017 ..TaskTemplate::default()
1018 },
1019 TaskTemplate {
1020 label: format!(
1021 "Test (package: {})",
1022 RUST_PACKAGE_TASK_VARIABLE.template_value()
1023 ),
1024 command: "cargo".into(),
1025 args: vec![
1026 "test".into(),
1027 "-p".into(),
1028 RUST_PACKAGE_TASK_VARIABLE.template_value(),
1029 ],
1030 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
1031 ..TaskTemplate::default()
1032 },
1033 TaskTemplate {
1034 label: "Run".into(),
1035 command: "cargo".into(),
1036 args: run_task_args,
1037 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
1038 ..TaskTemplate::default()
1039 },
1040 TaskTemplate {
1041 label: "Clean".into(),
1042 command: "cargo".into(),
1043 args: vec!["clean".into()],
1044 cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
1045 ..TaskTemplate::default()
1046 },
1047 ];
1048
1049 if let Some(custom_target_dir) = custom_target_dir {
1050 task_templates = task_templates
1051 .into_iter()
1052 .map(|mut task_template| {
1053 let mut args = task_template.args.split_off(1);
1054 task_template.args.append(&mut vec![
1055 "--target-dir".to_string(),
1056 custom_target_dir.clone(),
1057 ]);
1058 task_template.args.append(&mut args);
1059
1060 task_template
1061 })
1062 .collect();
1063 }
1064
1065 Task::ready(Some(TaskTemplates(task_templates)))
1066 }
1067
1068 fn lsp_task_source(&self) -> Option<LanguageServerName> {
1069 Some(SERVER_NAME)
1070 }
1071}
1072
1073/// Part of the data structure of Cargo metadata
1074#[derive(Debug, serde::Deserialize)]
1075struct CargoMetadata {
1076 packages: Vec<CargoPackage>,
1077}
1078
1079#[derive(Debug, serde::Deserialize)]
1080struct CargoPackage {
1081 id: String,
1082 targets: Vec<CargoTarget>,
1083 manifest_path: Arc<Path>,
1084}
1085
1086#[derive(Debug, serde::Deserialize)]
1087struct CargoTarget {
1088 name: String,
1089 kind: Vec<String>,
1090 src_path: String,
1091 #[serde(rename = "required-features", default)]
1092 required_features: Vec<String>,
1093}
1094
1095#[derive(Debug, PartialEq)]
1096enum TargetKind {
1097 Bin,
1098 Example,
1099}
1100
1101impl Display for TargetKind {
1102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1103 match self {
1104 TargetKind::Bin => write!(f, "bin"),
1105 TargetKind::Example => write!(f, "example"),
1106 }
1107 }
1108}
1109
1110impl TryFrom<&str> for TargetKind {
1111 type Error = ();
1112 fn try_from(value: &str) -> Result<Self, ()> {
1113 match value {
1114 "bin" => Ok(Self::Bin),
1115 "example" => Ok(Self::Example),
1116 _ => Err(()),
1117 }
1118 }
1119}
1120/// Which package and binary target are we in?
1121#[derive(Debug, PartialEq)]
1122struct TargetInfo {
1123 package_name: String,
1124 target_name: String,
1125 target_kind: TargetKind,
1126 required_features: Vec<String>,
1127}
1128
1129async fn target_info_from_abs_path(
1130 abs_path: &Path,
1131 project_env: Option<&HashMap<String, String>>,
1132) -> Option<(Option<TargetInfo>, Arc<Path>)> {
1133 let mut command = util::command::new_command("cargo");
1134 if let Some(envs) = project_env {
1135 command.envs(envs);
1136 }
1137 let output = command
1138 .current_dir(abs_path.parent()?)
1139 .arg("metadata")
1140 .arg("--no-deps")
1141 .arg("--format-version")
1142 .arg("1")
1143 .output()
1144 .await
1145 .log_err()?
1146 .stdout;
1147
1148 let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
1149 target_info_from_metadata(metadata, abs_path)
1150}
1151
1152fn target_info_from_metadata(
1153 metadata: CargoMetadata,
1154 abs_path: &Path,
1155) -> Option<(Option<TargetInfo>, Arc<Path>)> {
1156 let mut manifest_path = None;
1157 for package in metadata.packages {
1158 let Some(manifest_dir_path) = package.manifest_path.parent() else {
1159 continue;
1160 };
1161
1162 let Some(path_from_manifest_dir) = abs_path.strip_prefix(manifest_dir_path).ok() else {
1163 continue;
1164 };
1165 let candidate_path_length = path_from_manifest_dir.components().count();
1166 // Pick the most specific manifest path
1167 if let Some((path, current_length)) = &mut manifest_path {
1168 if candidate_path_length > *current_length {
1169 *path = Arc::from(manifest_dir_path);
1170 *current_length = candidate_path_length;
1171 }
1172 } else {
1173 manifest_path = Some((Arc::from(manifest_dir_path), candidate_path_length));
1174 };
1175
1176 for target in package.targets {
1177 let Some(bin_kind) = target
1178 .kind
1179 .iter()
1180 .find_map(|kind| TargetKind::try_from(kind.as_ref()).ok())
1181 else {
1182 continue;
1183 };
1184 let target_path = PathBuf::from(target.src_path);
1185 if target_path == abs_path {
1186 return manifest_path.map(|(path, _)| {
1187 (
1188 package_name_from_pkgid(&package.id).map(|package_name| TargetInfo {
1189 package_name: package_name.to_owned(),
1190 target_name: target.name,
1191 required_features: target.required_features,
1192 target_kind: bin_kind,
1193 }),
1194 path,
1195 )
1196 });
1197 }
1198 }
1199 }
1200
1201 manifest_path.map(|(path, _)| (None, path))
1202}
1203
1204async fn human_readable_package_name(
1205 package_directory: &Path,
1206 project_env: Option<&HashMap<String, String>>,
1207) -> Option<String> {
1208 let mut command = util::command::new_command("cargo");
1209 if let Some(envs) = project_env {
1210 command.envs(envs);
1211 }
1212 let pkgid = String::from_utf8(
1213 command
1214 .current_dir(package_directory)
1215 .arg("pkgid")
1216 .output()
1217 .await
1218 .log_err()?
1219 .stdout,
1220 )
1221 .ok()?;
1222 Some(package_name_from_pkgid(&pkgid)?.to_owned())
1223}
1224
1225// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
1226// Output example in the root of Zed project:
1227// ```sh
1228// ❯ cargo pkgid zed
1229// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
1230// ```
1231// Another variant, if a project has a custom package name or hyphen in the name:
1232// ```
1233// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
1234// ```
1235//
1236// Extracts the package name from the output according to the spec:
1237// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
1238fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
1239 fn split_off_suffix(input: &str, suffix_start: char) -> &str {
1240 match input.rsplit_once(suffix_start) {
1241 Some((without_suffix, _)) => without_suffix,
1242 None => input,
1243 }
1244 }
1245
1246 let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
1247 let package_name = match version_suffix.rsplit_once('@') {
1248 Some((custom_package_name, _version)) => custom_package_name,
1249 None => {
1250 let host_and_path = split_off_suffix(version_prefix, '?');
1251 let (_, package_name) = host_and_path.rsplit_once('/')?;
1252 package_name
1253 }
1254 };
1255 Some(package_name)
1256}
1257
1258async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
1259 let binary_result = maybe!(async {
1260 let mut last = None;
1261 let mut entries = fs::read_dir(&container_dir)
1262 .await
1263 .with_context(|| format!("listing {container_dir:?}"))?;
1264 while let Some(entry) = entries.next().await {
1265 let path = entry?.path();
1266 if path.extension().is_some_and(|ext| ext == "metadata") {
1267 continue;
1268 }
1269 last = Some(path);
1270 }
1271
1272 let path = match last {
1273 Some(last) => last,
1274 None => return Ok(None),
1275 };
1276 let path = match RustLspAdapter::GITHUB_ASSET_KIND {
1277 AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Gz => path, // Tar and gzip extract in place.
1278 AssetKind::Zip => path.join("rust-analyzer.exe"), // zip contains a .exe
1279 };
1280
1281 anyhow::Ok(Some(LanguageServerBinary {
1282 path,
1283 env: None,
1284 arguments: Vec::new(),
1285 }))
1286 })
1287 .await;
1288
1289 match binary_result {
1290 Ok(Some(binary)) => Some(binary),
1291 Ok(None) => {
1292 log::info!("No cached rust-analyzer binary found");
1293 None
1294 }
1295 Err(e) => {
1296 log::error!("Failed to look up cached rust-analyzer binary: {e:#}");
1297 None
1298 }
1299 }
1300}
1301
1302fn test_fragment(variables: &TaskVariables, path: &Path, stem: &str) -> String {
1303 let fragment = if stem == "lib" {
1304 // This isn't quite right---it runs the tests for the entire library, rather than
1305 // just for the top-level `mod tests`. But we don't really have the means here to
1306 // filter out just that module.
1307 Some("--lib".to_owned())
1308 } else if stem == "mod" {
1309 maybe!({ Some(path.parent()?.file_name()?.to_string_lossy().into_owned()) })
1310 } else if stem == "main" {
1311 if let (Some(bin_name), Some(bin_kind)) = (
1312 variables.get(&RUST_BIN_NAME_TASK_VARIABLE),
1313 variables.get(&RUST_BIN_KIND_TASK_VARIABLE),
1314 ) {
1315 Some(format!("--{bin_kind}={bin_name}"))
1316 } else {
1317 None
1318 }
1319 } else {
1320 Some(stem.to_owned())
1321 };
1322 fragment.unwrap_or_else(|| "--".to_owned())
1323}
1324
1325#[cfg(test)]
1326mod tests {
1327 use std::num::NonZeroU32;
1328
1329 use super::*;
1330 use crate::language;
1331 use gpui::{BorrowAppContext, Hsla, TestAppContext};
1332 use lsp::CompletionItemLabelDetails;
1333 use settings::SettingsStore;
1334 use theme::SyntaxTheme;
1335 use util::path;
1336
1337 #[gpui::test]
1338 async fn test_process_rust_diagnostics() {
1339 let mut params = lsp::PublishDiagnosticsParams {
1340 uri: lsp::Uri::from_file_path(path!("/a")).unwrap(),
1341 version: None,
1342 diagnostics: vec![
1343 // no newlines
1344 lsp::Diagnostic {
1345 message: "use of moved value `a`".to_string(),
1346 ..Default::default()
1347 },
1348 // newline at the end of a code span
1349 lsp::Diagnostic {
1350 message: "consider importing this struct: `use b::c;\n`".to_string(),
1351 ..Default::default()
1352 },
1353 // code span starting right after a newline
1354 lsp::Diagnostic {
1355 message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1356 .to_string(),
1357 ..Default::default()
1358 },
1359 ],
1360 };
1361 RustLspAdapter.process_diagnostics(&mut params, LanguageServerId(0), None);
1362
1363 assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
1364
1365 // remove trailing newline from code span
1366 assert_eq!(
1367 params.diagnostics[1].message,
1368 "consider importing this struct: `use b::c;`"
1369 );
1370
1371 // do not remove newline before the start of code span
1372 assert_eq!(
1373 params.diagnostics[2].message,
1374 "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1375 );
1376 }
1377
1378 #[gpui::test]
1379 async fn test_rust_label_for_completion() {
1380 let adapter = Arc::new(RustLspAdapter);
1381 let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1382 let grammar = language.grammar().unwrap();
1383 let theme = SyntaxTheme::new_test([
1384 ("type", Hsla::default()),
1385 ("keyword", Hsla::default()),
1386 ("function", Hsla::default()),
1387 ("property", Hsla::default()),
1388 ]);
1389
1390 language.set_theme(&theme);
1391
1392 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1393 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1394 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1395 let highlight_field = grammar.highlight_id_for_name("property").unwrap();
1396
1397 assert_eq!(
1398 adapter
1399 .label_for_completion(
1400 &lsp::CompletionItem {
1401 kind: Some(lsp::CompletionItemKind::FUNCTION),
1402 label: "hello(…)".to_string(),
1403 label_details: Some(CompletionItemLabelDetails {
1404 detail: Some("(use crate::foo)".into()),
1405 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string())
1406 }),
1407 ..Default::default()
1408 },
1409 &language
1410 )
1411 .await,
1412 Some(CodeLabel::new(
1413 "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1414 0..5,
1415 vec![
1416 (0..5, highlight_function),
1417 (7..10, highlight_keyword),
1418 (11..17, highlight_type),
1419 (18..19, highlight_type),
1420 (25..28, highlight_type),
1421 (29..30, highlight_type),
1422 ],
1423 ))
1424 );
1425 assert_eq!(
1426 adapter
1427 .label_for_completion(
1428 &lsp::CompletionItem {
1429 kind: Some(lsp::CompletionItemKind::FUNCTION),
1430 label: "hello(…)".to_string(),
1431 label_details: Some(CompletionItemLabelDetails {
1432 detail: Some("(use crate::foo)".into()),
1433 description: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
1434 }),
1435 ..Default::default()
1436 },
1437 &language
1438 )
1439 .await,
1440 Some(CodeLabel::new(
1441 "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1442 0..5,
1443 vec![
1444 (0..5, highlight_function),
1445 (7..10, highlight_keyword),
1446 (11..17, highlight_type),
1447 (18..19, highlight_type),
1448 (25..28, highlight_type),
1449 (29..30, highlight_type),
1450 ],
1451 ))
1452 );
1453 assert_eq!(
1454 adapter
1455 .label_for_completion(
1456 &lsp::CompletionItem {
1457 kind: Some(lsp::CompletionItemKind::FIELD),
1458 label: "len".to_string(),
1459 detail: Some("usize".to_string()),
1460 ..Default::default()
1461 },
1462 &language
1463 )
1464 .await,
1465 Some(CodeLabel::new(
1466 "len: usize".to_string(),
1467 0..3,
1468 vec![(0..3, highlight_field), (5..10, highlight_type),],
1469 ))
1470 );
1471
1472 assert_eq!(
1473 adapter
1474 .label_for_completion(
1475 &lsp::CompletionItem {
1476 kind: Some(lsp::CompletionItemKind::FUNCTION),
1477 label: "hello(…)".to_string(),
1478 label_details: Some(CompletionItemLabelDetails {
1479 detail: Some("(use crate::foo)".to_string()),
1480 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1481 }),
1482
1483 ..Default::default()
1484 },
1485 &language
1486 )
1487 .await,
1488 Some(CodeLabel::new(
1489 "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1490 0..5,
1491 vec![
1492 (0..5, highlight_function),
1493 (7..10, highlight_keyword),
1494 (11..17, highlight_type),
1495 (18..19, highlight_type),
1496 (25..28, highlight_type),
1497 (29..30, highlight_type),
1498 ],
1499 ))
1500 );
1501
1502 assert_eq!(
1503 adapter
1504 .label_for_completion(
1505 &lsp::CompletionItem {
1506 kind: Some(lsp::CompletionItemKind::FUNCTION),
1507 label: "hello".to_string(),
1508 label_details: Some(CompletionItemLabelDetails {
1509 detail: Some("(use crate::foo)".to_string()),
1510 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1511 }),
1512 ..Default::default()
1513 },
1514 &language
1515 )
1516 .await,
1517 Some(CodeLabel::new(
1518 "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1519 0..5,
1520 vec![
1521 (0..5, highlight_function),
1522 (7..10, highlight_keyword),
1523 (11..17, highlight_type),
1524 (18..19, highlight_type),
1525 (25..28, highlight_type),
1526 (29..30, highlight_type),
1527 ],
1528 ))
1529 );
1530
1531 assert_eq!(
1532 adapter
1533 .label_for_completion(
1534 &lsp::CompletionItem {
1535 kind: Some(lsp::CompletionItemKind::METHOD),
1536 label: "await.as_deref_mut()".to_string(),
1537 filter_text: Some("as_deref_mut".to_string()),
1538 label_details: Some(CompletionItemLabelDetails {
1539 detail: None,
1540 description: Some("fn(&mut self) -> IterMut<'_, T>".to_string()),
1541 }),
1542 ..Default::default()
1543 },
1544 &language
1545 )
1546 .await,
1547 Some(CodeLabel::new(
1548 "await.as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1549 6..18,
1550 vec![
1551 (6..18, HighlightId(2)),
1552 (20..23, HighlightId(1)),
1553 (33..40, HighlightId(0)),
1554 (45..46, HighlightId(0))
1555 ],
1556 ))
1557 );
1558
1559 assert_eq!(
1560 adapter
1561 .label_for_completion(
1562 &lsp::CompletionItem {
1563 kind: Some(lsp::CompletionItemKind::METHOD),
1564 label: "as_deref_mut()".to_string(),
1565 filter_text: Some("as_deref_mut".to_string()),
1566 label_details: Some(CompletionItemLabelDetails {
1567 detail: None,
1568 description: Some(
1569 "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string()
1570 ),
1571 }),
1572 ..Default::default()
1573 },
1574 &language
1575 )
1576 .await,
1577 Some(CodeLabel::new(
1578 "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1579 7..19,
1580 vec![
1581 (0..3, HighlightId(1)),
1582 (4..6, HighlightId(1)),
1583 (7..19, HighlightId(2)),
1584 (21..24, HighlightId(1)),
1585 (34..41, HighlightId(0)),
1586 (46..47, HighlightId(0))
1587 ],
1588 ))
1589 );
1590
1591 assert_eq!(
1592 adapter
1593 .label_for_completion(
1594 &lsp::CompletionItem {
1595 kind: Some(lsp::CompletionItemKind::FIELD),
1596 label: "inner_value".to_string(),
1597 filter_text: Some("value".to_string()),
1598 detail: Some("String".to_string()),
1599 ..Default::default()
1600 },
1601 &language,
1602 )
1603 .await,
1604 Some(CodeLabel::new(
1605 "inner_value: String".to_string(),
1606 6..11,
1607 vec![(0..11, HighlightId(3)), (13..19, HighlightId(0))],
1608 ))
1609 );
1610
1611 // Snippet with insert tabstop (empty placeholder)
1612 assert_eq!(
1613 adapter
1614 .label_for_completion(
1615 &lsp::CompletionItem {
1616 kind: Some(lsp::CompletionItemKind::SNIPPET),
1617 label: "println!".to_string(),
1618 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1619 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1620 range: lsp::Range::default(),
1621 new_text: "println!(\"$1\", $2)$0".to_string(),
1622 })),
1623 ..Default::default()
1624 },
1625 &language,
1626 )
1627 .await,
1628 Some(CodeLabel::new(
1629 "println!(\"…\", …)".to_string(),
1630 0..8,
1631 vec![
1632 (10..13, HighlightId::TABSTOP_INSERT_ID),
1633 (16..19, HighlightId::TABSTOP_INSERT_ID),
1634 (0..7, HighlightId(2)),
1635 (7..8, HighlightId(2)),
1636 ],
1637 ))
1638 );
1639
1640 // Snippet with replace tabstop (placeholder with default text)
1641 assert_eq!(
1642 adapter
1643 .label_for_completion(
1644 &lsp::CompletionItem {
1645 kind: Some(lsp::CompletionItemKind::SNIPPET),
1646 label: "vec!".to_string(),
1647 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1648 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1649 range: lsp::Range::default(),
1650 new_text: "vec![${1:elem}]$0".to_string(),
1651 })),
1652 ..Default::default()
1653 },
1654 &language,
1655 )
1656 .await,
1657 Some(CodeLabel::new(
1658 "vec![elem]".to_string(),
1659 0..4,
1660 vec![
1661 (5..9, HighlightId::TABSTOP_REPLACE_ID),
1662 (0..3, HighlightId(2)),
1663 (3..4, HighlightId(2)),
1664 ],
1665 ))
1666 );
1667
1668 // Snippet with tabstop appearing more than once
1669 assert_eq!(
1670 adapter
1671 .label_for_completion(
1672 &lsp::CompletionItem {
1673 kind: Some(lsp::CompletionItemKind::SNIPPET),
1674 label: "if let".to_string(),
1675 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1676 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1677 range: lsp::Range::default(),
1678 new_text: "if let ${1:pat} = $1 {\n $0\n}".to_string(),
1679 })),
1680 ..Default::default()
1681 },
1682 &language,
1683 )
1684 .await,
1685 Some(CodeLabel::new(
1686 "if let pat = … {\n \n}".to_string(),
1687 0..6,
1688 vec![
1689 (7..10, HighlightId::TABSTOP_REPLACE_ID),
1690 (13..16, HighlightId::TABSTOP_INSERT_ID),
1691 (0..2, HighlightId(1)),
1692 (3..6, HighlightId(1)),
1693 ],
1694 ))
1695 );
1696
1697 // Snippet with tabstops not in left-to-right order
1698 assert_eq!(
1699 adapter
1700 .label_for_completion(
1701 &lsp::CompletionItem {
1702 kind: Some(lsp::CompletionItemKind::SNIPPET),
1703 label: "for".to_string(),
1704 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1705 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1706 range: lsp::Range::default(),
1707 new_text: "for ${2:item} in ${1:iter} {\n $0\n}".to_string(),
1708 })),
1709 ..Default::default()
1710 },
1711 &language,
1712 )
1713 .await,
1714 Some(CodeLabel::new(
1715 "for item in iter {\n \n}".to_string(),
1716 0..3,
1717 vec![
1718 (4..8, HighlightId::TABSTOP_REPLACE_ID),
1719 (12..16, HighlightId::TABSTOP_REPLACE_ID),
1720 (0..3, HighlightId(1)),
1721 (9..11, HighlightId(1)),
1722 ],
1723 ))
1724 );
1725
1726 // Postfix completion without actual tabstops (only implicit final $0)
1727 // The label should use completion.label so it can be filtered by "ref"
1728 let ref_completion = adapter
1729 .label_for_completion(
1730 &lsp::CompletionItem {
1731 kind: Some(lsp::CompletionItemKind::SNIPPET),
1732 label: "ref".to_string(),
1733 filter_text: Some("ref".to_string()),
1734 label_details: Some(CompletionItemLabelDetails {
1735 detail: None,
1736 description: Some("&expr".to_string()),
1737 }),
1738 detail: Some("&expr".to_string()),
1739 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1740 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1741 range: lsp::Range::default(),
1742 new_text: "&String::new()".to_string(),
1743 })),
1744 ..Default::default()
1745 },
1746 &language,
1747 )
1748 .await;
1749 assert!(
1750 ref_completion.is_some(),
1751 "ref postfix completion should have a label"
1752 );
1753 let ref_label = ref_completion.unwrap();
1754 let filter_text = &ref_label.text[ref_label.filter_range.clone()];
1755 assert!(
1756 filter_text.contains("ref"),
1757 "filter range text '{filter_text}' should contain 'ref' for filtering to work",
1758 );
1759
1760 // Test for correct range calculation with mixed empty and non-empty tabstops.(See https://github.com/zed-industries/zed/issues/44825)
1761 let res = adapter
1762 .label_for_completion(
1763 &lsp::CompletionItem {
1764 kind: Some(lsp::CompletionItemKind::STRUCT),
1765 label: "Particles".to_string(),
1766 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1767 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1768 range: lsp::Range::default(),
1769 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(),
1770 })),
1771 ..Default::default()
1772 },
1773 &language,
1774 )
1775 .await
1776 .unwrap();
1777
1778 assert_eq!(
1779 res,
1780 CodeLabel::new(
1781 "Particles { pos_x: …, pos_y: …, vel_x: …, vel_y: …, acc_x: (), acc_y: (), mass: … }".to_string(),
1782 0..9,
1783 vec![
1784 (19..22, HighlightId::TABSTOP_INSERT_ID),
1785 (31..34, HighlightId::TABSTOP_INSERT_ID),
1786 (43..46, HighlightId::TABSTOP_INSERT_ID),
1787 (55..58, HighlightId::TABSTOP_INSERT_ID),
1788 (67..69, HighlightId::TABSTOP_REPLACE_ID),
1789 (78..80, HighlightId::TABSTOP_REPLACE_ID),
1790 (88..91, HighlightId::TABSTOP_INSERT_ID),
1791 (0..9, highlight_type),
1792 (60..65, highlight_field),
1793 (71..76, highlight_field),
1794 ],
1795 )
1796 );
1797 }
1798
1799 #[gpui::test]
1800 async fn test_rust_label_for_symbol() {
1801 let adapter = Arc::new(RustLspAdapter);
1802 let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1803 let grammar = language.grammar().unwrap();
1804 let theme = SyntaxTheme::new_test([
1805 ("type", Hsla::default()),
1806 ("keyword", Hsla::default()),
1807 ("function", Hsla::default()),
1808 ("property", Hsla::default()),
1809 ]);
1810
1811 language.set_theme(&theme);
1812
1813 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1814 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1815 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1816
1817 assert_eq!(
1818 adapter
1819 .label_for_symbol(
1820 &language::Symbol {
1821 name: "hello".to_string(),
1822 kind: lsp::SymbolKind::FUNCTION,
1823 container_name: None,
1824 },
1825 &language
1826 )
1827 .await,
1828 Some(CodeLabel::new(
1829 "fn hello".to_string(),
1830 3..8,
1831 vec![(0..2, highlight_keyword), (3..8, highlight_function)],
1832 ))
1833 );
1834
1835 assert_eq!(
1836 adapter
1837 .label_for_symbol(
1838 &language::Symbol {
1839 name: "World".to_string(),
1840 kind: lsp::SymbolKind::TYPE_PARAMETER,
1841 container_name: None,
1842 },
1843 &language
1844 )
1845 .await,
1846 Some(CodeLabel::new(
1847 "type World".to_string(),
1848 5..10,
1849 vec![(0..4, highlight_keyword), (5..10, highlight_type)],
1850 ))
1851 );
1852
1853 assert_eq!(
1854 adapter
1855 .label_for_symbol(
1856 &language::Symbol {
1857 name: "zed".to_string(),
1858 kind: lsp::SymbolKind::PACKAGE,
1859 container_name: None,
1860 },
1861 &language
1862 )
1863 .await,
1864 Some(CodeLabel::new(
1865 "extern crate zed".to_string(),
1866 13..16,
1867 vec![(0..6, highlight_keyword), (7..12, highlight_keyword),],
1868 ))
1869 );
1870
1871 assert_eq!(
1872 adapter
1873 .label_for_symbol(
1874 &language::Symbol {
1875 name: "Variant".to_string(),
1876 kind: lsp::SymbolKind::ENUM_MEMBER,
1877 container_name: None,
1878 },
1879 &language
1880 )
1881 .await,
1882 Some(CodeLabel::new(
1883 "Variant".to_string(),
1884 0..7,
1885 vec![(0..7, highlight_type)],
1886 ))
1887 );
1888 }
1889
1890 #[gpui::test]
1891 async fn test_rust_autoindent(cx: &mut TestAppContext) {
1892 // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
1893 cx.update(|cx| {
1894 let test_settings = SettingsStore::test(cx);
1895 cx.set_global(test_settings);
1896 cx.update_global::<SettingsStore, _>(|store, cx| {
1897 store.update_user_settings(cx, |s| {
1898 s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
1899 });
1900 });
1901 });
1902
1903 let language = crate::language("rust", tree_sitter_rust::LANGUAGE.into());
1904
1905 cx.new(|cx| {
1906 let mut buffer = Buffer::local("", cx).with_language(language, cx);
1907
1908 // indent between braces
1909 buffer.set_text("fn a() {}", cx);
1910 let ix = buffer.len() - 1;
1911 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1912 assert_eq!(buffer.text(), "fn a() {\n \n}");
1913
1914 // indent between braces, even after empty lines
1915 buffer.set_text("fn a() {\n\n\n}", cx);
1916 let ix = buffer.len() - 2;
1917 buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1918 assert_eq!(buffer.text(), "fn a() {\n\n\n \n}");
1919
1920 // indent a line that continues a field expression
1921 buffer.set_text("fn a() {\n \n}", cx);
1922 let ix = buffer.len() - 2;
1923 buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
1924 assert_eq!(buffer.text(), "fn a() {\n b\n .c\n}");
1925
1926 // indent further lines that continue the field expression, even after empty lines
1927 let ix = buffer.len() - 2;
1928 buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
1929 assert_eq!(buffer.text(), "fn a() {\n b\n .c\n \n .d\n}");
1930
1931 // dedent the line after the field expression
1932 let ix = buffer.len() - 2;
1933 buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
1934 assert_eq!(
1935 buffer.text(),
1936 "fn a() {\n b\n .c\n \n .d;\n e\n}"
1937 );
1938
1939 // indent inside a struct within a call
1940 buffer.set_text("const a: B = c(D {});", cx);
1941 let ix = buffer.len() - 3;
1942 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1943 assert_eq!(buffer.text(), "const a: B = c(D {\n \n});");
1944
1945 // indent further inside a nested call
1946 let ix = buffer.len() - 4;
1947 buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
1948 assert_eq!(buffer.text(), "const a: B = c(D {\n e: f(\n \n )\n});");
1949
1950 // keep that indent after an empty line
1951 let ix = buffer.len() - 8;
1952 buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1953 assert_eq!(
1954 buffer.text(),
1955 "const a: B = c(D {\n e: f(\n \n \n )\n});"
1956 );
1957
1958 buffer
1959 });
1960 }
1961
1962 #[test]
1963 fn test_package_name_from_pkgid() {
1964 for (input, expected) in [
1965 (
1966 "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
1967 "zed",
1968 ),
1969 (
1970 "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
1971 "my-custom-package",
1972 ),
1973 ] {
1974 assert_eq!(package_name_from_pkgid(input), Some(expected));
1975 }
1976 }
1977
1978 #[test]
1979 fn test_target_info_from_metadata() {
1980 for (input, absolute_path, expected) in [
1981 (
1982 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"}]}]}"#,
1983 "/path/to/zed/src/main.rs",
1984 Some((
1985 Some(TargetInfo {
1986 package_name: "zed".into(),
1987 target_name: "zed".into(),
1988 required_features: Vec::new(),
1989 target_kind: TargetKind::Bin,
1990 }),
1991 Arc::from("/path/to/zed".as_ref()),
1992 )),
1993 ),
1994 (
1995 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"}]}]}"#,
1996 "/path/to/custom-package/src/main.rs",
1997 Some((
1998 Some(TargetInfo {
1999 package_name: "my-custom-package".into(),
2000 target_name: "my-custom-bin".into(),
2001 required_features: Vec::new(),
2002 target_kind: TargetKind::Bin,
2003 }),
2004 Arc::from("/path/to/custom-package".as_ref()),
2005 )),
2006 ),
2007 (
2008 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"}]}"#,
2009 "/path/to/custom-package/src/main.rs",
2010 Some((
2011 Some(TargetInfo {
2012 package_name: "my-custom-package".into(),
2013 target_name: "my-custom-bin".into(),
2014 required_features: Vec::new(),
2015 target_kind: TargetKind::Example,
2016 }),
2017 Arc::from("/path/to/custom-package".as_ref()),
2018 )),
2019 ),
2020 (
2021 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"]}]}]}"#,
2022 "/path/to/custom-package/src/main.rs",
2023 Some((
2024 Some(TargetInfo {
2025 package_name: "my-custom-package".into(),
2026 target_name: "my-custom-bin".into(),
2027 required_features: vec!["foo".to_owned(), "bar".to_owned()],
2028 target_kind: TargetKind::Example,
2029 }),
2030 Arc::from("/path/to/custom-package".as_ref()),
2031 )),
2032 ),
2033 (
2034 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"}]}"#,
2035 "/path/to/custom-package/src/main.rs",
2036 Some((
2037 Some(TargetInfo {
2038 package_name: "my-custom-package".into(),
2039 target_name: "my-custom-bin".into(),
2040 required_features: vec![],
2041 target_kind: TargetKind::Example,
2042 }),
2043 Arc::from("/path/to/custom-package".as_ref()),
2044 )),
2045 ),
2046 (
2047 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"}]}"#,
2048 "/path/to/custom-package/src/main.rs",
2049 Some((None, Arc::from("/path/to/custom-package".as_ref()))),
2050 ),
2051 ] {
2052 let metadata: CargoMetadata = serde_json::from_str(input).context(input).unwrap();
2053
2054 let absolute_path = Path::new(absolute_path);
2055
2056 assert_eq!(target_info_from_metadata(metadata, absolute_path), expected);
2057 }
2058 }
2059
2060 #[test]
2061 fn test_rust_test_fragment() {
2062 #[track_caller]
2063 fn check(
2064 variables: impl IntoIterator<Item = (VariableName, &'static str)>,
2065 path: &str,
2066 expected: &str,
2067 ) {
2068 let path = Path::new(path);
2069 let found = test_fragment(
2070 &TaskVariables::from_iter(variables.into_iter().map(|(k, v)| (k, v.to_owned()))),
2071 path,
2072 path.file_stem().unwrap().to_str().unwrap(),
2073 );
2074 assert_eq!(expected, found);
2075 }
2076
2077 check([], "/project/src/lib.rs", "--lib");
2078 check([], "/project/src/foo/mod.rs", "foo");
2079 check(
2080 [
2081 (RUST_BIN_KIND_TASK_VARIABLE.clone(), "bin"),
2082 (RUST_BIN_NAME_TASK_VARIABLE, "x"),
2083 ],
2084 "/project/src/main.rs",
2085 "--bin=x",
2086 );
2087 check([], "/project/src/main.rs", "--");
2088 }
2089
2090 #[test]
2091 fn test_convert_rust_analyzer_schema() {
2092 let raw_schema = serde_json::json!([
2093 {
2094 "title": "Assist",
2095 "properties": {
2096 "rust-analyzer.assist.emitMustUse": {
2097 "markdownDescription": "Insert #[must_use] when generating `as_` methods for enum variants.",
2098 "default": false,
2099 "type": "boolean"
2100 }
2101 }
2102 },
2103 {
2104 "title": "Assist",
2105 "properties": {
2106 "rust-analyzer.assist.expressionFillDefault": {
2107 "markdownDescription": "Placeholder expression to use for missing expressions in assists.",
2108 "default": "todo",
2109 "type": "string"
2110 }
2111 }
2112 },
2113 {
2114 "title": "Cache Priming",
2115 "properties": {
2116 "rust-analyzer.cachePriming.enable": {
2117 "markdownDescription": "Warm up caches on project load.",
2118 "default": true,
2119 "type": "boolean"
2120 }
2121 }
2122 }
2123 ]);
2124
2125 let converted = RustLspAdapter::convert_rust_analyzer_schema(&raw_schema);
2126
2127 assert_eq!(
2128 converted.get("type").and_then(|v| v.as_str()),
2129 Some("object")
2130 );
2131
2132 let properties = converted
2133 .pointer("/properties")
2134 .expect("should have properties")
2135 .as_object()
2136 .expect("properties should be object");
2137
2138 assert!(properties.contains_key("assist"));
2139 assert!(properties.contains_key("cachePriming"));
2140 assert!(!properties.contains_key("rust-analyzer"));
2141
2142 let assist_props = properties
2143 .get("assist")
2144 .expect("should have assist")
2145 .pointer("/properties")
2146 .expect("assist should have properties")
2147 .as_object()
2148 .expect("assist properties should be object");
2149
2150 assert!(assist_props.contains_key("emitMustUse"));
2151 assert!(assist_props.contains_key("expressionFillDefault"));
2152
2153 let emit_must_use = assist_props
2154 .get("emitMustUse")
2155 .expect("should have emitMustUse");
2156 assert_eq!(
2157 emit_must_use.get("type").and_then(|v| v.as_str()),
2158 Some("boolean")
2159 );
2160 assert_eq!(
2161 emit_must_use.get("default").and_then(|v| v.as_bool()),
2162 Some(false)
2163 );
2164
2165 let cache_priming_props = properties
2166 .get("cachePriming")
2167 .expect("should have cachePriming")
2168 .pointer("/properties")
2169 .expect("cachePriming should have properties")
2170 .as_object()
2171 .expect("cachePriming properties should be object");
2172
2173 assert!(cache_priming_props.contains_key("enable"));
2174 }
2175}