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