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