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