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