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