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