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