Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion crates/ark/src/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,5 @@ pub(crate) use _log;
pub(crate) use log_error;
pub(crate) use log_info;
pub(crate) use log_warn;
pub(crate) use main_loop::diagnostics_refresh_all;
pub(crate) use main_loop::publish_diagnostics;
pub(crate) use main_loop::spawn_blocking;
32 changes: 21 additions & 11 deletions crates/ark/src/lsp/main_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,8 +388,9 @@ impl GlobalState {
// resolved definitions), so any handler that writes to oak invalidates
// them. Rather than have each write site remember to refresh, we watch
// the oak revision across the whole tick: if a handler advanced it,
// refresh centrally. Config and console state live outside oak, so
// handlers that mutate those still refresh explicitly.
// refresh centrally. Config and console state live outside oak, so the
// handlers that mutate those advance the revision synthetically (see
// `WorldState::bump_revision`) to route through this same path.
let old_revision = salsa::plumbing::current_revision(&self.world.db);

match event {
Expand Down Expand Up @@ -1031,15 +1032,12 @@ static DIAGNOSTICS_QUEUE: LazyLock<tokio::sync::mpsc::UnboundedSender<RefreshDia
/// Tasks are batched and deduplicated per URL (only the last task per URL is
/// processed), so stale-version diagnostics get superseded within a batch.
///
/// Batches triggered by an oak write can't publish out of order. Each pass
/// holds a db snapshot, and a salsa write blocks until all snapshots drop, so
/// by the time the write completes and the newer batch is enqueued, any older
/// pass has either unwound with `Cancelled` or already produced its result.
///
/// FIXME: Batches triggered without an oak write (console inputs, diagnostics
/// config) have no such barrier. An older pass can run concurrently with the
/// newer one and publish last, leaving diagnostics computed from the older
/// console scopes or config until the next refresh.
/// Batches can't publish out of order. Each pass holds a db snapshot, and a
/// salsa write blocks until all snapshots drop, so by the time the write
/// completes and the newer batch is enqueued, any older pass has either unwound
/// with `Cancelled` or already produced its result. Changes to state that lives
/// outside oak (console inputs, diagnostics config) get the same barrier by
/// advancing the revision synthetically (see `WorldState::bump_revision`).
async fn process_diagnostics_queue(mut rx: mpsc::UnboundedReceiver<RefreshDiagnosticsTask>) {
while let Some(task) = rx.recv().await {
let mut batch = vec![task];
Expand Down Expand Up @@ -1252,4 +1250,16 @@ mod tests {
let after = salsa::plumbing::current_revision(&state.db);
assert_ne!(before, after);
}

/// Console-scope and config changes live outside oak, so they lean on
/// `bump_revision` to advance the revision and trigger the same central
/// refresh. This pins that the synthetic write actually moves the revision.
#[test]
fn test_bump_revision_advances_revision() {
let mut state = WorldState::default();
let before = salsa::plumbing::current_revision(&state.db);
state.bump_revision();
let after = salsa::plumbing::current_revision(&state.db);
assert_ne!(before, after);
}
}
11 changes: 11 additions & 0 deletions crates/ark/src/lsp/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use anyhow::anyhow;
use oak_db::File;
use oak_db::OakDatabase;
use oak_semantic::library::Library;
use salsa::Database;
use url::Url;

use crate::lsp::config::LspConfig;
Expand Down Expand Up @@ -120,6 +121,16 @@ impl WorldState {
workspace: Workspace::default(),
}
}
/// Advance the oak revision without changing any oak input.
///
/// Currently used for state that lives on `WorldState` but not in the Oak
/// DB (e.g. console scopes and the diagnostics config). The revision bump
/// invalidates in-flight background workers (e.g. diagnostics), and
/// triggers a diagnostic refresh.
pub(crate) fn bump_revision(&mut self) {
self.db.synthetic_write(salsa::Durability::LOW);
}

pub(crate) fn open_file_mut(&mut self, uri: &Url) -> anyhow::Result<&mut OpenFile> {
let key = FilePath::from_url(uri);
if let Some(open_file) = self.open_files.get_mut(&key) {
Expand Down
10 changes: 6 additions & 4 deletions crates/ark/src/lsp/state_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,10 +506,11 @@ async fn update_config(
}
}

// Refresh diagnostics if the configuration changed
// Refresh diagnostics if the configuration changed. The config lives
// outside Oak so we bump the revision manually.
if state.config.diagnostics != diagnostics_config {
tracing::info!("Refreshing diagnostics after configuration changed");
lsp::main_loop::diagnostics_refresh_all(state);
state.bump_revision();
}

Ok(())
Expand All @@ -526,8 +527,9 @@ pub(crate) fn did_change_console_inputs(
// We currently rely on global console scopes for diagnostics, in particular
// during package development in conjunction with `devtools::load_all()`.
// Ideally diagnostics would not rely on these though, and we wouldn't need
// to refresh from here.
lsp::diagnostics_refresh_all(state);
// to refresh from here. The scopes live outside oak so we bump the revision
// manually.
state.bump_revision();

Ok(())
}
Expand Down
25 changes: 25 additions & 0 deletions crates/ark/src/lsp/tests/state_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ use crate::lsp::main_loop::LspState;
use crate::lsp::main_loop::TokioUnboundedSender;
use crate::lsp::sources::SourceScheduler;
use crate::lsp::state::WorldState;
use crate::lsp::state_handlers::did_change_console_inputs;
use crate::lsp::state_handlers::did_close;
use crate::lsp::state_handlers::effective_workspace_uris;
use crate::lsp::state_handlers::ConsoleInputs;

/// Local sync wrappers around the async-shaped scheduler API. Tests
/// don't need the timing flexibility, so each operation kicks off
Expand Down Expand Up @@ -661,3 +663,26 @@ fn test_did_close_releases_orphan_file_to_stale() {
AuxiliaryEvent::PublishDiagnostics(u, diags, _) if u == url && diags.is_empty()
));
}

/// A console-inputs push carries no oak write, but diagnostics read the console
/// scopes it updates. The handler advances the oak revision so the main loop's
/// central refresh (and its snapshot barrier) fires. Pin that: reverting to a
/// direct refresh that doesn't bump the revision would silently stop the
/// central refresh and let a stale-scope pass publish last.
#[test]
fn test_console_inputs_advance_revision() {
let mut state = WorldState::default();
let before = salsa::plumbing::current_revision(&state.db);

did_change_console_inputs(
ConsoleInputs {
console_scopes: vec![vec!["foo".to_string()]],
installed_packages: vec![],
},
&mut state,
)
.unwrap();

let after = salsa::plumbing::current_revision(&state.db);
assert_ne!(before, after);
}
Loading