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