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