Synced from monorepo Changes: - Temporarily disable session share link creation in the TUI - Do not approve plan on empty Enter from the revise prompt - Expose chat product Skills via ACP available_commands_update - Return immediately from a blocking wait on an already-completed ACP task - Split headless pager module for clearer structure - Stop git worktree prune from removing user registrations on resume - Use compaction sampler tokenizer for item token counts - Opt-in extra root CAs via GROK_EXTRA_CA_BUNDLE - Cancel all session subagents when the user stops - Let the session persistence actor exit when its session ends - Make fullscreen terminal resize much cheaper on long sessions - Report honestly from kill_task when an ACP task does not exist - Hide /usage for external-auth deployments - Forward the history-load trailer’s computer_reason to the client - Remove ineffective no-op tool reminder - Declare slash-command screen-mode support in one place - Keep settings enum picker on the committed value until Enter - Reap a PTY’s full process tree - Stream tool calls from headless mode over ACP - Bridge gateway task lifecycle to ACP for chat session background tasks - Don’t warn about truncated history on a suppressed replay - Fit full-replace summarizer input and recover on context-length errors - Stop dropping agents over an unrecognized frontmatter color - Add /undo as a slash alias for /rewind - Harden sleep/wake token-refresh paths against forced re-login - Add session/list ACP method - Give each sampling backend its own conversion module - Treat an unenrolled child process as a lint error - Suppress the cancelled marker on send-now wake turns - Stop tearing down Roslyn on every edit, and read C# diagnostics Source-Revision: 2a28b4a86cfc4a4c133c35b7fc2a6a9964387c39
115 lines
4.5 KiB
Rust
115 lines
4.5 KiB
Rust
//! When the server tells us its answers have changed.
|
|
//!
|
|
//! A pull-model server answers whatever it knows at the moment it is asked,
|
|
//! which after an edit — or during the first load of a large solution — may be
|
|
//! nothing yet. The protocol's answer to that is not for the client to guess
|
|
//! how long analysis takes, but for the server to say when it has finished:
|
|
//! `workspace/diagnostic/refresh` asks the client to re-pull everything, and
|
|
//! Roslyn sends it on re-analysis, project load and configuration changes.
|
|
//! Roslyn also has its own `workspace/projectInitializationComplete`, which is
|
|
//! what other clients use as the "the solution is really open now" signal.
|
|
//!
|
|
//! Both mean the same thing to us: ask again about everything we have open.
|
|
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::sync::{Arc, OnceLock};
|
|
|
|
use async_lsp::lsp_types::notification::Notification;
|
|
|
|
use super::pull::PullDiagnostics;
|
|
|
|
/// Roslyn's notification that the solution has finished loading.
|
|
///
|
|
/// Not part of the specification, so it is spelled out here. Roslyn analyzes
|
|
/// nothing meaningful until this point, and its answers before it are worth
|
|
/// re-asking.
|
|
pub enum ProjectInitializationComplete {}
|
|
|
|
impl Notification for ProjectInitializationComplete {
|
|
/// Roslyn sends `null`, older builds send an empty array; neither carries
|
|
/// anything we need.
|
|
type Params = serde_json::Value;
|
|
const METHOD: &'static str = "workspace/projectInitializationComplete";
|
|
}
|
|
|
|
/// The pull handle, as seen by the router.
|
|
///
|
|
/// The router is built before the handshake, and the pull handle is only
|
|
/// complete after it, so the two meet here. A refresh that arrives before the
|
|
/// handshake finishes is dropped, which is right: there is nothing open to
|
|
/// re-pull yet, and the first pull of each document is about to happen anyway.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct RefreshTarget {
|
|
pull: Arc<OnceLock<PullDiagnostics>>,
|
|
/// Set when the server has told us its answers are out of date, cleared by
|
|
/// the manager once it has re-opened the questions they answered.
|
|
invalidated: Arc<AtomicBool>,
|
|
}
|
|
|
|
impl RefreshTarget {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Hand the router the pull handle, once there is one.
|
|
pub fn publish(&self, pull: PullDiagnostics) {
|
|
if self.pull.set(pull).is_err() {
|
|
tracing::debug!("refresh target already published");
|
|
}
|
|
}
|
|
|
|
/// Throw away what the server has told us so far and ask again.
|
|
///
|
|
/// Forgetting is the point. The server has said its previous answers no
|
|
/// longer describe the code, and an answer that is known to be out of date
|
|
/// is worse than none: presented as current it is a lie, and left in place
|
|
/// it makes the re-pull look like it has already been answered.
|
|
pub fn refresh_all(&self, server_name: &str, reason: &str) {
|
|
let Some(pull) = self.pull.get() else {
|
|
tracing::debug!(server = %server_name, reason, "diagnostics refresh before the handshake finished; nothing open to re-pull");
|
|
return;
|
|
};
|
|
tracing::debug!(server = %server_name, reason, "re-pulling diagnostics for every open document");
|
|
if pull.refresh_all() {
|
|
self.invalidated.store(true, Ordering::Release);
|
|
}
|
|
}
|
|
|
|
/// Whether a refresh has happened since this was last asked.
|
|
///
|
|
/// Consuming: the caller owns re-opening the questions the refresh
|
|
/// invalidated, and dropping the answer drops the refresh with it.
|
|
///
|
|
/// The re-pull puts the answers back, but only someone waiting on a
|
|
/// document ever gets told about them — so the questions have to be asked
|
|
/// again too, and only the manager keeps those.
|
|
#[must_use]
|
|
pub fn take_invalidated(&self) -> bool {
|
|
self.invalidated.swap(false, Ordering::Acquire)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn a_refresh_before_the_handshake_is_a_no_op() {
|
|
let target = RefreshTarget::new();
|
|
// No panic, no work: there is nothing open to re-pull yet, so there is
|
|
// nothing to re-ask about either.
|
|
target.refresh_all("test", "unit test");
|
|
assert!(!target.take_invalidated());
|
|
}
|
|
|
|
#[test]
|
|
fn the_invalidation_is_reported_once() {
|
|
let target = RefreshTarget::new();
|
|
target.invalidated.store(true, Ordering::Release);
|
|
assert!(target.take_invalidated());
|
|
assert!(
|
|
!target.take_invalidated(),
|
|
"a second reader must not re-open questions already re-opened"
|
|
);
|
|
}
|
|
}
|