forked from 1b-agent/opencadstudio-upstream
plugin: announce DocumentOpened once per load, not per tab switch
The guard remembered only the last announced (tab, generation), so switching A -> B -> A announced A's load again. Each tab now carries its own pending flag, set when a document is installed and cleared by the boundary dispatch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
68330ce39e
commit
4d66a58374
7 files changed
+28
-51
No files matched your search
@@ -427,8 +427,8 @@ and `SelectionChanged` (the plain selection set) at the fingerprint-guarded mess
|
|||||||
the v4 bridge already uses (`notify_plugins_selection_changed` / `notify_plugins_document_changed`,
|
the v4 bridge already uses (`notify_plugins_selection_changed` / `notify_plugins_document_changed`,
|
||||||
plus the automation post-op boundary), and a new additive `DocumentOpened { tab_id }` variant
|
plus the automation post-op boundary), and a new additive `DocumentOpened { tab_id }` variant
|
||||||
(discriminant 10, the counterpart of the existing `DocumentTabClosed`) that fires once per whole
|
(discriminant 10, the counterpart of the existing `DocumentTabClosed`) that fires once per whole
|
||||||
document install — file open, template new, system open — keyed on a new per-tab document
|
document install — file open, template new, system open — keyed on a per-tab pending flag, so
|
||||||
generation. Boundary rule: dispatch happens after the mutation settles and pending history scopes
|
neither an edit nor a tab switch repeats it. Boundary rule: dispatch happens after the mutation settles and pending history scopes
|
||||||
closed, never mid-undo/mid-pick, and history (undo/redo) replay itself emits nothing — the
|
closed, never mid-undo/mid-pick, and history (undo/redo) replay itself emits nothing — the
|
||||||
boundary is the only channel; plugin-initiated writes publish immediately after settling, like
|
boundary is the only channel; plugin-initiated writes publish immediately after settling, like
|
||||||
the drawing-changed event, with the boundary fingerprint deduplicating. Handlers may call HostApi
|
the drawing-changed event, with the boundary fingerprint deduplicating. Handlers may call HostApi
|
||||||
|
|||||||
@@ -729,8 +729,8 @@ impl OpenCADStudio {
|
|||||||
// The headless session starts on the welcome (Start) tab, which
|
// The headless session starts on the welcome (Start) tab, which
|
||||||
// blocks drawing commands; turn it into a real drawing.
|
// blocks drawing commands; turn it into a real drawing.
|
||||||
self.tabs[i].is_start = false;
|
self.tabs[i].is_start = false;
|
||||||
// A fresh document became resident (API v9): mint a new
|
// A fresh document became resident (API v9): the post-op
|
||||||
// generation so the post-op boundary announces `DocumentOpened`.
|
// boundary announces `DocumentOpened`.
|
||||||
self.tabs[i].note_document_loaded();
|
self.tabs[i].note_document_loaded();
|
||||||
self.entity_summary()
|
self.entity_summary()
|
||||||
}
|
}
|
||||||
@@ -770,9 +770,8 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].current_path = Some(path_buf);
|
self.tabs[i].current_path = Some(path_buf);
|
||||||
self.tabs[i].is_start = false;
|
self.tabs[i].is_start = false;
|
||||||
self.tabs[i].scene.rebuild_derived_caches();
|
self.tabs[i].scene.rebuild_derived_caches();
|
||||||
// The opened document settled (API v9): mint a new
|
// The opened document settled (API v9): the post-op
|
||||||
// generation so the post-op boundary announces
|
// boundary announces `DocumentOpened` once for this load.
|
||||||
// `DocumentOpened` once for this load.
|
|
||||||
self.tabs[i].note_document_loaded();
|
self.tabs[i].note_document_loaded();
|
||||||
let mut summary = self.entity_summary();
|
let mut summary = self.entity_summary();
|
||||||
if dropped > 0 {
|
if dropped > 0 {
|
||||||
@@ -1855,7 +1854,7 @@ mod tests {
|
|||||||
"a new document announces exactly one open: {log:?}"
|
"a new document announces exactly one open: {log:?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
// A follow-up op does not repeat it: generation-keyed dispatch.
|
// A follow-up op does not repeat it: the tab's pending flag cleared.
|
||||||
app.automation_op(r#"{"op":"query","type":"Line"}"#);
|
app.automation_op(r#"{"op":"query","type":"Line"}"#);
|
||||||
let log = plugin_dispatch_log();
|
let log = plugin_dispatch_log();
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1917,7 +1916,7 @@ mod tests {
|
|||||||
// The next message boundary does not repeat it.
|
// The next message boundary does not repeat it.
|
||||||
app.notify_plugins_document_opened();
|
app.notify_plugins_document_opened();
|
||||||
let log = plugin_dispatch_log();
|
let log = plugin_dispatch_log();
|
||||||
assert_eq!(log.len(), 0, "generation-keyed dispatch dedupes: {log:?}");
|
assert_eq!(log.len(), 0, "the pending flag dedupes: {log:?}");
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -466,8 +466,8 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].current_path = None;
|
self.tabs[i].current_path = None;
|
||||||
self.tabs[i].is_start = false;
|
self.tabs[i].is_start = false;
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
// A whole document just became resident in the tab (API v9): mint a
|
// A whole document just became resident in the tab (API v9): the
|
||||||
// fresh generation so the boundary announces `DocumentOpened` once.
|
// boundary announces `DocumentOpened` once.
|
||||||
self.tabs[i].note_document_loaded();
|
self.tabs[i].note_document_loaded();
|
||||||
Ok(purged)
|
Ok(purged)
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-23
@@ -18,17 +18,6 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
static NEXT_DOCUMENT_TAB_ID: AtomicU64 = AtomicU64::new(1);
|
static NEXT_DOCUMENT_TAB_ID: AtomicU64 = AtomicU64::new(1);
|
||||||
|
|
||||||
/// Monotonic source for `DocumentTab::document_generation` (API v9): every
|
|
||||||
/// minted value is process-unique, so any freshly installed document — file
|
|
||||||
/// open, template new, system open — differs from every generation a plugin
|
|
||||||
/// has been told about, and the opened-notification guard fires exactly once
|
|
||||||
/// per load.
|
|
||||||
static NEXT_DOCUMENT_GENERATION: AtomicU64 = AtomicU64::new(1);
|
|
||||||
|
|
||||||
fn next_document_generation() -> u64 {
|
|
||||||
NEXT_DOCUMENT_GENERATION.fetch_add(1, Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Dynamic input ──────────────────────────────────────────────────────────
|
// ── Dynamic input ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// One quantity shown in the dynamic-input overlay near the cursor.
|
/// One quantity shown in the dynamic-input overlay near the cursor.
|
||||||
@@ -158,13 +147,12 @@ pub(super) struct DocumentTab {
|
|||||||
/// Monotonic committed-edit/undo/redo revision. Background save completion
|
/// Monotonic committed-edit/undo/redo revision. Background save completion
|
||||||
/// uses it with scene epochs so an older snapshot never clears newer work.
|
/// uses it with scene epochs so an older snapshot never clears newer work.
|
||||||
pub(super) edit_revision: u64,
|
pub(super) edit_revision: u64,
|
||||||
/// Monotonic identity of the document currently resident in this tab: a
|
/// A whole document replaced this tab's content (file open, template new,
|
||||||
/// fresh value is minted whenever a whole document replaces the tab's
|
/// system open) and native plugins have not been told yet: the
|
||||||
/// content (file open, template new, system open), and edits leave it
|
/// message boundary (`OpenCADStudio::notify_plugins_document_opened`,
|
||||||
/// alone. The plugin-opened notification guard
|
/// API v9) dispatches `DocumentOpened` once and clears it, so edits and
|
||||||
/// (`OpenCADStudio::notify_plugins_document_opened`, API v9) keys on it
|
/// switching between tabs never announce the same load again.
|
||||||
/// so `DocumentOpened` dispatches exactly once per load.
|
pub(super) document_opened_pending: bool,
|
||||||
pub(super) document_generation: u64,
|
|
||||||
pub(super) tab_title: String,
|
pub(super) tab_title: String,
|
||||||
pub(super) properties: PropertiesPanel,
|
pub(super) properties: PropertiesPanel,
|
||||||
pub(super) layers: LayerPanel,
|
pub(super) layers: LayerPanel,
|
||||||
@@ -648,7 +636,7 @@ impl DocumentTab {
|
|||||||
dirty: false,
|
dirty: false,
|
||||||
recovery_save_as_required: false,
|
recovery_save_as_required: false,
|
||||||
edit_revision: 0,
|
edit_revision: 0,
|
||||||
document_generation: next_document_generation(),
|
document_opened_pending: true,
|
||||||
prev_selection: Vec::new(),
|
prev_selection: Vec::new(),
|
||||||
tab_title: format!("Drawing{}", n),
|
tab_title: format!("Drawing{}", n),
|
||||||
properties: PropertiesPanel::empty(),
|
properties: PropertiesPanel::empty(),
|
||||||
@@ -723,11 +711,11 @@ impl DocumentTab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A whole document just replaced this tab's content (file open, template
|
/// A whole document just replaced this tab's content (file open, template
|
||||||
/// new, system open): mint a fresh generation so the message-boundary
|
/// new, system open): the next message boundary announces
|
||||||
/// dispatch announces `DocumentOpened` exactly once for this load (API
|
/// `DocumentOpened` once for this load (API v9). Call after the document
|
||||||
/// v9). Call after the document and its derived caches have settled.
|
/// and its derived caches have settled.
|
||||||
pub(super) fn note_document_loaded(&mut self) {
|
pub(super) fn note_document_loaded(&mut self) {
|
||||||
self.document_generation = next_document_generation();
|
self.document_opened_pending = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn tab_display_name(&self) -> String {
|
pub(super) fn tab_display_name(&self) -> String {
|
||||||
|
|||||||
@@ -1153,12 +1153,6 @@ pub(super) struct OpenCADStudio {
|
|||||||
/// boundaries as well as after plugin-initiated writes.
|
/// boundaries as well as after plugin-initiated writes.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
last_plugin_document: Option<(u64, u64)>,
|
last_plugin_document: Option<(u64, u64)>,
|
||||||
/// `(tab id, document generation)` last dispatched as `DocumentOpened` —
|
|
||||||
/// a tab's generation changes only when a whole document replaced its
|
|
||||||
/// content, so the opened notification fires once per load, never per
|
|
||||||
/// edit (API v9).
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
last_plugin_document_opened: Option<(u64, u64)>,
|
|
||||||
/// External add-on packages found in the plugins folder, refreshed when the
|
/// External add-on packages found in the plugins folder, refreshed when the
|
||||||
/// Plugin Manager opens.
|
/// Plugin Manager opens.
|
||||||
external_plugins: Vec<crate::plugin::external::ExternalPlugin>,
|
external_plugins: Vec<crate::plugin::external::ExternalPlugin>,
|
||||||
@@ -4838,8 +4832,6 @@ impl OpenCADStudio {
|
|||||||
last_plugin_selection: None,
|
last_plugin_selection: None,
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
last_plugin_document: None,
|
last_plugin_document: None,
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
last_plugin_document_opened: None,
|
|
||||||
external_plugins: Vec::new(),
|
external_plugins: Vec::new(),
|
||||||
loaded_plugin_ids: rustc_hash::FxHashSet::default(),
|
loaded_plugin_ids: rustc_hash::FxHashSet::default(),
|
||||||
plugin_load_errors: rustc_hash::FxHashMap::default(),
|
plugin_load_errors: rustc_hash::FxHashMap::default(),
|
||||||
|
|||||||
@@ -2276,9 +2276,9 @@ impl OpenCADStudio {
|
|||||||
.set(crate::app::OPEN_PHASE_FINALIZING, 10000, 1, 1);
|
.set(crate::app::OPEN_PHASE_FINALIZING, 10000, 1, 1);
|
||||||
}
|
}
|
||||||
self.opening.take();
|
self.opening.take();
|
||||||
// The document and its derived caches have settled: mark the tab's
|
// The document and its derived caches have settled: the next message
|
||||||
// generation so the next message boundary announces `DocumentOpened`
|
// boundary announces `DocumentOpened` to native plugins exactly once
|
||||||
// to native plugins exactly once for this load (API v9).
|
// for this load (API v9).
|
||||||
self.tabs[i].note_document_loaded();
|
self.tabs[i].note_document_loaded();
|
||||||
// The tab list just changed (and the active tab with it): remember
|
// The tab list just changed (and the active tab with it): remember
|
||||||
// it for the next boot's restore flow (web session memory).
|
// it for the next boot's restore flow (web session memory).
|
||||||
|
|||||||
@@ -344,9 +344,9 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
/// Announce that a whole document became resident in the active tab —
|
/// Announce that a whole document became resident in the active tab —
|
||||||
/// opened from file, created from a template, or loaded by a system open
|
/// opened from file, created from a template, or loaded by a system open
|
||||||
/// (API v9). Keyed on `(tab id, document generation)`, which changes only
|
/// (API v9). Each tab carries its own pending flag, set only when its
|
||||||
/// when a tab's content is wholesale replaced, so an edit can never fire
|
/// content is wholesale replaced, so neither an edit nor a tab switch can
|
||||||
/// it twice and a reload fires it exactly once. Part of the same
|
/// fire it twice and a reload fires it exactly once. Part of the same
|
||||||
/// post-message boundary as the selection and document dispatches: runs
|
/// post-message boundary as the selection and document dispatches: runs
|
||||||
/// after the mutation settles and pending history scopes closed, never
|
/// after the mutation settles and pending history scopes closed, never
|
||||||
/// inside history replay or interactive picking.
|
/// inside history replay or interactive picking.
|
||||||
@@ -359,11 +359,9 @@ impl OpenCADStudio {
|
|||||||
if self.tabs[i].is_start {
|
if self.tabs[i].is_start {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let key = (self.tabs[i].id, self.tabs[i].document_generation);
|
if !std::mem::take(&mut self.tabs[i].document_opened_pending) {
|
||||||
if self.last_plugin_document_opened == Some(key) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.last_plugin_document_opened = Some(key);
|
|
||||||
crate::plugin::v4_support::publish_document_opened(self.tabs[i].id);
|
crate::plugin::v4_support::publish_document_opened(self.tabs[i].id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user