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