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