1use std::collections::{HashMap, HashSet};
3use std::env;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7
8use argh::FromArgs;
9use bacon::Bacon;
10use flume::RecvError;
11use ls_types::{Diagnostic, DiagnosticSeverity, MessageType, ProgressToken, Range, Uri, WorkspaceFolder};
12use native::Cargo;
13use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
14use serde_json::{Map, Value};
15use shadow::ShadowWorkspace;
16use tokio::sync::{RwLock, RwLockWriteGuard};
17use tokio::task::JoinHandle;
18use tokio_util::sync::CancellationToken;
19use tower_lsp_server::{Client, LspService, Server, jsonrpc};
20use tracing_subscriber::fmt::format::FmtSpan;
21
22mod bacon;
23mod lsp;
24mod native;
25mod shadow;
26
27const PKG_NAME: &str = env!("CARGO_PKG_NAME");
28pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
29const LOCATIONS_FILE: &str = ".bacon-locations";
30const BACON_BACKGROUND_COMMAND: &str = "bacon";
31const BACON_BACKGROUND_COMMAND_ARGS: &str = "--headless -j bacon-ls";
32
33const PATH_ENCODE_SET: &AsciiSet = &CONTROLS
39 .add(b' ')
40 .add(b'"')
41 .add(b'#')
42 .add(b'<')
43 .add(b'>')
44 .add(b'?')
45 .add(b'[')
46 .add(b'\\')
47 .add(b']')
48 .add(b'^')
49 .add(b'`')
50 .add(b'{')
51 .add(b'|')
52 .add(b'}')
53 .add(b'%');
54
55pub(crate) fn path_to_file_uri(path: &str) -> String {
59 format!("file://{}", utf8_percent_encode(path, PATH_ENCODE_SET))
60}
61
62pub(crate) type DiagKey = (Range, i32, String);
66
67pub(crate) fn diag_key(d: &Diagnostic) -> DiagKey {
68 (d.range, severity_tag(d.severity), d.message.clone())
69}
70
71fn severity_tag(s: Option<DiagnosticSeverity>) -> i32 {
72 match s {
73 None => 0,
74 Some(s) if s == DiagnosticSeverity::ERROR => 1,
75 Some(s) if s == DiagnosticSeverity::WARNING => 2,
76 Some(s) if s == DiagnosticSeverity::INFORMATION => 3,
77 Some(s) if s == DiagnosticSeverity::HINT => 4,
78 Some(_) => -1,
79 }
80}
81
82#[derive(Debug, FromArgs)]
84pub struct Args {
85 #[argh(switch, short = 'v')]
87 pub version: bool,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91enum BackendChoice {
92 Cargo,
93 Bacon,
94}
95
96#[derive(Debug)]
97enum BackendRuntime {
98 Bacon {
99 config: BaconOptions,
100 runtime: BaconRuntime,
101 },
102 Cargo {
103 config: CargoOptions,
104 runtime: CargoRuntime,
105 },
106}
107
108impl BackendRuntime {
109 fn backend_choice(&self) -> BackendChoice {
110 match self {
111 Self::Bacon { .. } => BackendChoice::Bacon,
112 Self::Cargo { .. } => BackendChoice::Cargo,
113 }
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub(crate) enum CargoRunState {
119 Idle,
120 Running,
121 RunningPending,
122}
123
124#[derive(Debug, Copy, Clone)]
125pub(crate) enum PublishMode {
126 CancelRunning,
127 QueueIfRunning,
128}
129
130#[derive(Debug)]
131pub(crate) struct CargoOptions {
132 pub(crate) command: String,
134 pub(crate) features: Vec<String>,
135 pub(crate) package: Option<String>,
137 pub(crate) extra_command_args: Vec<String>,
139 pub(crate) env: Vec<(String, String)>,
140 pub(crate) publish_mode: PublishMode,
141 pub(crate) refresh_interval_seconds: Option<Duration>,
144 pub(crate) separate_child_diagnostics: Option<bool>,
148 pub(crate) check_on_save: bool,
149 pub(crate) clear_diagnostics_on_check: bool,
150 pub(crate) update_on_insert: bool,
156 pub(crate) update_on_insert_debounce: Duration,
160}
161
162impl CargoOptions {
163 pub(crate) fn build_command_args(&self) -> Vec<String> {
164 let mut args = vec![self.command.clone()];
165 args.push("--message-format=json-diagnostic-rendered-ansi".to_string());
166
167 if !self.features.is_empty() {
168 args.push("--features".to_string());
169 let mut features = String::new();
170 for feature in &self.features[..self.features.len() - 1] {
171 features += feature;
172 features += ",";
173 }
174 features += &self.features[self.features.len() - 1];
175 args.push(features);
176 }
177
178 if let Some(pkg) = self.package.clone() {
179 args.push("-p".to_string());
180 args.push(pkg);
181 }
182
183 for arg in self.extra_command_args.iter().cloned() {
184 args.push(arg);
185 }
186
187 args
188 }
189
190 pub(crate) fn update_from_json_obj(&mut self, cargo_obj: &Map<String, Value>) -> jsonrpc::Result<()> {
191 if let Some(value) = cargo_obj.get("command") {
192 self.command = value
193 .as_str()
194 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?
195 .to_string();
196 }
197
198 if let Some(value) = cargo_obj.get("features") {
199 self.features = value
200 .as_array()
201 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?
202 .iter()
203 .map(|item| {
204 item.as_str()
205 .map(|s| s.to_string())
206 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))
207 })
208 .collect::<jsonrpc::Result<Vec<_>>>()?;
209 }
210
211 if let Some(value) = cargo_obj.get("package") {
212 self.package = Some(
213 value
214 .as_str()
215 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?
216 .to_string(),
217 );
218 }
219
220 if let Some(value) = cargo_obj.get("extraArgs") {
221 self.extra_command_args = value
222 .as_array()
223 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?
224 .iter()
225 .map(|item| {
226 item.as_str()
227 .map(|s| s.to_string())
228 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))
229 })
230 .collect::<jsonrpc::Result<Vec<_>>>()?;
231 }
232
233 if let Some(value) = cargo_obj.get("env") {
234 self.env = value
235 .as_object()
236 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?
237 .iter()
238 .map(|(k, v)| {
239 let val = v
240 .as_str()
241 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?;
242 Ok((k.clone(), val.to_string()))
243 })
244 .collect::<jsonrpc::Result<Vec<_>>>()?;
245 }
246
247 if let Some(value) = cargo_obj.get("cancelRunning") {
248 let cancel = value
249 .as_bool()
250 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?;
251 self.publish_mode = if cancel {
252 PublishMode::CancelRunning
253 } else {
254 PublishMode::QueueIfRunning
255 };
256 }
257
258 if let Some(value) = cargo_obj.get("refreshIntervalSeconds") {
259 if value.is_null() {
260 self.refresh_interval_seconds = None;
261 } else {
262 let seconds = value
263 .as_i64()
264 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?;
265 if seconds < 0 {
266 self.refresh_interval_seconds = None;
267 } else {
268 self.refresh_interval_seconds = Some(Duration::from_secs(seconds as u64));
269 }
270 }
271 }
272
273 if let Some(value) = cargo_obj.get("separateChildDiagnostics") {
274 self.separate_child_diagnostics = value.as_bool();
275 }
276
277 if let Some(value) = cargo_obj.get("checkOnSave") {
278 self.check_on_save = value
279 .as_bool()
280 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?;
281 }
282
283 if let Some(value) = cargo_obj.get("clearDiagnosticsOnCheck") {
284 self.clear_diagnostics_on_check = value
285 .as_bool()
286 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?;
287 }
288
289 if let Some(value) = cargo_obj.get("updateOnInsertDebounceMillis") {
290 let millis = value
291 .as_u64()
292 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?;
293 self.update_on_insert_debounce = Duration::from_millis(millis);
294 }
295
296 Ok(())
297 }
298
299 pub(crate) fn reset(&mut self) {
300 *self = Self::default();
301 }
302}
303
304impl Default for CargoOptions {
305 fn default() -> Self {
306 Self {
307 env: Vec::new(),
308 publish_mode: PublishMode::CancelRunning,
309 command: "check".to_string(),
310 features: vec![],
311 extra_command_args: vec![],
312 package: None,
313 refresh_interval_seconds: Some(Duration::from_secs(1)),
314 separate_child_diagnostics: None,
315 check_on_save: true,
316 clear_diagnostics_on_check: false,
317 update_on_insert: false,
318 update_on_insert_debounce: Duration::from_millis(500),
319 }
320 }
321}
322
323#[derive(Debug)]
324pub(crate) struct BaconOptions {
325 pub(crate) locations_file: String,
326 pub(crate) run_in_background: bool,
327 pub(crate) run_in_background_command: String,
328 pub(crate) run_in_background_command_args: String,
329 pub(crate) validate_preferences: bool,
330 pub(crate) create_preferences_file: bool,
331 pub(crate) synchronize_all_open_files_wait: Duration,
332 pub(crate) update_on_save: bool,
333 pub(crate) update_on_save_wait: Duration,
334}
335
336impl BaconOptions {
337 pub(crate) fn update_from_json_obj(&mut self, bacon_obj: &Map<String, Value>) -> jsonrpc::Result<()> {
338 if let Some(value) = bacon_obj.get("locationsFile") {
339 self.locations_file = value
340 .as_str()
341 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?
342 .to_string();
343 }
344 if let Some(value) = bacon_obj.get("runInBackground") {
345 self.run_in_background = value
346 .as_bool()
347 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?;
348 }
349 if let Some(value) = bacon_obj.get("runInBackgroundCommand") {
350 self.run_in_background_command = value
351 .as_str()
352 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?
353 .to_string();
354 }
355 if let Some(value) = bacon_obj.get("runInBackgroundCommandArguments") {
356 self.run_in_background_command_args = value
357 .as_str()
358 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?
359 .to_string();
360 }
361 if let Some(value) = bacon_obj.get("validatePreferences") {
362 self.validate_preferences = value
363 .as_bool()
364 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?;
365 }
366 if let Some(value) = bacon_obj.get("createPreferencesFile") {
367 self.create_preferences_file = value
368 .as_bool()
369 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?;
370 }
371 if let Some(value) = bacon_obj.get("synchronizeAllOpenFilesWaitMillis") {
372 self.synchronize_all_open_files_wait = Duration::from_millis(
373 value
374 .as_u64()
375 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?,
376 );
377 }
378 if let Some(value) = bacon_obj.get("updateOnSave") {
379 self.update_on_save = value
380 .as_bool()
381 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?;
382 }
383 if let Some(value) = bacon_obj.get("updateOnSaveWaitMillis") {
384 self.update_on_save_wait = Duration::from_millis(
385 value
386 .as_u64()
387 .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?,
388 );
389 }
390
391 Ok(())
392 }
393
394 pub fn reset(&mut self) {
395 *self = Self::default();
396 }
397}
398
399impl Default for BaconOptions {
400 fn default() -> Self {
401 Self {
402 locations_file: LOCATIONS_FILE.to_string(),
403 run_in_background: true,
404 run_in_background_command: BACON_BACKGROUND_COMMAND.to_string(),
405 run_in_background_command_args: BACON_BACKGROUND_COMMAND_ARGS.to_string(),
406 validate_preferences: true,
407 create_preferences_file: true,
408 synchronize_all_open_files_wait: Duration::from_millis(2000),
409 update_on_save: true,
410 update_on_save_wait: Duration::from_millis(1000),
411 }
412 }
413}
414
415#[derive(Debug)]
418pub(crate) struct LiveCheckContext {
419 pub(crate) shadow_root: PathBuf,
420 pub(crate) shadow_target_dir: PathBuf,
421 pub(crate) real_root: PathBuf,
422}
423
424#[derive(Debug)]
425pub(crate) struct CargoRuntime {
426 cancel_token: CancellationToken,
427 run_state: CargoRunState,
428 files_with_diags: HashSet<Uri>,
429 diagnostics_version: i32,
430 build_folder: PathBuf,
431 last_run_started: Option<Instant>,
436 pub(crate) shadow: Option<ShadowWorkspace>,
441 pub(crate) dirty_files: HashSet<Uri>,
445 pub(crate) live_debounce: Option<JoinHandle<()>>,
449}
450
451impl Default for CargoRuntime {
452 fn default() -> Self {
453 Self {
454 cancel_token: CancellationToken::new(),
455 run_state: CargoRunState::Idle,
456 files_with_diags: HashSet::new(),
457 diagnostics_version: 0,
458 build_folder: PathBuf::new(),
459 last_run_started: None,
460 shadow: None,
461 dirty_files: HashSet::new(),
462 live_debounce: None,
463 }
464 }
465}
466
467#[derive(Debug)]
468pub(crate) struct BaconRuntime {
469 pub(crate) shutdown_token: CancellationToken,
470 pub(crate) open_files: HashSet<Uri>,
471 pub(crate) command_handle: Option<JoinHandle<()>>,
473 pub(crate) sync_files_handle: JoinHandle<()>,
474 pub(crate) diagnostics_version: i32,
477}
478
479#[derive(Debug, Default)]
480struct State {
481 project_root: Option<PathBuf>,
482 workspace_folders: Option<Vec<WorkspaceFolder>>,
483 diagnostics_data_supported: bool,
484 related_information_supported: bool,
485 backend: Option<BackendRuntime>,
486 init_update_on_insert: bool,
493}
494
495#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
496pub(crate) struct CorrectionEdit {
497 pub(crate) range: Range,
498 pub(crate) new_text: String,
499}
500
501#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
506pub(crate) struct Correction {
507 pub(crate) label: String,
508 pub(crate) edits: Vec<CorrectionEdit>,
509}
510
511impl Correction {
512 pub(crate) fn from_single(range: Range, new_text: &str) -> Self {
513 let label = if new_text.is_empty() {
514 "Remove".to_string()
515 } else {
516 format!("Replace with: {new_text}")
517 };
518 Self {
519 label,
520 edits: vec![CorrectionEdit {
521 range,
522 new_text: new_text.to_string(),
523 }],
524 }
525 }
526
527 pub(crate) fn from_multi(edits: Vec<CorrectionEdit>) -> Self {
528 let label = match edits.iter().find(|e| !e.new_text.is_empty()) {
529 None => "Remove".to_string(),
530 Some(e) => format!("Replace with: {}", e.new_text),
531 };
532 Self { label, edits }
533 }
534}
535
536#[derive(Debug, serde::Serialize, serde::Deserialize)]
537struct DiagnosticData {
538 corrections: Vec<Correction>,
539}
540
541#[derive(Debug, Clone)]
542pub struct BaconLs {
543 client: Arc<Client>,
544 state: Arc<RwLock<State>>,
545}
546
547impl BaconLs {
548 fn new(client: Client) -> Self {
549 Self {
550 client: Arc::new(client),
551 state: Arc::new(RwLock::new(State::default())),
552 }
553 }
554
555 fn configure_tracing(log_level: Option<String>, log_path: Option<&Path>) {
556 let level = log_level.unwrap_or_else(|| env::var("RUST_LOG").unwrap_or("off".to_string()));
558 if level == "off" {
559 return;
560 }
561 let default_path = PathBuf::from(format!("{PKG_NAME}.log"));
562 let log_path = log_path.unwrap_or(&default_path);
563 let file = match std::fs::OpenOptions::new()
564 .create(true)
565 .write(true)
566 .truncate(true)
567 .open(log_path)
568 {
569 Ok(file) => file,
570 Err(e) => {
571 eprintln!(
575 "{PKG_NAME}: could not open log file {}: {e} (tracing disabled)",
576 log_path.display()
577 );
578 return;
579 }
580 };
581 let _ = tracing_subscriber::fmt()
585 .with_env_filter(level)
586 .with_writer(file)
587 .with_thread_names(true)
588 .with_span_events(FmtSpan::CLOSE)
589 .with_target(true)
590 .with_file(true)
591 .with_line_number(true)
592 .try_init();
593 }
594
595 pub async fn serve() {
597 Self::configure_tracing(None, None);
598 let stdin = tokio::io::stdin();
600 let stdout = tokio::io::stdout();
601 let (service, socket) = LspService::new(Self::new);
603 Server::new(stdin, stdout, socket).serve(service).await;
604 std::process::exit(0);
610 }
611
612 async fn find_git_root_directory(path: &Path) -> Option<PathBuf> {
613 let output = tokio::process::Command::new("git")
614 .arg("-C")
615 .arg(path)
616 .arg("rev-parse")
617 .arg("--show-toplevel")
618 .output()
619 .await
620 .ok()?;
621
622 if output.status.success() {
623 String::from_utf8(output.stdout).ok().map(|v| PathBuf::from(v.trim()))
624 } else {
625 None
626 }
627 }
628
629 fn detect_backend(values: &Map<String, Value>) -> Result<BackendChoice, String> {
630 if let Some(value) = values.get("backend") {
631 let backend = value.as_str().ok_or("'backend' must be a string")?;
632 match backend {
633 "cargo" => Ok(BackendChoice::Cargo),
634 "bacon" => Ok(BackendChoice::Bacon),
635 other => Err(format!("Invalid backend value '{other}'. Must be 'cargo' or 'bacon'.")),
636 }
637 } else {
638 let has_cargo = values.get("cargo").and_then(|v| v.as_object()).is_some();
639 let has_bacon = values.get("bacon").and_then(|v| v.as_object()).is_some();
640 match (has_cargo, has_bacon) {
641 (true, true) => Err(
642 "Both 'cargo' and 'bacon' config sections present without a 'backend' key. \
643 Set 'backend' to 'cargo' or 'bacon'."
644 .to_string(),
645 ),
646 (_, true) => Ok(BackendChoice::Bacon),
647 _ => Ok(BackendChoice::Cargo),
648 }
649 }
650 }
651
652 async fn pull_configuration(&self) {
653 tracing::debug!("pull_configuration");
654
655 let configuration_fut = self.client.configuration(vec![ls_types::ConfigurationItem {
656 scope_uri: None,
657 section: Some("bacon_ls".to_string()),
658 }]);
659 let response = match tokio::time::timeout(std::time::Duration::from_secs(5), configuration_fut).await {
664 Ok(Ok(response)) => response,
665 Ok(Err(e)) => {
666 tracing::error!("failed to pull configuration: {e}");
667 return;
668 }
669 Err(_) => {
670 tracing::warn!("workspace/configuration request timed out; proceeding with defaults");
671 return;
672 }
673 };
674
675 let Some(settings) = response.into_iter().next() else {
676 tracing::warn!("empty configuration response from client");
677 return;
678 };
679
680 tracing::trace!("pulled configuration: {settings:#?}");
681 self.adapt_to_settings(&settings).await;
682 }
683
684 async fn adapt_to_settings(&self, settings: &Value) {
685 let mut state = self.state.write().await;
686 let Some(values) = settings.as_object() else {
687 tracing::warn!("configuration is not a JSON object");
688 return;
689 };
690
691 if state.backend.is_none() {
692 let backend_choice = match Self::detect_backend(values) {
693 Ok(choice) => {
694 tracing::info!(backend = ?choice, "backend detected");
695 choice
696 }
697 Err(msg) => {
698 tracing::error!("{msg}");
699 self.client.show_message(MessageType::ERROR, &msg).await;
700 return;
701 }
702 };
703
704 match backend_choice {
705 BackendChoice::Bacon => {
706 let mut config = BaconOptions::default();
707 if let Some(bacon_obj) = values.get("bacon").and_then(|v| v.as_object())
708 && let Err(e) = config.update_from_json_obj(bacon_obj)
709 {
710 tracing::error!("invalid bacon configuration: {e}");
711 self.client
712 .show_message(MessageType::ERROR, format!("Error in \"bacon\" section: {e}"))
713 .await;
714 }
715
716 if config.validate_preferences {
717 if let Err(e) = Bacon::validate_preferences(
718 &config.run_in_background_command,
719 config.create_preferences_file,
720 )
721 .await
722 {
723 tracing::error!("{e}");
724 self.client.show_message(MessageType::ERROR, e).await;
725 }
726 } else {
727 tracing::warn!("skipping validation of bacon preferences, validateBaconPreferences is false");
728 }
729
730 let proj_root = state.project_root.clone();
731 let shutdown_token = CancellationToken::new();
732 let command_handle = if config.run_in_background {
733 let mut current_dir = None;
734 if let Ok(cwd) = env::current_dir() {
735 current_dir = Self::find_git_root_directory(&cwd).await;
736 if let Some(dir) = ¤t_dir {
737 if !dir.join("Cargo.toml").exists() {
738 current_dir = proj_root;
739 }
740 } else {
741 current_dir = proj_root;
742 }
743 }
744
745 match Bacon::run_in_background(
746 &config.run_in_background_command,
747 &config.run_in_background_command_args,
748 current_dir.as_ref(),
749 shutdown_token.clone(),
750 )
751 .await
752 {
753 Ok(command) => {
754 tracing::info!("bacon was started successfully and is running in the background");
755 Some(command)
756 }
757 Err(e) => {
758 tracing::error!("{e}");
759 self.client.show_message(MessageType::ERROR, e).await;
760 None
761 }
762 }
763 } else {
764 tracing::warn!("skipping background bacon startup, runBaconInBackground is false");
765 None
766 };
767
768 let task_state = self.state.clone();
769 let task_client = self.client.clone();
770 state.backend = Some(BackendRuntime::Bacon {
771 config,
772 runtime: BaconRuntime {
773 shutdown_token,
774 open_files: HashSet::new(),
775 command_handle,
776 sync_files_handle: tokio::task::spawn(Self::synchronize_diagnostics(
777 task_state,
778 task_client,
779 )),
780 diagnostics_version: 0,
781 },
782 });
783 tracing::info!("bacon backend initialized");
784 }
785 BackendChoice::Cargo => {
786 let mut config = CargoOptions::default();
787 if state.init_update_on_insert {
794 config.update_on_insert = true;
795 }
796 if let Some(cargo_obj) = values.get("cargo").and_then(|v| v.as_object())
797 && let Err(e) = config.update_from_json_obj(cargo_obj)
798 {
799 tracing::error!("invalid cargo configuration: {e}");
800 self.client
801 .show_message(MessageType::ERROR, format!("Error in \"cargo\" section: {e}"))
802 .await;
803 }
804 if let Err(e) = Self::init_cargo_backend(&mut state, config) {
805 tracing::error!("{e}");
806 drop(state);
807 self.client.show_message(MessageType::ERROR, e).await;
808 return;
809 }
810 drop(state);
811 }
812 }
813 } else {
814 let current_choice = match &state.backend {
815 Some(BackendRuntime::Bacon { .. }) => BackendChoice::Bacon,
816 Some(BackendRuntime::Cargo { .. }) => BackendChoice::Cargo,
817 None => unreachable!("backend is Some in this branch"),
818 };
819 let desired = match Self::detect_backend(values) {
820 Ok(choice) => choice,
821 Err(err) => {
822 tracing::error!("invalid backend configuration on reload: {err}");
823 self.client.show_message(MessageType::ERROR, &err).await;
824 return;
825 }
826 };
827
828 if desired != current_choice {
829 let msg = "Backend cannot be changed while the server is running. \
830 Restart the server to switch backends.";
831 tracing::error!("{msg}");
832 self.client.show_message(MessageType::ERROR, msg).await;
833 return;
834 }
835
836 let project_root = state.project_root.clone();
837 let init_update_on_insert = state.init_update_on_insert;
838 match &mut state.backend {
839 Some(BackendRuntime::Cargo { config, runtime }) => {
840 config.reset();
841 if init_update_on_insert {
842 config.update_on_insert = true;
843 }
844 if let Some(cargo_obj) = values.get("cargo").and_then(|v| v.as_object())
845 && let Err(e) = config.update_from_json_obj(cargo_obj)
846 {
847 tracing::error!("invalid cargo configuration: {e}");
848 self.client
849 .show_message(MessageType::ERROR, format!("Error in \"cargo\" section: {e}"))
850 .await;
851 }
852 if let Some(root) = project_root {
853 runtime.build_folder = root;
854 }
855 tracing::debug!("cargo configuration updated");
856 }
857 Some(BackendRuntime::Bacon { config, .. }) => {
858 config.reset();
859 if let Some(bacon_obj) = values.get("bacon").and_then(|v| v.as_object())
860 && let Err(e) = config.update_from_json_obj(bacon_obj)
861 {
862 tracing::error!("invalid bacon configuration: {e}");
863 self.client
864 .show_message(MessageType::ERROR, format!("Error in \"bacon\" section: {e}"))
865 .await;
866 }
867 tracing::debug!("bacon configuration updated");
868 }
869 None => unreachable!("backend is Some in this branch"),
870 }
871 }
872 }
873
874 fn init_cargo_backend(state: &mut RwLockWriteGuard<'_, State>, config: CargoOptions) -> Result<(), String> {
875 let build_folder = match &state.project_root {
876 Some(root) => root.clone(),
877 None => match env::current_dir() {
878 Ok(cwd) => {
879 tracing::warn!(
880 "no Cargo project root detected; falling back to current working directory: {}",
881 cwd.display()
882 );
883 cwd
884 }
885 Err(e) => {
886 return Err(format!(
887 "cargo backend cannot start: no project root detected and current working \
888 directory is unavailable ({e}). Open a folder containing a Cargo.toml and \
889 restart the server."
890 ));
891 }
892 },
893 };
894 let runtime = CargoRuntime {
895 build_folder,
896 ..CargoRuntime::default()
897 };
898 tracing::info!(build_folder = ?runtime.build_folder, "cargo backend initialized");
899 state.backend = Some(BackendRuntime::Cargo { config, runtime });
900 Ok(())
901 }
902
903 async fn publish_cargo_diagnostics(&self) {
905 self.publish_cargo_diagnostics_inner(None).await;
906 }
907
908 pub(crate) async fn publish_cargo_diagnostics_live(&self) {
912 let live_on = {
913 let state = self.state.read().await;
914 matches!(
915 &state.backend,
916 Some(BackendRuntime::Cargo { config, .. }) if config.update_on_insert
917 )
918 };
919 if !live_on {
920 return;
921 }
922 let Some(shadow) = self.ensure_shadow_built().await else {
923 return;
924 };
925 let ctx = LiveCheckContext {
926 shadow_root: shadow.shadow_root().to_path_buf(),
927 shadow_target_dir: shadow.target_dir().to_path_buf(),
928 real_root: shadow.real_root().to_path_buf(),
929 };
930 self.publish_cargo_diagnostics_inner(Some(&ctx)).await;
931 }
932
933 async fn publish_cargo_diagnostics_inner(&self, live: Option<&LiveCheckContext>) {
934 tracing::info!(live = live.is_some(), "starting cargo diagnostics run");
935 let mut guard = self.state.write().await;
936 let project_root = guard.project_root.clone();
937 let related_information_supported = guard.related_information_supported;
938
939 let Some(BackendRuntime::Cargo { config, runtime }) = &mut guard.backend else {
940 return;
941 };
942 let use_related_information = !config
943 .separate_child_diagnostics
944 .unwrap_or(!related_information_supported);
945 let cargo_command = config.command.clone();
946 let mut cargo_env = config.env.clone();
947 let mut cmd_args = config.build_command_args();
948 let publish_mode = config.publish_mode;
949 let clear_diagnostics_on_check = config.clear_diagnostics_on_check;
950 let build_folder = match live {
951 Some(ctx) => {
952 cmd_args.push(format!("--target-dir={}", ctx.shadow_target_dir.display()));
953 let rustflags = format!(
957 "--remap-path-prefix={}={}",
958 ctx.shadow_root.display(),
959 ctx.real_root.display()
960 );
961 if let Some(slot) = cargo_env.iter_mut().find(|(k, _)| k == "RUSTFLAGS") {
963 slot.1.push(' ');
964 slot.1.push_str(&rustflags);
965 } else {
966 cargo_env.push(("RUSTFLAGS".to_string(), rustflags));
967 }
968 ctx.shadow_root.clone()
969 }
970 None => runtime.build_folder.clone(),
971 };
972 runtime.diagnostics_version = runtime.diagnostics_version.wrapping_add(1);
973 runtime.last_run_started = Some(Instant::now());
974 let version = runtime.diagnostics_version;
975 let refresh_interval = config.refresh_interval_seconds;
976
977 let cancel_token = match publish_mode {
978 PublishMode::CancelRunning => {
979 runtime.cancel_token.cancel();
980 runtime.cancel_token = CancellationToken::new();
981 runtime.cancel_token.clone()
982 }
983 PublishMode::QueueIfRunning => match runtime.run_state {
984 CargoRunState::Running | CargoRunState::RunningPending => {
985 runtime.run_state = CargoRunState::RunningPending;
986 tracing::debug!("cargo already running, marking pending");
987 drop(guard);
988 return;
989 }
990 CargoRunState::Idle => {
991 runtime.run_state = CargoRunState::Running;
992 runtime.cancel_token.clone()
993 }
994 },
995 };
996
997 let files_to_clear: Vec<Uri> = if clear_diagnostics_on_check {
1002 runtime.files_with_diags.drain().collect()
1003 } else {
1004 Vec::new()
1005 };
1006
1007 drop(guard);
1008
1009 for file in files_to_clear {
1010 self.client.publish_diagnostics(file, vec![], Some(version)).await;
1011 }
1012
1013 let token = ProgressToken::Number(version);
1014 let progress = self
1015 .client
1016 .progress(token, "checking")
1017 .with_message(format!("cargo {cargo_command}"))
1018 .with_percentage(0)
1019 .begin()
1020 .await;
1021
1022 let (tx, rx) = flume::unbounded();
1023
1024 let cargo_future = Cargo::cargo_diagnostics(
1025 cmd_args,
1026 &cargo_env,
1027 project_root.as_ref(),
1028 &build_folder,
1029 use_related_information,
1030 &progress,
1031 tx,
1032 );
1033
1034 let consumer_client = self.client.clone();
1035 let diagnostic_consumer = async move {
1036 let mut diagnostics_map = HashMap::<Uri, (Vec<Diagnostic>, HashSet<DiagKey>, bool)>::new();
1040
1041 enum AccumulateResult {
1042 Closed,
1043 NewDiagnostic,
1044 Duplicate,
1045 }
1046
1047 fn accumulate_diagnostics(
1048 recv_result: Result<(Uri, Diagnostic), RecvError>,
1049 diagnostics_map: &mut HashMap<Uri, (Vec<Diagnostic>, HashSet<DiagKey>, bool)>,
1050 ) -> AccumulateResult {
1051 let Ok((url, diagnostic)) = recv_result else {
1052 return AccumulateResult::Closed;
1053 };
1054 let (diagnostics, seen, dirty) = diagnostics_map.entry(url).or_default();
1055 if seen.insert(diag_key(&diagnostic)) {
1056 diagnostics.push(diagnostic);
1057 *dirty = true;
1058 AccumulateResult::NewDiagnostic
1059 } else {
1060 AccumulateResult::Duplicate
1061 }
1062 }
1063
1064 if let Some(refresh_interval) = refresh_interval {
1065 let mut first_published = false;
1071 let mut t = std::time::Instant::now();
1072 loop {
1073 let mut got_new = false;
1074 tokio::select! {
1075 result = rx.recv_async() => {
1076 match accumulate_diagnostics(result, &mut diagnostics_map) {
1077 AccumulateResult::Closed => break,
1078 AccumulateResult::NewDiagnostic => got_new = true,
1079 AccumulateResult::Duplicate => {}
1080 }
1081 }
1082 _ = tokio::time::sleep_until(tokio::time::Instant::from_std(t + refresh_interval)) => {}
1083 }
1084
1085 let publish_first = got_new && !first_published;
1086 if publish_first || t.elapsed() >= refresh_interval {
1087 for (url, (diagnostics, _seen, dirty)) in diagnostics_map.iter_mut() {
1088 if *dirty {
1089 consumer_client
1090 .publish_diagnostics(url.clone(), diagnostics.clone(), Some(version))
1091 .await;
1092 *dirty = false;
1093 }
1094 }
1095 if publish_first {
1096 tracing::debug!("first diagnostic published; switching to refresh-interval cadence");
1097 first_published = true;
1098 }
1099 t = std::time::Instant::now();
1100 }
1101 }
1102 } else {
1103 loop {
1104 if matches!(
1105 accumulate_diagnostics(rx.recv_async().await, &mut diagnostics_map),
1106 AccumulateResult::Closed
1107 ) {
1108 break;
1109 }
1110 }
1111 }
1112
1113 diagnostics_map
1114 };
1115
1116 let consumer_handle = tokio::spawn(diagnostic_consumer);
1117
1118 let result = tokio::select! {
1119 result = cargo_future => {
1120 result.map(|_| false)
1121 },
1122 () = cancel_token.cancelled() => {
1123 tracing::info!("cargo run cancelled by newer request");
1124 Ok(true)
1125 }
1126 };
1127
1128 let was_cancelled = match result {
1129 Ok(t) => t,
1130 Err(error) => {
1131 tracing::error!(?error, "error building diagnostics");
1134 progress.finish().await;
1135 let _ = consumer_handle.await;
1136 self.client.log_message(MessageType::ERROR, format!("{error}")).await;
1137 self.client.show_message(MessageType::ERROR, format!("{error}")).await;
1138 return;
1139 }
1140 };
1141
1142 if was_cancelled {
1143 let _ = consumer_handle.await;
1147 progress.finish_with_message("cancelled by user").await;
1148 return;
1149 }
1150
1151 tracing::info!("cargo run finished, collecting diagnostics");
1152
1153 let mut diagnostics = match consumer_handle.await {
1154 Ok(d) => d,
1155 Err(error) => {
1156 tracing::error!(?error, "diagnostics fetching task panicked");
1157 progress.finish().await;
1158 self.client.log_message(MessageType::ERROR, format!("{error}")).await;
1159 self.client.show_message(MessageType::ERROR, format!("{error}")).await;
1160 return;
1161 }
1162 };
1163
1164 let mut state = self.state.write().await;
1165 let Some(BackendRuntime::Cargo {
1166 config,
1167 runtime: cargo_rt,
1168 }) = &mut state.backend
1169 else {
1170 tracing::error!("backend changed during cargo run");
1172 return;
1173 };
1174 let publish_mode = config.publish_mode;
1175
1176 if let PublishMode::CancelRunning = publish_mode
1181 && version != cargo_rt.diagnostics_version
1182 {
1183 tracing::info!(
1184 version,
1185 current = cargo_rt.diagnostics_version,
1186 "skipping stale publish"
1187 );
1188 progress.finish_with_message("superseded by newer run").await;
1189 return;
1190 }
1191
1192 for file in cargo_rt.files_with_diags.drain() {
1193 let _ = diagnostics.entry(file).or_insert((vec![], HashSet::new(), true));
1195 }
1196
1197 let mut num_warnings = 0;
1198 let mut num_errors = 0;
1199 for (uri, (diagnostics, _seen, is_dirty)) in diagnostics.into_iter() {
1200 tracing::debug!(uri = uri.to_string(), "sent {} cargo diagnostics", diagnostics.len());
1201 for diagnostic in &diagnostics {
1202 match diagnostic.severity {
1203 Some(DiagnosticSeverity::ERROR) => num_errors += 1,
1204 Some(DiagnosticSeverity::WARNING) => num_warnings += 1,
1205 Some(_) | None => {}
1206 }
1207 }
1208 if !diagnostics.is_empty() {
1209 let _ = cargo_rt.files_with_diags.insert(uri.clone());
1210 }
1211 if is_dirty {
1212 self.client.publish_diagnostics(uri, diagnostics, Some(version)).await;
1213 }
1214 }
1215 let message = format!("done, errors: {num_errors}, warnings: {num_warnings}");
1216 progress.finish_with_message(message).await;
1217
1218 if let PublishMode::QueueIfRunning = publish_mode {
1219 match cargo_rt.run_state {
1220 CargoRunState::RunningPending => {
1221 cargo_rt.run_state = CargoRunState::Idle;
1222 drop(state);
1223 tracing::info!("re-running cargo after queued request");
1224 Box::pin(self.publish_cargo_diagnostics()).await;
1225 }
1226 _ => {
1227 cargo_rt.run_state = CargoRunState::Idle;
1228 drop(state);
1229 }
1230 }
1231 }
1232 }
1233
1234 pub(crate) async fn ensure_shadow_built(&self) -> Option<ShadowWorkspace> {
1238 {
1240 let state = self.state.read().await;
1241 if let Some(BackendRuntime::Cargo { runtime, .. }) = &state.backend
1242 && let Some(shadow) = &runtime.shadow
1243 {
1244 return Some(shadow.clone());
1245 }
1246 }
1247
1248 let project_root = {
1249 let state = self.state.read().await;
1250 state.project_root.clone()
1251 };
1252 let Some(root) = project_root else {
1253 tracing::warn!("updateOnInsert: no project root; cannot build live shadow");
1254 return None;
1255 };
1256
1257 tracing::info!(root = ?root, "updateOnInsert: building live shadow workspace");
1258 self.client
1263 .show_message(
1264 MessageType::INFO,
1265 "bacon-ls: building live diagnostics shadow workspace (first run only)…",
1266 )
1267 .await;
1268 let shadow = match ShadowWorkspace::build(root).await {
1269 Ok(s) => s,
1270 Err(e) => {
1271 tracing::error!("updateOnInsert: failed to build shadow: {e}");
1272 self.client
1273 .show_message(
1274 MessageType::ERROR,
1275 format!("bacon-ls: failed to build live shadow workspace: {e}"),
1276 )
1277 .await;
1278 return None;
1279 }
1280 };
1281
1282 let mut state = self.state.write().await;
1285 if let Some(BackendRuntime::Cargo { runtime, .. }) = &mut state.backend {
1286 runtime.shadow = Some(shadow.clone());
1287 }
1288 drop(state);
1289 self.client
1292 .log_message(
1293 MessageType::INFO,
1294 "bacon-ls: live diagnostics shadow ready; subsequent edits will be checked as you type.",
1295 )
1296 .await;
1297 Some(shadow)
1298 }
1299
1300 pub(crate) async fn live_update_dirty(&self, uri: Uri, content: String) {
1304 let Some(real_path_cow) = uri.to_file_path() else {
1305 tracing::warn!(uri = uri.as_str(), "updateOnInsert: did_change uri is not a file path");
1306 return;
1307 };
1308 let real_path = real_path_cow.into_owned();
1309
1310 let Some(shadow) = self.ensure_shadow_built().await else {
1311 tracing::warn!("updateOnInsert: shadow workspace not available; skipping live update");
1312 return;
1313 };
1314 if let Err(e) = shadow.write_dirty(&real_path, &content).await {
1315 tracing::warn!(path = ?real_path, ?e, "updateOnInsert: shadow write failed (file outside workspace?)");
1316 return;
1317 }
1318
1319 let debounce = {
1320 let mut state = self.state.write().await;
1321 let Some(BackendRuntime::Cargo { config, runtime }) = &mut state.backend else {
1322 return;
1323 };
1324 runtime.dirty_files.insert(uri.clone());
1325 config.update_on_insert_debounce
1326 };
1327
1328 tracing::info!(
1329 uri = uri.as_str(),
1330 debounce_ms = debounce.as_millis() as u64,
1331 "updateOnInsert: shadow updated, scheduling live cargo run"
1332 );
1333 self.schedule_live_run(debounce).await;
1334 }
1335
1336 pub(crate) async fn schedule_live_run(&self, delay: Duration) {
1340 let mut state = self.state.write().await;
1341 let Some(BackendRuntime::Cargo { runtime, .. }) = &mut state.backend else {
1342 return;
1343 };
1344 if let Some(prev) = runtime.live_debounce.take() {
1345 prev.abort();
1346 }
1347 let bacon = self.clone();
1348 runtime.live_debounce = Some(tokio::spawn(async move {
1349 tokio::time::sleep(delay).await;
1350 bacon.publish_cargo_diagnostics_live().await;
1351 }));
1352 }
1353
1354 pub(crate) async fn cancel_live_debounce(&self) {
1358 let mut state = self.state.write().await;
1359 if let Some(BackendRuntime::Cargo { runtime, .. }) = &mut state.backend
1360 && let Some(handle) = runtime.live_debounce.take()
1361 {
1362 handle.abort();
1363 }
1364 }
1365
1366 pub(crate) async fn restore_shadow_link_if_dirty(&self, uri: &Uri) {
1369 let (shadow, real_path) = {
1370 let mut state = self.state.write().await;
1371 let Some(BackendRuntime::Cargo { runtime, .. }) = &mut state.backend else {
1372 return;
1373 };
1374 if !runtime.dirty_files.remove(uri) {
1375 return;
1376 }
1377 let Some(shadow) = runtime.shadow.clone() else {
1378 return;
1379 };
1380 let Some(path_cow) = uri.to_file_path() else {
1381 return;
1382 };
1383 (shadow, path_cow.into_owned())
1384 };
1385 if let Err(e) = shadow.restore_link(&real_path).await {
1386 tracing::warn!(path = ?real_path, ?e, "updateOnInsert: failed to restore shadow link");
1387 }
1388 }
1389
1390 async fn publish_bacon_diagnostics(&self, uri: &Uri) {
1391 let mut guard = self.state.write().await;
1392 let workspace_folders = guard.workspace_folders.clone();
1393
1394 let Some(BackendRuntime::Bacon { config, runtime }) = &mut guard.backend else {
1395 return;
1396 };
1397 tracing::info!(uri = uri.to_string(), "publish bacon diagnostics");
1398 let locations_file_name = config.locations_file.clone();
1399 runtime.diagnostics_version = runtime.diagnostics_version.wrapping_add(1);
1400 let version = runtime.diagnostics_version;
1401 drop(guard);
1402 Bacon::publish_diagnostics(
1403 &self.client,
1404 uri,
1405 &locations_file_name,
1406 workspace_folders.as_deref(),
1407 version,
1408 )
1409 .await;
1410 }
1411
1412 async fn synchronize_diagnostics(state: Arc<RwLock<State>>, client: Arc<Client>) {
1413 Bacon::synchronize_diagnostics(state, client).await;
1414 }
1415}
1416
1417#[cfg(test)]
1418mod tests {
1419 use super::*;
1420
1421 #[test]
1422 fn test_can_configure_tracing() {
1423 let tmp = tempfile::tempdir().expect("tempdir");
1427 let log_path = tmp.path().join("bacon-ls.log");
1428 BaconLs::configure_tracing(Some("info".to_string()), Some(&log_path));
1429 }
1430
1431 #[test]
1432 fn test_path_to_file_uri_plain_ascii() {
1433 let uri = path_to_file_uri("/home/me/src/lib.rs");
1434 assert_eq!(uri, "file:///home/me/src/lib.rs");
1435 let parsed = uri.parse::<Uri>().expect("must parse as Uri");
1436 assert_eq!(parsed.path().as_str(), "/home/me/src/lib.rs");
1437 }
1438
1439 #[test]
1440 fn test_path_to_file_uri_escapes_space_and_hash_and_percent() {
1441 let uri = path_to_file_uri("/home/me/My Projects/tests#1/file%.rs");
1442 assert_eq!(uri, "file:///home/me/My%20Projects/tests%231/file%25.rs");
1443 let parsed = uri.parse::<Uri>().expect("must parse as Uri");
1444 assert_eq!(parsed.path().as_str(), "/home/me/My%20Projects/tests%231/file%25.rs");
1447 }
1448
1449 #[test]
1450 fn test_path_to_file_uri_preserves_path_separators() {
1451 let uri = path_to_file_uri("/a/b/c");
1454 assert_eq!(uri, "file:///a/b/c");
1455 }
1456
1457 #[test]
1458 fn test_path_to_file_uri_relative_path_preserves_segments() {
1459 let uri = path_to_file_uri("src/lib.rs");
1463 assert_eq!(uri, "file://src/lib.rs");
1464 let parsed = uri.parse::<Uri>().expect("must parse as Uri");
1465 assert_eq!(
1466 parsed.authority().map(|a| a.host().to_string()),
1467 Some("src".to_string())
1468 );
1469 assert_eq!(parsed.path().as_str(), "/lib.rs");
1470 }
1471
1472 #[test]
1473 fn test_cancel_mode_replaces_token() {
1474 let original = CancellationToken::new();
1475 let token = original.clone();
1476 token.cancel();
1477 assert!(original.is_cancelled());
1478 let new_token = CancellationToken::new();
1479 assert!(!new_token.is_cancelled());
1480 }
1481
1482 #[test]
1483 fn test_detect_backend_explicit_cargo() {
1484 let values: Map<String, Value> = serde_json::from_str(r#"{"backend": "cargo"}"#).unwrap();
1485 assert_eq!(BaconLs::detect_backend(&values).unwrap(), BackendChoice::Cargo);
1486 }
1487
1488 #[test]
1489 fn test_detect_backend_explicit_bacon() {
1490 let values: Map<String, Value> = serde_json::from_str(r#"{"backend": "bacon"}"#).unwrap();
1491 assert_eq!(BaconLs::detect_backend(&values).unwrap(), BackendChoice::Bacon);
1492 }
1493
1494 #[test]
1495 fn test_detect_backend_invalid_value() {
1496 let values: Map<String, Value> = serde_json::from_str(r#"{"backend": "invalid"}"#).unwrap();
1497 assert!(BaconLs::detect_backend(&values).is_err());
1498 }
1499
1500 #[test]
1501 fn test_detect_backend_infer_from_cargo_key() {
1502 let values: Map<String, Value> = serde_json::from_str(r#"{"cargo": {"command": "check"}}"#).unwrap();
1503 assert_eq!(BaconLs::detect_backend(&values).unwrap(), BackendChoice::Cargo);
1504 }
1505
1506 #[test]
1507 fn test_detect_backend_infer_from_bacon_key() {
1508 let values: Map<String, Value> =
1509 serde_json::from_str(r#"{"bacon": {"locationsFile": ".bacon-locations"}}"#).unwrap();
1510 assert_eq!(BaconLs::detect_backend(&values).unwrap(), BackendChoice::Bacon);
1511 }
1512
1513 #[test]
1514 fn test_detect_backend_both_keys_error() {
1515 let values: Map<String, Value> = serde_json::from_str(r#"{"cargo": {}, "bacon": {}}"#).unwrap();
1516 assert!(BaconLs::detect_backend(&values).is_err());
1517 }
1518
1519 #[test]
1520 fn test_detect_backend_no_keys_defaults_to_cargo() {
1521 let values: Map<String, Value> = serde_json::from_str(r#"{}"#).unwrap();
1522 assert_eq!(BaconLs::detect_backend(&values).unwrap(), BackendChoice::Cargo);
1523 }
1524
1525 #[test]
1526 fn test_detect_backend_explicit_overrides_keys() {
1527 let values: Map<String, Value> = serde_json::from_str(r#"{"backend": "cargo", "bacon": {}}"#).unwrap();
1528 assert_eq!(BaconLs::detect_backend(&values).unwrap(), BackendChoice::Cargo);
1529 }
1530
1531 #[test]
1532 fn test_cargo_options_build_args_default() {
1533 let args = CargoOptions::default().build_command_args();
1534 assert_eq!(args, vec!["check", "--message-format=json-diagnostic-rendered-ansi"]);
1535 }
1536
1537 #[test]
1538 fn test_cargo_options_build_args_with_features() {
1539 let opts = CargoOptions {
1540 features: vec!["a".into(), "b".into(), "c".into()],
1541 ..CargoOptions::default()
1542 };
1543 let args = opts.build_command_args();
1544 assert_eq!(
1545 args,
1546 vec![
1547 "check",
1548 "--message-format=json-diagnostic-rendered-ansi",
1549 "--features",
1550 "a,b,c"
1551 ]
1552 );
1553 }
1554
1555 #[test]
1556 fn test_cargo_options_build_args_single_feature() {
1557 let opts = CargoOptions {
1558 features: vec!["only".into()],
1559 ..CargoOptions::default()
1560 };
1561 let args = opts.build_command_args();
1562 assert_eq!(
1563 args,
1564 vec![
1565 "check",
1566 "--message-format=json-diagnostic-rendered-ansi",
1567 "--features",
1568 "only"
1569 ]
1570 );
1571 }
1572
1573 #[test]
1574 fn test_cargo_options_build_args_with_package_and_extras() {
1575 let opts = CargoOptions {
1576 command: "clippy".into(),
1577 package: Some("my-crate".into()),
1578 extra_command_args: vec!["--workspace".into(), "--all-targets".into()],
1579 ..CargoOptions::default()
1580 };
1581 let args = opts.build_command_args();
1582 assert_eq!(
1583 args,
1584 vec![
1585 "clippy",
1586 "--message-format=json-diagnostic-rendered-ansi",
1587 "-p",
1588 "my-crate",
1589 "--workspace",
1590 "--all-targets",
1591 ]
1592 );
1593 }
1594
1595 #[test]
1596 fn test_cargo_options_update_from_json_full_roundtrip() {
1597 let mut opts = CargoOptions::default();
1598 let json = serde_json::json!({
1599 "command": "clippy",
1600 "features": ["a", "b"],
1601 "package": "pkg",
1602 "extraArgs": ["--workspace"],
1603 "env": {"RUST_LOG": "trace"},
1604 "cancelRunning": false,
1605 "refreshIntervalSeconds": 10,
1606 "separateChildDiagnostics": true,
1607 "checkOnSave": false,
1608 "clearDiagnosticsOnCheck": true,
1609 "updateOnInsertDebounceMillis": 250,
1610 });
1611 let obj = json.as_object().unwrap();
1612 opts.update_from_json_obj(obj).expect("should parse");
1613 assert_eq!(opts.command, "clippy");
1614 assert_eq!(opts.features, vec!["a".to_string(), "b".to_string()]);
1615 assert_eq!(opts.package.as_deref(), Some("pkg"));
1616 assert_eq!(opts.extra_command_args, vec!["--workspace".to_string()]);
1617 assert_eq!(opts.env, vec![("RUST_LOG".into(), "trace".into())]);
1618 assert!(matches!(opts.publish_mode, PublishMode::QueueIfRunning));
1619 assert_eq!(opts.refresh_interval_seconds, Some(Duration::from_secs(10)));
1620 assert_eq!(opts.separate_child_diagnostics, Some(true));
1621 assert!(!opts.check_on_save);
1622 assert!(opts.clear_diagnostics_on_check);
1623 assert_eq!(opts.update_on_insert_debounce, Duration::from_millis(250));
1624 }
1625
1626 #[test]
1627 fn test_cargo_options_update_on_insert_defaults_off() {
1628 let opts = CargoOptions::default();
1629 assert!(!opts.update_on_insert);
1630 assert_eq!(opts.update_on_insert_debounce, Duration::from_millis(500));
1631 }
1632
1633 #[test]
1634 fn test_cargo_options_update_on_insert_debounce_rejects_negative() {
1635 let mut opts = CargoOptions::default();
1636 let json = serde_json::json!({"updateOnInsertDebounceMillis": -50});
1637 assert!(opts.update_from_json_obj(json.as_object().unwrap()).is_err());
1638 }
1639
1640 #[test]
1641 fn test_cargo_options_update_from_json_refresh_null_means_no_partial() {
1642 let mut opts = CargoOptions::default();
1643 let json = serde_json::json!({"refreshIntervalSeconds": null});
1644 opts.update_from_json_obj(json.as_object().unwrap()).unwrap();
1645 assert_eq!(opts.refresh_interval_seconds, None);
1646 }
1647
1648 #[test]
1649 fn test_cargo_options_update_from_json_refresh_negative_means_no_partial() {
1650 let mut opts = CargoOptions::default();
1651 let json = serde_json::json!({"refreshIntervalSeconds": -1});
1652 opts.update_from_json_obj(json.as_object().unwrap()).unwrap();
1653 assert_eq!(opts.refresh_interval_seconds, None);
1654 }
1655
1656 #[test]
1657 fn test_cargo_options_update_from_json_rejects_wrong_type() {
1658 let mut opts = CargoOptions::default();
1659 let json = serde_json::json!({"command": 42});
1660 assert!(opts.update_from_json_obj(json.as_object().unwrap()).is_err());
1661 }
1662
1663 #[test]
1664 fn test_cargo_options_update_from_json_partial_leaves_others_unchanged() {
1665 let mut opts = CargoOptions {
1666 command: "clippy".into(),
1667 ..CargoOptions::default()
1668 };
1669 let json = serde_json::json!({"checkOnSave": false});
1670 opts.update_from_json_obj(json.as_object().unwrap()).unwrap();
1671 assert_eq!(opts.command, "clippy");
1672 assert!(!opts.check_on_save);
1673 }
1674
1675 #[test]
1676 fn test_cargo_options_reset_restores_defaults() {
1677 let mut opts = CargoOptions {
1678 command: "clippy".into(),
1679 features: vec!["foo".into()],
1680 check_on_save: false,
1681 ..CargoOptions::default()
1682 };
1683 opts.reset();
1684 let defaults = CargoOptions::default();
1685 assert_eq!(opts.command, defaults.command);
1686 assert_eq!(opts.features, defaults.features);
1687 assert_eq!(opts.check_on_save, defaults.check_on_save);
1688 }
1689
1690 #[test]
1691 fn test_bacon_options_update_from_json_full_roundtrip() {
1692 let mut opts = BaconOptions::default();
1693 let json = serde_json::json!({
1694 "locationsFile": "custom.locations",
1695 "runInBackground": false,
1696 "runInBackgroundCommand": "/usr/local/bin/bacon",
1697 "runInBackgroundCommandArguments": "--headless -j custom",
1698 "validatePreferences": false,
1699 "createPreferencesFile": false,
1700 "synchronizeAllOpenFilesWaitMillis": 500,
1701 "updateOnSave": false,
1702 "updateOnSaveWaitMillis": 250,
1703 });
1704 opts.update_from_json_obj(json.as_object().unwrap()).unwrap();
1705 assert_eq!(opts.locations_file, "custom.locations");
1706 assert!(!opts.run_in_background);
1707 assert_eq!(opts.run_in_background_command, "/usr/local/bin/bacon");
1708 assert_eq!(opts.run_in_background_command_args, "--headless -j custom");
1709 assert!(!opts.validate_preferences);
1710 assert!(!opts.create_preferences_file);
1711 assert_eq!(opts.synchronize_all_open_files_wait, Duration::from_millis(500));
1712 assert!(!opts.update_on_save);
1713 assert_eq!(opts.update_on_save_wait, Duration::from_millis(250));
1714 }
1715
1716 #[test]
1717 fn test_bacon_options_update_from_json_rejects_wrong_type() {
1718 let mut opts = BaconOptions::default();
1719 let json = serde_json::json!({"runInBackground": "yes"});
1720 assert!(opts.update_from_json_obj(json.as_object().unwrap()).is_err());
1721 }
1722
1723 #[test]
1724 fn test_bacon_options_reset_restores_defaults() {
1725 let mut opts = BaconOptions {
1726 run_in_background: false,
1727 locations_file: "foo".into(),
1728 ..BaconOptions::default()
1729 };
1730 opts.reset();
1731 let defaults = BaconOptions::default();
1732 assert_eq!(opts.run_in_background, defaults.run_in_background);
1733 assert_eq!(opts.locations_file, defaults.locations_file);
1734 }
1735
1736 #[test]
1737 fn test_correction_from_single_empty_is_remove() {
1738 let range = Range::default();
1739 let c = Correction::from_single(range, "");
1740 assert_eq!(c.label, "Remove");
1741 assert_eq!(c.edits.len(), 1);
1742 assert_eq!(c.edits[0].new_text, "");
1743 }
1744
1745 #[test]
1746 fn test_correction_from_single_nonempty_is_replace() {
1747 let range = Range::default();
1748 let c = Correction::from_single(range, "foo");
1749 assert_eq!(c.label, "Replace with: foo");
1750 assert_eq!(c.edits.len(), 1);
1751 }
1752
1753 #[test]
1754 fn test_correction_from_multi_all_empty_is_remove() {
1755 let edits = vec![
1756 CorrectionEdit {
1757 range: Range::default(),
1758 new_text: "".into(),
1759 },
1760 CorrectionEdit {
1761 range: Range::default(),
1762 new_text: "".into(),
1763 },
1764 ];
1765 let c = Correction::from_multi(edits);
1766 assert_eq!(c.label, "Remove");
1767 assert_eq!(c.edits.len(), 2);
1768 }
1769
1770 #[test]
1771 fn test_correction_from_multi_labels_by_first_nonempty() {
1772 let edits = vec![
1773 CorrectionEdit {
1774 range: Range::default(),
1775 new_text: "".into(),
1776 },
1777 CorrectionEdit {
1778 range: Range::default(),
1779 new_text: "new".into(),
1780 },
1781 ];
1782 let c = Correction::from_multi(edits);
1783 assert_eq!(c.label, "Replace with: new");
1784 }
1785
1786 #[test]
1787 fn test_severity_tag_distinguishes_levels() {
1788 assert_eq!(severity_tag(None), 0);
1789 assert_eq!(severity_tag(Some(DiagnosticSeverity::ERROR)), 1);
1790 assert_eq!(severity_tag(Some(DiagnosticSeverity::WARNING)), 2);
1791 assert_eq!(severity_tag(Some(DiagnosticSeverity::INFORMATION)), 3);
1792 assert_eq!(severity_tag(Some(DiagnosticSeverity::HINT)), 4);
1793 let tags = [
1796 severity_tag(Some(DiagnosticSeverity::ERROR)),
1797 severity_tag(Some(DiagnosticSeverity::WARNING)),
1798 severity_tag(Some(DiagnosticSeverity::INFORMATION)),
1799 severity_tag(Some(DiagnosticSeverity::HINT)),
1800 ];
1801 let unique: HashSet<_> = tags.iter().collect();
1802 assert_eq!(unique.len(), tags.len());
1803 }
1804
1805 #[test]
1806 fn test_diag_key_collides_for_equal_diagnostics() {
1807 let a = Diagnostic {
1808 range: Range::default(),
1809 severity: Some(DiagnosticSeverity::ERROR),
1810 message: "hi".into(),
1811 ..Diagnostic::default()
1812 };
1813 let b = a.clone();
1814 assert_eq!(diag_key(&a), diag_key(&b));
1815 }
1816
1817 #[test]
1818 fn test_diag_key_differs_when_message_differs() {
1819 let mut a = Diagnostic {
1820 range: Range::default(),
1821 severity: Some(DiagnosticSeverity::ERROR),
1822 message: "first".into(),
1823 ..Diagnostic::default()
1824 };
1825 let b = a.clone();
1826 a.message = "second".into();
1827 assert_ne!(diag_key(&a), diag_key(&b));
1828 }
1829
1830 #[test]
1831 fn test_path_to_file_uri_empty_path() {
1832 assert_eq!(path_to_file_uri(""), "file://");
1835 }
1836
1837 #[test]
1838 fn test_correction_from_single_label_replaces_with_text() {
1839 let c = Correction::from_single(Range::default(), "x");
1840 assert_eq!(c.label, "Replace with: x");
1841 assert_eq!(c.edits.len(), 1);
1842 assert_eq!(c.edits[0].new_text, "x");
1843 }
1844
1845 #[test]
1846 fn test_correction_from_multi_empty_edits_is_remove() {
1847 let c = Correction::from_multi(vec![]);
1848 assert_eq!(c.label, "Remove");
1849 assert!(c.edits.is_empty());
1850 }
1851
1852 #[test]
1853 fn test_cargo_options_env_roundtrip_preserves_order_in_serde_iteration() {
1854 let mut opts = CargoOptions::default();
1857 let json = serde_json::json!({
1858 "env": {"A": "1", "B": "2", "C": "3"}
1859 });
1860 opts.update_from_json_obj(json.as_object().unwrap()).unwrap();
1861 assert_eq!(opts.env.len(), 3);
1862 let keys: Vec<_> = opts.env.iter().map(|(k, _)| k.as_str()).collect();
1863 assert_eq!(keys, vec!["A", "B", "C"]);
1864 }
1865
1866 #[test]
1867 fn test_cargo_options_update_rejects_non_object_env() {
1868 let mut opts = CargoOptions::default();
1869 let json = serde_json::json!({"env": ["A=1"]});
1870 assert!(opts.update_from_json_obj(json.as_object().unwrap()).is_err());
1871 }
1872
1873 #[test]
1874 fn test_cargo_options_update_rejects_non_string_env_value() {
1875 let mut opts = CargoOptions::default();
1876 let json = serde_json::json!({"env": {"A": 1}});
1877 assert!(opts.update_from_json_obj(json.as_object().unwrap()).is_err());
1878 }
1879
1880 #[test]
1881 fn test_cargo_options_update_rejects_non_string_feature_item() {
1882 let mut opts = CargoOptions::default();
1883 let json = serde_json::json!({"features": ["a", 2, "c"]});
1884 assert!(opts.update_from_json_obj(json.as_object().unwrap()).is_err());
1885 }
1886
1887 #[test]
1888 fn test_cargo_options_publish_mode_toggle_via_cancel_running() {
1889 let mut opts = CargoOptions::default();
1890 assert!(matches!(opts.publish_mode, PublishMode::CancelRunning));
1892 opts.update_from_json_obj(serde_json::json!({"cancelRunning": false}).as_object().unwrap())
1893 .unwrap();
1894 assert!(matches!(opts.publish_mode, PublishMode::QueueIfRunning));
1895 opts.update_from_json_obj(serde_json::json!({"cancelRunning": true}).as_object().unwrap())
1896 .unwrap();
1897 assert!(matches!(opts.publish_mode, PublishMode::CancelRunning));
1898 }
1899
1900 #[test]
1901 fn test_cargo_options_separate_child_diagnostics_can_unset() {
1902 let mut opts = CargoOptions {
1903 separate_child_diagnostics: Some(true),
1904 ..CargoOptions::default()
1905 };
1906 opts.update_from_json_obj(
1909 serde_json::json!({"separateChildDiagnostics": null})
1910 .as_object()
1911 .unwrap(),
1912 )
1913 .unwrap();
1914 assert_eq!(opts.separate_child_diagnostics, None);
1915 }
1916
1917 #[tokio::test]
1918 async fn test_find_git_root_directory_returns_none_outside_git() {
1919 let tmp = tempfile::TempDir::new().unwrap();
1920 let root = BaconLs::find_git_root_directory(tmp.path()).await;
1921 assert_eq!(root, None);
1922 }
1923
1924 #[tokio::test]
1925 async fn test_find_git_root_directory_finds_top_of_repo() {
1926 let crate_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
1929 let src = crate_root.join("src");
1930 let from_subdir = BaconLs::find_git_root_directory(&src).await;
1931 assert!(from_subdir.is_some(), "src/ is inside a git repo");
1932 let from_root = BaconLs::find_git_root_directory(crate_root).await.unwrap();
1933 assert_eq!(from_subdir.unwrap(), from_root);
1935 }
1936
1937 #[test]
1938 fn test_init_cargo_backend_uses_existing_project_root() {
1939 let tmp = tempfile::TempDir::new().unwrap();
1940 let root = tmp.path().to_path_buf();
1941 let mut state = State {
1942 project_root: Some(root.clone()),
1943 ..State::default()
1944 };
1945 let lock = RwLock::new(std::mem::take(&mut state));
1948 let mut guard = lock.try_write().unwrap();
1949 BaconLs::init_cargo_backend(&mut guard, CargoOptions::default())
1950 .expect("init should succeed with explicit project root");
1951 match &guard.backend {
1952 Some(BackendRuntime::Cargo { runtime, .. }) => {
1953 assert_eq!(runtime.build_folder, root);
1954 assert_eq!(runtime.run_state, CargoRunState::Idle);
1955 assert_eq!(runtime.diagnostics_version, 0);
1956 }
1957 other => panic!("expected Cargo backend, got {other:?}"),
1958 }
1959 }
1960
1961 #[test]
1962 fn test_init_cargo_backend_falls_back_to_cwd_when_no_project_root() {
1963 let mut state = State::default();
1964 let lock = RwLock::new(std::mem::take(&mut state));
1965 let mut guard = lock.try_write().unwrap();
1966 BaconLs::init_cargo_backend(&mut guard, CargoOptions::default())
1967 .expect("init should fall back to CWD when project root is unset");
1968 match &guard.backend {
1969 Some(BackendRuntime::Cargo { runtime, .. }) => {
1970 let cwd = std::env::current_dir().unwrap();
1971 assert_eq!(runtime.build_folder, cwd, "should fall back to CWD");
1972 }
1973 other => panic!("expected Cargo backend, got {other:?}"),
1974 }
1975 }
1976
1977 #[test]
1978 fn test_cargo_options_build_args_with_env_does_not_leak_into_args() {
1979 let opts = CargoOptions {
1981 env: vec![("A".into(), "1".into())],
1982 ..CargoOptions::default()
1983 };
1984 let args = opts.build_command_args();
1985 assert!(args.iter().all(|a| !a.contains("A=1") && !a.contains("=1")));
1986 }
1987}