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