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