1use anyhow::{anyhow, Context, Result};
2use async_compression::futures::bufread::GzipDecoder;
3use async_trait::async_trait;
4use collections::HashMap;
5use futures::{io::BufReader, StreamExt};
6use gpui::{AppContext, AsyncAppContext, Task};
7use http_client::github::AssetKind;
8use http_client::github::{latest_github_release, GitHubLspBinaryVersion};
9pub use language::*;
10use lsp::{LanguageServerBinary, LanguageServerName};
11use regex::Regex;
12use smol::fs::{self};
13use std::fmt::Display;
14use std::{
15 any::Any,
16 borrow::Cow,
17 path::{Path, PathBuf},
18 sync::{Arc, LazyLock},
19};
20use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
21use util::{fs::remove_matching, maybe, ResultExt};
22
23use crate::language_settings::language_settings;
24
25pub struct RustLspAdapter;
26
27#[cfg(target_os = "macos")]
28impl RustLspAdapter {
29 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
30 const ARCH_SERVER_NAME: &str = "apple-darwin";
31}
32
33#[cfg(target_os = "linux")]
34impl RustLspAdapter {
35 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
36 const ARCH_SERVER_NAME: &str = "unknown-linux-gnu";
37}
38
39#[cfg(target_os = "freebsd")]
40impl RustLspAdapter {
41 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
42 const ARCH_SERVER_NAME: &str = "unknown-freebsd";
43}
44
45#[cfg(target_os = "windows")]
46impl RustLspAdapter {
47 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
48 const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
49}
50
51impl RustLspAdapter {
52 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer");
53
54 fn build_asset_name() -> String {
55 let extension = match Self::GITHUB_ASSET_KIND {
56 AssetKind::TarGz => "tar.gz",
57 AssetKind::Gz => "gz",
58 AssetKind::Zip => "zip",
59 };
60
61 format!(
62 "{}-{}-{}.{}",
63 Self::SERVER_NAME,
64 std::env::consts::ARCH,
65 Self::ARCH_SERVER_NAME,
66 extension
67 )
68 }
69}
70
71#[async_trait(?Send)]
72impl LspAdapter for RustLspAdapter {
73 fn name(&self) -> LanguageServerName {
74 Self::SERVER_NAME.clone()
75 }
76
77 async fn check_if_user_installed(
78 &self,
79 delegate: &dyn LspAdapterDelegate,
80 _: Arc<dyn LanguageToolchainStore>,
81 _: &AsyncAppContext,
82 ) -> Option<LanguageServerBinary> {
83 let path = delegate.which("rust-analyzer".as_ref()).await?;
84 let env = delegate.shell_env().await;
85
86 // It is surprisingly common for ~/.cargo/bin/rust-analyzer to be a symlink to
87 // /usr/bin/rust-analyzer that fails when you run it; so we need to test it.
88 log::info!("found rust-analyzer in PATH. trying to run `rust-analyzer --help`");
89 let result = delegate
90 .try_exec(LanguageServerBinary {
91 path: path.clone(),
92 arguments: vec!["--help".into()],
93 env: Some(env.clone()),
94 })
95 .await;
96 if let Err(err) = result {
97 log::error!(
98 "failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {}",
99 path,
100 err
101 );
102 return None;
103 }
104
105 Some(LanguageServerBinary {
106 path,
107 env: Some(env),
108 arguments: vec![],
109 })
110 }
111
112 async fn fetch_latest_server_version(
113 &self,
114 delegate: &dyn LspAdapterDelegate,
115 ) -> Result<Box<dyn 'static + Send + Any>> {
116 let release = latest_github_release(
117 "rust-lang/rust-analyzer",
118 true,
119 false,
120 delegate.http_client(),
121 )
122 .await?;
123 let asset_name = Self::build_asset_name();
124
125 let asset = release
126 .assets
127 .iter()
128 .find(|asset| asset.name == asset_name)
129 .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
130 Ok(Box::new(GitHubLspBinaryVersion {
131 name: release.tag_name,
132 url: asset.browser_download_url.clone(),
133 }))
134 }
135
136 async fn fetch_server_binary(
137 &self,
138 version: Box<dyn 'static + Send + Any>,
139 container_dir: PathBuf,
140 delegate: &dyn LspAdapterDelegate,
141 ) -> Result<LanguageServerBinary> {
142 let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
143 let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
144 let server_path = match Self::GITHUB_ASSET_KIND {
145 AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place.
146 AssetKind::Zip => destination_path.clone().join("rust-analyzer.exe"), // zip contains a .exe
147 };
148
149 if fs::metadata(&server_path).await.is_err() {
150 remove_matching(&container_dir, |entry| entry != destination_path).await;
151
152 let mut response = delegate
153 .http_client()
154 .get(&version.url, Default::default(), true)
155 .await
156 .with_context(|| format!("downloading release from {}", version.url))?;
157 match Self::GITHUB_ASSET_KIND {
158 AssetKind::TarGz => {
159 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
160 let archive = async_tar::Archive::new(decompressed_bytes);
161 archive.unpack(&destination_path).await.with_context(|| {
162 format!("extracting {} to {:?}", version.url, destination_path)
163 })?;
164 }
165 AssetKind::Gz => {
166 let mut decompressed_bytes =
167 GzipDecoder::new(BufReader::new(response.body_mut()));
168 let mut file =
169 fs::File::create(&destination_path).await.with_context(|| {
170 format!(
171 "creating a file {:?} for a download from {}",
172 destination_path, version.url,
173 )
174 })?;
175 futures::io::copy(&mut decompressed_bytes, &mut file)
176 .await
177 .with_context(|| {
178 format!("extracting {} to {:?}", version.url, destination_path)
179 })?;
180 }
181 AssetKind::Zip => {
182 node_runtime::extract_zip(
183 &destination_path,
184 BufReader::new(response.body_mut()),
185 )
186 .await
187 .with_context(|| {
188 format!("unzipping {} to {:?}", version.url, destination_path)
189 })?;
190 }
191 };
192
193 // todo("windows")
194 #[cfg(not(windows))]
195 {
196 fs::set_permissions(
197 &server_path,
198 <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
199 )
200 .await?;
201 }
202 }
203
204 Ok(LanguageServerBinary {
205 path: server_path,
206 env: None,
207 arguments: Default::default(),
208 })
209 }
210
211 async fn cached_server_binary(
212 &self,
213 container_dir: PathBuf,
214 _: &dyn LspAdapterDelegate,
215 ) -> Option<LanguageServerBinary> {
216 get_cached_server_binary(container_dir).await
217 }
218
219 fn disk_based_diagnostic_sources(&self) -> Vec<String> {
220 vec!["rustc".into()]
221 }
222
223 fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
224 Some("rust-analyzer/flycheck".into())
225 }
226
227 fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
228 static REGEX: LazyLock<Regex> =
229 LazyLock::new(|| Regex::new(r"(?m)`([^`]+)\n`$").expect("Failed to create REGEX"));
230
231 for diagnostic in &mut params.diagnostics {
232 for message in diagnostic
233 .related_information
234 .iter_mut()
235 .flatten()
236 .map(|info| &mut info.message)
237 .chain([&mut diagnostic.message])
238 {
239 if let Cow::Owned(sanitized) = REGEX.replace_all(message, "`$1`") {
240 *message = sanitized;
241 }
242 }
243 }
244 }
245
246 async fn label_for_completion(
247 &self,
248 completion: &lsp::CompletionItem,
249 language: &Arc<Language>,
250 ) -> Option<CodeLabel> {
251 let detail = completion
252 .label_details
253 .as_ref()
254 .and_then(|detail| detail.detail.as_ref())
255 .or(completion.detail.as_ref())
256 .map(ToOwned::to_owned);
257 let function_signature = completion
258 .label_details
259 .as_ref()
260 .and_then(|detail| detail.description.as_ref())
261 .or(completion.detail.as_ref())
262 .map(ToOwned::to_owned);
263 match completion.kind {
264 Some(lsp::CompletionItemKind::FIELD) if detail.is_some() => {
265 let name = &completion.label;
266 let text = format!("{}: {}", name, detail.unwrap());
267 let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
268 let runs = language.highlight_text(&source, 11..11 + text.len());
269 return Some(CodeLabel {
270 text,
271 runs,
272 filter_range: 0..name.len(),
273 });
274 }
275 Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
276 if detail.is_some()
277 && completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) =>
278 {
279 let name = &completion.label;
280 let text = format!(
281 "{}: {}",
282 name,
283 completion.detail.as_ref().or(detail.as_ref()).unwrap()
284 );
285 let source = Rope::from(format!("let {} = ();", text).as_str());
286 let runs = language.highlight_text(&source, 4..4 + text.len());
287 return Some(CodeLabel {
288 text,
289 runs,
290 filter_range: 0..name.len(),
291 });
292 }
293 Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
294 if detail.is_some() =>
295 {
296 static REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("\\(…?\\)").unwrap());
297
298 let detail = detail.unwrap();
299 const FUNCTION_PREFIXES: [&str; 6] = [
300 "async fn",
301 "async unsafe fn",
302 "const fn",
303 "const unsafe fn",
304 "unsafe fn",
305 "fn",
306 ];
307 // Is it function `async`?
308 let fn_keyword = FUNCTION_PREFIXES.iter().find_map(|prefix| {
309 function_signature.as_ref().and_then(|signature| {
310 signature
311 .strip_prefix(*prefix)
312 .map(|suffix| (*prefix, suffix))
313 })
314 });
315 // fn keyword should be followed by opening parenthesis.
316 if let Some((prefix, suffix)) = fn_keyword {
317 let mut text = REGEX.replace(&completion.label, suffix).to_string();
318 let source = Rope::from(format!("{prefix} {} {{}}", text).as_str());
319 let run_start = prefix.len() + 1;
320 let runs = language.highlight_text(&source, run_start..run_start + text.len());
321 if detail.starts_with(" (") {
322 text.push_str(&detail);
323 }
324
325 return Some(CodeLabel {
326 filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
327 text,
328 runs,
329 });
330 } else if completion
331 .detail
332 .as_ref()
333 .map_or(false, |detail| detail.starts_with("macro_rules! "))
334 {
335 let source = Rope::from(completion.label.as_str());
336 let runs = language.highlight_text(&source, 0..completion.label.len());
337
338 return Some(CodeLabel {
339 filter_range: 0..completion.label.len(),
340 text: completion.label.clone(),
341 runs,
342 });
343 }
344 }
345 Some(kind) => {
346 let highlight_name = match kind {
347 lsp::CompletionItemKind::STRUCT
348 | lsp::CompletionItemKind::INTERFACE
349 | lsp::CompletionItemKind::ENUM => Some("type"),
350 lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
351 lsp::CompletionItemKind::KEYWORD => Some("keyword"),
352 lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
353 Some("constant")
354 }
355 _ => None,
356 };
357
358 let mut label = completion.label.clone();
359 if let Some(detail) = detail.filter(|detail| detail.starts_with(" (")) {
360 use std::fmt::Write;
361 write!(label, "{detail}").ok()?;
362 }
363 let mut label = CodeLabel::plain(label, None);
364 if let Some(highlight_name) = highlight_name {
365 let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name)?;
366 label.runs.push((
367 0..label.text.rfind('(').unwrap_or(completion.label.len()),
368 highlight_id,
369 ));
370 }
371
372 return Some(label);
373 }
374 _ => {}
375 }
376 None
377 }
378
379 async fn label_for_symbol(
380 &self,
381 name: &str,
382 kind: lsp::SymbolKind,
383 language: &Arc<Language>,
384 ) -> Option<CodeLabel> {
385 let (text, filter_range, display_range) = match kind {
386 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
387 let text = format!("fn {} () {{}}", name);
388 let filter_range = 3..3 + name.len();
389 let display_range = 0..filter_range.end;
390 (text, filter_range, display_range)
391 }
392 lsp::SymbolKind::STRUCT => {
393 let text = format!("struct {} {{}}", name);
394 let filter_range = 7..7 + name.len();
395 let display_range = 0..filter_range.end;
396 (text, filter_range, display_range)
397 }
398 lsp::SymbolKind::ENUM => {
399 let text = format!("enum {} {{}}", name);
400 let filter_range = 5..5 + name.len();
401 let display_range = 0..filter_range.end;
402 (text, filter_range, display_range)
403 }
404 lsp::SymbolKind::INTERFACE => {
405 let text = format!("trait {} {{}}", name);
406 let filter_range = 6..6 + name.len();
407 let display_range = 0..filter_range.end;
408 (text, filter_range, display_range)
409 }
410 lsp::SymbolKind::CONSTANT => {
411 let text = format!("const {}: () = ();", name);
412 let filter_range = 6..6 + name.len();
413 let display_range = 0..filter_range.end;
414 (text, filter_range, display_range)
415 }
416 lsp::SymbolKind::MODULE => {
417 let text = format!("mod {} {{}}", name);
418 let filter_range = 4..4 + name.len();
419 let display_range = 0..filter_range.end;
420 (text, filter_range, display_range)
421 }
422 lsp::SymbolKind::TYPE_PARAMETER => {
423 let text = format!("type {} {{}}", name);
424 let filter_range = 5..5 + name.len();
425 let display_range = 0..filter_range.end;
426 (text, filter_range, display_range)
427 }
428 _ => return None,
429 };
430
431 Some(CodeLabel {
432 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
433 text: text[display_range].to_string(),
434 filter_range,
435 })
436 }
437}
438
439pub(crate) struct RustContextProvider;
440
441const RUST_PACKAGE_TASK_VARIABLE: VariableName =
442 VariableName::Custom(Cow::Borrowed("RUST_PACKAGE"));
443
444/// The bin name corresponding to the current file in Cargo.toml
445const RUST_BIN_NAME_TASK_VARIABLE: VariableName =
446 VariableName::Custom(Cow::Borrowed("RUST_BIN_NAME"));
447
448/// The bin kind (bin/example) corresponding to the current file in Cargo.toml
449const RUST_BIN_KIND_TASK_VARIABLE: VariableName =
450 VariableName::Custom(Cow::Borrowed("RUST_BIN_KIND"));
451
452const RUST_MAIN_FUNCTION_TASK_VARIABLE: VariableName =
453 VariableName::Custom(Cow::Borrowed("_rust_main_function_end"));
454
455impl ContextProvider for RustContextProvider {
456 fn build_context(
457 &self,
458 task_variables: &TaskVariables,
459 location: &Location,
460 project_env: Option<HashMap<String, String>>,
461 _: Arc<dyn LanguageToolchainStore>,
462 cx: &mut gpui::AppContext,
463 ) -> Task<Result<TaskVariables>> {
464 let local_abs_path = location
465 .buffer
466 .read(cx)
467 .file()
468 .and_then(|file| Some(file.as_local()?.abs_path(cx)));
469
470 let local_abs_path = local_abs_path.as_deref();
471
472 let is_main_function = task_variables
473 .get(&RUST_MAIN_FUNCTION_TASK_VARIABLE)
474 .is_some();
475
476 if is_main_function {
477 if let Some(target) = local_abs_path.and_then(|path| {
478 package_name_and_bin_name_from_abs_path(path, project_env.as_ref())
479 }) {
480 return Task::ready(Ok(TaskVariables::from_iter([
481 (RUST_PACKAGE_TASK_VARIABLE.clone(), target.package_name),
482 (RUST_BIN_NAME_TASK_VARIABLE.clone(), target.target_name),
483 (
484 RUST_BIN_KIND_TASK_VARIABLE.clone(),
485 target.target_kind.to_string(),
486 ),
487 ])));
488 }
489 }
490
491 if let Some(package_name) = local_abs_path
492 .and_then(|local_abs_path| local_abs_path.parent())
493 .and_then(|path| human_readable_package_name(path, project_env.as_ref()))
494 {
495 return Task::ready(Ok(TaskVariables::from_iter([(
496 RUST_PACKAGE_TASK_VARIABLE.clone(),
497 package_name,
498 )])));
499 }
500
501 Task::ready(Ok(TaskVariables::default()))
502 }
503
504 fn associated_tasks(
505 &self,
506 file: Option<Arc<dyn language::File>>,
507 cx: &AppContext,
508 ) -> Option<TaskTemplates> {
509 const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
510 let package_to_run = language_settings(Some("Rust".into()), file.as_ref(), cx)
511 .tasks
512 .variables
513 .get(DEFAULT_RUN_NAME_STR)
514 .cloned();
515 let run_task_args = if let Some(package_to_run) = package_to_run {
516 vec!["run".into(), "-p".into(), package_to_run]
517 } else {
518 vec!["run".into()]
519 };
520 Some(TaskTemplates(vec![
521 TaskTemplate {
522 label: format!(
523 "cargo check -p {}",
524 RUST_PACKAGE_TASK_VARIABLE.template_value(),
525 ),
526 command: "cargo".into(),
527 args: vec![
528 "check".into(),
529 "-p".into(),
530 RUST_PACKAGE_TASK_VARIABLE.template_value(),
531 ],
532 cwd: Some("$ZED_DIRNAME".to_owned()),
533 ..TaskTemplate::default()
534 },
535 TaskTemplate {
536 label: "cargo check --workspace --all-targets".into(),
537 command: "cargo".into(),
538 args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
539 cwd: Some("$ZED_DIRNAME".to_owned()),
540 ..TaskTemplate::default()
541 },
542 TaskTemplate {
543 label: format!(
544 "cargo test -p {} {} -- --nocapture",
545 RUST_PACKAGE_TASK_VARIABLE.template_value(),
546 VariableName::Symbol.template_value(),
547 ),
548 command: "cargo".into(),
549 args: vec![
550 "test".into(),
551 "-p".into(),
552 RUST_PACKAGE_TASK_VARIABLE.template_value(),
553 VariableName::Symbol.template_value(),
554 "--".into(),
555 "--nocapture".into(),
556 ],
557 tags: vec!["rust-test".to_owned()],
558 cwd: Some("$ZED_DIRNAME".to_owned()),
559 ..TaskTemplate::default()
560 },
561 TaskTemplate {
562 label: format!(
563 "cargo test -p {} {}",
564 RUST_PACKAGE_TASK_VARIABLE.template_value(),
565 VariableName::Stem.template_value(),
566 ),
567 command: "cargo".into(),
568 args: vec![
569 "test".into(),
570 "-p".into(),
571 RUST_PACKAGE_TASK_VARIABLE.template_value(),
572 VariableName::Stem.template_value(),
573 ],
574 tags: vec!["rust-mod-test".to_owned()],
575 cwd: Some("$ZED_DIRNAME".to_owned()),
576 ..TaskTemplate::default()
577 },
578 TaskTemplate {
579 label: format!(
580 "cargo run -p {} --{} {}",
581 RUST_PACKAGE_TASK_VARIABLE.template_value(),
582 RUST_BIN_KIND_TASK_VARIABLE.template_value(),
583 RUST_BIN_NAME_TASK_VARIABLE.template_value(),
584 ),
585 command: "cargo".into(),
586 args: vec![
587 "run".into(),
588 "-p".into(),
589 RUST_PACKAGE_TASK_VARIABLE.template_value(),
590 format!("--{}", RUST_BIN_KIND_TASK_VARIABLE.template_value()),
591 RUST_BIN_NAME_TASK_VARIABLE.template_value(),
592 ],
593 cwd: Some("$ZED_DIRNAME".to_owned()),
594 tags: vec!["rust-main".to_owned()],
595 ..TaskTemplate::default()
596 },
597 TaskTemplate {
598 label: format!(
599 "cargo test -p {}",
600 RUST_PACKAGE_TASK_VARIABLE.template_value()
601 ),
602 command: "cargo".into(),
603 args: vec![
604 "test".into(),
605 "-p".into(),
606 RUST_PACKAGE_TASK_VARIABLE.template_value(),
607 ],
608 cwd: Some("$ZED_DIRNAME".to_owned()),
609 ..TaskTemplate::default()
610 },
611 TaskTemplate {
612 label: "cargo run".into(),
613 command: "cargo".into(),
614 args: run_task_args,
615 cwd: Some("$ZED_DIRNAME".to_owned()),
616 ..TaskTemplate::default()
617 },
618 TaskTemplate {
619 label: "cargo clean".into(),
620 command: "cargo".into(),
621 args: vec!["clean".into()],
622 cwd: Some("$ZED_DIRNAME".to_owned()),
623 ..TaskTemplate::default()
624 },
625 ]))
626 }
627}
628
629/// Part of the data structure of Cargo metadata
630#[derive(serde::Deserialize)]
631struct CargoMetadata {
632 packages: Vec<CargoPackage>,
633}
634
635#[derive(serde::Deserialize)]
636struct CargoPackage {
637 id: String,
638 targets: Vec<CargoTarget>,
639}
640
641#[derive(serde::Deserialize)]
642struct CargoTarget {
643 name: String,
644 kind: Vec<String>,
645 src_path: String,
646}
647
648#[derive(Debug, PartialEq)]
649enum TargetKind {
650 Bin,
651 Example,
652}
653
654impl Display for TargetKind {
655 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
656 match self {
657 TargetKind::Bin => write!(f, "bin"),
658 TargetKind::Example => write!(f, "example"),
659 }
660 }
661}
662
663impl TryFrom<&str> for TargetKind {
664 type Error = ();
665 fn try_from(value: &str) -> Result<Self, ()> {
666 match value {
667 "bin" => Ok(Self::Bin),
668 "example" => Ok(Self::Example),
669 _ => Err(()),
670 }
671 }
672}
673/// Which package and binary target are we in?
674struct TargetInfo {
675 package_name: String,
676 target_name: String,
677 target_kind: TargetKind,
678}
679
680fn package_name_and_bin_name_from_abs_path(
681 abs_path: &Path,
682 project_env: Option<&HashMap<String, String>>,
683) -> Option<TargetInfo> {
684 let mut command = util::command::new_std_command("cargo");
685 if let Some(envs) = project_env {
686 command.envs(envs);
687 }
688 let output = command
689 .current_dir(abs_path.parent()?)
690 .arg("metadata")
691 .arg("--no-deps")
692 .arg("--format-version")
693 .arg("1")
694 .output()
695 .log_err()?
696 .stdout;
697
698 let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
699
700 retrieve_package_id_and_bin_name_from_metadata(metadata, abs_path).and_then(
701 |(package_id, bin_name, target_kind)| {
702 let package_name = package_name_from_pkgid(&package_id);
703
704 package_name.map(|package_name| TargetInfo {
705 package_name: package_name.to_owned(),
706 target_name: bin_name,
707 target_kind,
708 })
709 },
710 )
711}
712
713fn retrieve_package_id_and_bin_name_from_metadata(
714 metadata: CargoMetadata,
715 abs_path: &Path,
716) -> Option<(String, String, TargetKind)> {
717 for package in metadata.packages {
718 for target in package.targets {
719 let Some(bin_kind) = target
720 .kind
721 .iter()
722 .find_map(|kind| TargetKind::try_from(kind.as_ref()).ok())
723 else {
724 continue;
725 };
726 let target_path = PathBuf::from(target.src_path);
727 if target_path == abs_path {
728 return Some((package.id, target.name, bin_kind));
729 }
730 }
731 }
732
733 None
734}
735
736fn human_readable_package_name(
737 package_directory: &Path,
738 project_env: Option<&HashMap<String, String>>,
739) -> Option<String> {
740 let mut command = util::command::new_std_command("cargo");
741 if let Some(envs) = project_env {
742 command.envs(envs);
743 }
744 let pkgid = String::from_utf8(
745 command
746 .current_dir(package_directory)
747 .arg("pkgid")
748 .output()
749 .log_err()?
750 .stdout,
751 )
752 .ok()?;
753 Some(package_name_from_pkgid(&pkgid)?.to_owned())
754}
755
756// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
757// Output example in the root of Zed project:
758// ```sh
759// ❯ cargo pkgid zed
760// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
761// ```
762// Another variant, if a project has a custom package name or hyphen in the name:
763// ```
764// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
765// ```
766//
767// Extracts the package name from the output according to the spec:
768// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
769fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
770 fn split_off_suffix(input: &str, suffix_start: char) -> &str {
771 match input.rsplit_once(suffix_start) {
772 Some((without_suffix, _)) => without_suffix,
773 None => input,
774 }
775 }
776
777 let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
778 let package_name = match version_suffix.rsplit_once('@') {
779 Some((custom_package_name, _version)) => custom_package_name,
780 None => {
781 let host_and_path = split_off_suffix(version_prefix, '?');
782 let (_, package_name) = host_and_path.rsplit_once('/')?;
783 package_name
784 }
785 };
786 Some(package_name)
787}
788
789async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
790 maybe!(async {
791 let mut last = None;
792 let mut entries = fs::read_dir(&container_dir).await?;
793 while let Some(entry) = entries.next().await {
794 last = Some(entry?.path());
795 }
796
797 anyhow::Ok(LanguageServerBinary {
798 path: last.ok_or_else(|| anyhow!("no cached binary"))?,
799 env: None,
800 arguments: Default::default(),
801 })
802 })
803 .await
804 .log_err()
805}
806
807#[cfg(test)]
808mod tests {
809 use std::num::NonZeroU32;
810
811 use super::*;
812 use crate::language;
813 use gpui::{BorrowAppContext, Context, Hsla, TestAppContext};
814 use language::language_settings::AllLanguageSettings;
815 use lsp::CompletionItemLabelDetails;
816 use settings::SettingsStore;
817 use theme::SyntaxTheme;
818
819 #[gpui::test]
820 async fn test_process_rust_diagnostics() {
821 let mut params = lsp::PublishDiagnosticsParams {
822 uri: lsp::Url::from_file_path("/a").unwrap(),
823 version: None,
824 diagnostics: vec![
825 // no newlines
826 lsp::Diagnostic {
827 message: "use of moved value `a`".to_string(),
828 ..Default::default()
829 },
830 // newline at the end of a code span
831 lsp::Diagnostic {
832 message: "consider importing this struct: `use b::c;\n`".to_string(),
833 ..Default::default()
834 },
835 // code span starting right after a newline
836 lsp::Diagnostic {
837 message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
838 .to_string(),
839 ..Default::default()
840 },
841 ],
842 };
843 RustLspAdapter.process_diagnostics(&mut params);
844
845 assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
846
847 // remove trailing newline from code span
848 assert_eq!(
849 params.diagnostics[1].message,
850 "consider importing this struct: `use b::c;`"
851 );
852
853 // do not remove newline before the start of code span
854 assert_eq!(
855 params.diagnostics[2].message,
856 "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
857 );
858 }
859
860 #[gpui::test]
861 async fn test_rust_label_for_completion() {
862 let adapter = Arc::new(RustLspAdapter);
863 let language = language("rust", tree_sitter_rust::LANGUAGE.into());
864 let grammar = language.grammar().unwrap();
865 let theme = SyntaxTheme::new_test([
866 ("type", Hsla::default()),
867 ("keyword", Hsla::default()),
868 ("function", Hsla::default()),
869 ("property", Hsla::default()),
870 ]);
871
872 language.set_theme(&theme);
873
874 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
875 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
876 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
877 let highlight_field = grammar.highlight_id_for_name("property").unwrap();
878
879 assert_eq!(
880 adapter
881 .label_for_completion(
882 &lsp::CompletionItem {
883 kind: Some(lsp::CompletionItemKind::FUNCTION),
884 label: "hello(…)".to_string(),
885 label_details: Some(CompletionItemLabelDetails {
886 detail: Some(" (use crate::foo)".into()),
887 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string())
888 }),
889 ..Default::default()
890 },
891 &language
892 )
893 .await,
894 Some(CodeLabel {
895 text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
896 filter_range: 0..5,
897 runs: vec![
898 (0..5, highlight_function),
899 (7..10, highlight_keyword),
900 (11..17, highlight_type),
901 (18..19, highlight_type),
902 (25..28, highlight_type),
903 (29..30, highlight_type),
904 ],
905 })
906 );
907 assert_eq!(
908 adapter
909 .label_for_completion(
910 &lsp::CompletionItem {
911 kind: Some(lsp::CompletionItemKind::FUNCTION),
912 label: "hello(…)".to_string(),
913 label_details: Some(CompletionItemLabelDetails {
914 detail: Some(" (use crate::foo)".into()),
915 description: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
916 }),
917 ..Default::default()
918 },
919 &language
920 )
921 .await,
922 Some(CodeLabel {
923 text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
924 filter_range: 0..5,
925 runs: vec![
926 (0..5, highlight_function),
927 (7..10, highlight_keyword),
928 (11..17, highlight_type),
929 (18..19, highlight_type),
930 (25..28, highlight_type),
931 (29..30, highlight_type),
932 ],
933 })
934 );
935 assert_eq!(
936 adapter
937 .label_for_completion(
938 &lsp::CompletionItem {
939 kind: Some(lsp::CompletionItemKind::FIELD),
940 label: "len".to_string(),
941 detail: Some("usize".to_string()),
942 ..Default::default()
943 },
944 &language
945 )
946 .await,
947 Some(CodeLabel {
948 text: "len: usize".to_string(),
949 filter_range: 0..3,
950 runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
951 })
952 );
953
954 assert_eq!(
955 adapter
956 .label_for_completion(
957 &lsp::CompletionItem {
958 kind: Some(lsp::CompletionItemKind::FUNCTION),
959 label: "hello(…)".to_string(),
960 label_details: Some(CompletionItemLabelDetails {
961 detail: Some(" (use crate::foo)".to_string()),
962 description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
963 }),
964
965 ..Default::default()
966 },
967 &language
968 )
969 .await,
970 Some(CodeLabel {
971 text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
972 filter_range: 0..5,
973 runs: vec![
974 (0..5, highlight_function),
975 (7..10, highlight_keyword),
976 (11..17, highlight_type),
977 (18..19, highlight_type),
978 (25..28, highlight_type),
979 (29..30, highlight_type),
980 ],
981 })
982 );
983 }
984
985 #[gpui::test]
986 async fn test_rust_label_for_symbol() {
987 let adapter = Arc::new(RustLspAdapter);
988 let language = language("rust", tree_sitter_rust::LANGUAGE.into());
989 let grammar = language.grammar().unwrap();
990 let theme = SyntaxTheme::new_test([
991 ("type", Hsla::default()),
992 ("keyword", Hsla::default()),
993 ("function", Hsla::default()),
994 ("property", Hsla::default()),
995 ]);
996
997 language.set_theme(&theme);
998
999 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1000 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1001 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1002
1003 assert_eq!(
1004 adapter
1005 .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
1006 .await,
1007 Some(CodeLabel {
1008 text: "fn hello".to_string(),
1009 filter_range: 3..8,
1010 runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
1011 })
1012 );
1013
1014 assert_eq!(
1015 adapter
1016 .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
1017 .await,
1018 Some(CodeLabel {
1019 text: "type World".to_string(),
1020 filter_range: 5..10,
1021 runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
1022 })
1023 );
1024 }
1025
1026 #[gpui::test]
1027 async fn test_rust_autoindent(cx: &mut TestAppContext) {
1028 // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
1029 cx.update(|cx| {
1030 let test_settings = SettingsStore::test(cx);
1031 cx.set_global(test_settings);
1032 language::init(cx);
1033 cx.update_global::<SettingsStore, _>(|store, cx| {
1034 store.update_user_settings::<AllLanguageSettings>(cx, |s| {
1035 s.defaults.tab_size = NonZeroU32::new(2);
1036 });
1037 });
1038 });
1039
1040 let language = crate::language("rust", tree_sitter_rust::LANGUAGE.into());
1041
1042 cx.new_model(|cx| {
1043 let mut buffer = Buffer::local("", cx).with_language(language, cx);
1044
1045 // indent between braces
1046 buffer.set_text("fn a() {}", cx);
1047 let ix = buffer.len() - 1;
1048 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1049 assert_eq!(buffer.text(), "fn a() {\n \n}");
1050
1051 // indent between braces, even after empty lines
1052 buffer.set_text("fn a() {\n\n\n}", cx);
1053 let ix = buffer.len() - 2;
1054 buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1055 assert_eq!(buffer.text(), "fn a() {\n\n\n \n}");
1056
1057 // indent a line that continues a field expression
1058 buffer.set_text("fn a() {\n \n}", cx);
1059 let ix = buffer.len() - 2;
1060 buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
1061 assert_eq!(buffer.text(), "fn a() {\n b\n .c\n}");
1062
1063 // indent further lines that continue the field expression, even after empty lines
1064 let ix = buffer.len() - 2;
1065 buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
1066 assert_eq!(buffer.text(), "fn a() {\n b\n .c\n \n .d\n}");
1067
1068 // dedent the line after the field expression
1069 let ix = buffer.len() - 2;
1070 buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
1071 assert_eq!(
1072 buffer.text(),
1073 "fn a() {\n b\n .c\n \n .d;\n e\n}"
1074 );
1075
1076 // indent inside a struct within a call
1077 buffer.set_text("const a: B = c(D {});", cx);
1078 let ix = buffer.len() - 3;
1079 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1080 assert_eq!(buffer.text(), "const a: B = c(D {\n \n});");
1081
1082 // indent further inside a nested call
1083 let ix = buffer.len() - 4;
1084 buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
1085 assert_eq!(buffer.text(), "const a: B = c(D {\n e: f(\n \n )\n});");
1086
1087 // keep that indent after an empty line
1088 let ix = buffer.len() - 8;
1089 buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1090 assert_eq!(
1091 buffer.text(),
1092 "const a: B = c(D {\n e: f(\n \n \n )\n});"
1093 );
1094
1095 buffer
1096 });
1097 }
1098
1099 #[test]
1100 fn test_package_name_from_pkgid() {
1101 for (input, expected) in [
1102 (
1103 "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
1104 "zed",
1105 ),
1106 (
1107 "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
1108 "my-custom-package",
1109 ),
1110 ] {
1111 assert_eq!(package_name_from_pkgid(input), Some(expected));
1112 }
1113 }
1114
1115 #[test]
1116 fn test_retrieve_package_id_and_bin_name_from_metadata() {
1117 for (input, absolute_path, expected) in [
1118 (
1119 r#"{"packages":[{"id":"path+file:///path/to/zed/crates/zed#0.131.0","targets":[{"name":"zed","kind":["bin"],"src_path":"/path/to/zed/src/main.rs"}]}]}"#,
1120 "/path/to/zed/src/main.rs",
1121 Some((
1122 "path+file:///path/to/zed/crates/zed#0.131.0",
1123 "zed",
1124 TargetKind::Bin,
1125 )),
1126 ),
1127 (
1128 r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","targets":[{"name":"my-custom-bin","kind":["bin"],"src_path":"/path/to/custom-package/src/main.rs"}]}]}"#,
1129 "/path/to/custom-package/src/main.rs",
1130 Some((
1131 "path+file:///path/to/custom-package#my-custom-package@0.1.0",
1132 "my-custom-bin",
1133 TargetKind::Bin,
1134 )),
1135 ),
1136 (
1137 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"}]}]}"#,
1138 "/path/to/custom-package/src/main.rs",
1139 Some((
1140 "path+file:///path/to/custom-package#my-custom-package@0.1.0",
1141 "my-custom-bin",
1142 TargetKind::Example,
1143 )),
1144 ),
1145 (
1146 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"}]}]}"#,
1147 "/path/to/custom-package/src/main.rs",
1148 None,
1149 ),
1150 ] {
1151 let metadata: CargoMetadata = serde_json::from_str(input).unwrap();
1152
1153 let absolute_path = Path::new(absolute_path);
1154
1155 assert_eq!(
1156 retrieve_package_id_and_bin_name_from_metadata(metadata, absolute_path),
1157 expected.map(|(pkgid, name, kind)| (pkgid.to_owned(), name.to_owned(), kind))
1158 );
1159 }
1160 }
1161}