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