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