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