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