diff --git a/docs/plugin-host-model-completion-plan.md b/docs/plugin-host-model-completion-plan.md index c2995e4cc..c3c1b1efa 100644 --- a/docs/plugin-host-model-completion-plan.md +++ b/docs/plugin-host-model-completion-plan.md @@ -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`, 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 -document install — file open, template new, system open — keyed on a new per-tab document -generation. Boundary rule: dispatch happens after the mutation settles and pending history scopes +document install — file open, template new, system open — keyed on a per-tab pending flag, so +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 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 diff --git a/src/app/automation.rs b/src/app/automation.rs index 816873df2..a827eafb5 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -729,8 +729,8 @@ impl OpenCADStudio { // The headless session starts on the welcome (Start) tab, which // blocks drawing commands; turn it into a real drawing. self.tabs[i].is_start = false; - // A fresh document became resident (API v9): mint a new - // generation so the post-op boundary announces `DocumentOpened`. + // A fresh document became resident (API v9): the post-op + // boundary announces `DocumentOpened`. self.tabs[i].note_document_loaded(); self.entity_summary() } @@ -770,9 +770,8 @@ impl OpenCADStudio { self.tabs[i].current_path = Some(path_buf); self.tabs[i].is_start = false; self.tabs[i].scene.rebuild_derived_caches(); - // The opened document settled (API v9): mint a new - // generation so the post-op boundary announces - // `DocumentOpened` once for this load. + // The opened document settled (API v9): the post-op + // boundary announces `DocumentOpened` once for this load. self.tabs[i].note_document_loaded(); let mut summary = self.entity_summary(); if dropped > 0 { @@ -1855,7 +1854,7 @@ mod tests { "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"}"#); let log = plugin_dispatch_log(); assert!( @@ -1917,7 +1916,7 @@ mod tests { // The next message boundary does not repeat it. app.notify_plugins_document_opened(); 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); } diff --git a/src/app/control/sheets.rs b/src/app/control/sheets.rs index 0ca385253..358b60d1e 100644 --- a/src/app/control/sheets.rs +++ b/src/app/control/sheets.rs @@ -466,8 +466,8 @@ impl OpenCADStudio { self.tabs[i].current_path = None; self.tabs[i].is_start = false; self.tabs[i].dirty = true; - // A whole document just became resident in the tab (API v9): mint a - // fresh generation so the boundary announces `DocumentOpened` once. + // A whole document just became resident in the tab (API v9): the + // boundary announces `DocumentOpened` once. self.tabs[i].note_document_loaded(); Ok(purged) } diff --git a/src/app/document.rs b/src/app/document.rs index 406a67c9b..a0ce06b08 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -18,17 +18,6 @@ use std::sync::Arc; 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 ────────────────────────────────────────────────────────── /// 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 /// uses it with scene epochs so an older snapshot never clears newer work. pub(super) edit_revision: u64, - /// Monotonic identity of the document currently resident in this tab: a - /// fresh value is minted whenever a whole document replaces the tab's - /// content (file open, template new, system open), and edits leave it - /// alone. The plugin-opened notification guard - /// (`OpenCADStudio::notify_plugins_document_opened`, API v9) keys on it - /// so `DocumentOpened` dispatches exactly once per load. - pub(super) document_generation: u64, + /// A whole document replaced this tab's content (file open, template new, + /// system open) and native plugins have not been told yet: the + /// message boundary (`OpenCADStudio::notify_plugins_document_opened`, + /// API v9) dispatches `DocumentOpened` once and clears it, so edits and + /// switching between tabs never announce the same load again. + pub(super) document_opened_pending: bool, pub(super) tab_title: String, pub(super) properties: PropertiesPanel, pub(super) layers: LayerPanel, @@ -648,7 +636,7 @@ impl DocumentTab { dirty: false, recovery_save_as_required: false, edit_revision: 0, - document_generation: next_document_generation(), + document_opened_pending: true, prev_selection: Vec::new(), tab_title: format!("Drawing{}", n), properties: PropertiesPanel::empty(), @@ -723,11 +711,11 @@ impl DocumentTab { } /// A whole document just replaced this tab's content (file open, template - /// new, system open): mint a fresh generation so the message-boundary - /// dispatch announces `DocumentOpened` exactly once for this load (API - /// v9). Call after the document and its derived caches have settled. + /// new, system open): the next message boundary announces + /// `DocumentOpened` once for this load (API v9). Call after the document + /// and its derived caches have settled. 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 { diff --git a/src/app/mod.rs b/src/app/mod.rs index 85d4391f9..ee66dddd5 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1153,12 +1153,6 @@ pub(super) struct OpenCADStudio { /// boundaries as well as after plugin-initiated writes. #[cfg(not(target_arch = "wasm32"))] 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 /// Plugin Manager opens. external_plugins: Vec, @@ -4838,8 +4832,6 @@ impl OpenCADStudio { last_plugin_selection: None, #[cfg(not(target_arch = "wasm32"))] last_plugin_document: None, - #[cfg(not(target_arch = "wasm32"))] - last_plugin_document_opened: None, external_plugins: Vec::new(), loaded_plugin_ids: rustc_hash::FxHashSet::default(), plugin_load_errors: rustc_hash::FxHashMap::default(), diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 6e34a9965..3b492c0b7 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -2276,9 +2276,9 @@ impl OpenCADStudio { .set(crate::app::OPEN_PHASE_FINALIZING, 10000, 1, 1); } self.opening.take(); - // The document and its derived caches have settled: mark the tab's - // generation so the next message boundary announces `DocumentOpened` - // to native plugins exactly once for this load (API v9). + // The document and its derived caches have settled: the next message + // boundary announces `DocumentOpened` to native plugins exactly once + // for this load (API v9). self.tabs[i].note_document_loaded(); // The tab list just changed (and the active tab with it): remember // it for the next boot's restore flow (web session memory). diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 6b4d88759..b8ace3c10 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -344,9 +344,9 @@ impl OpenCADStudio { /// Announce that a whole document became resident in the active tab — /// opened from file, created from a template, or loaded by a system open - /// (API v9). Keyed on `(tab id, document generation)`, which changes only - /// when a tab's content is wholesale replaced, so an edit can never fire - /// it twice and a reload fires it exactly once. Part of the same + /// (API v9). Each tab carries its own pending flag, set only when its + /// content is wholesale replaced, so neither an edit nor a tab switch can + /// fire it twice and a reload fires it exactly once. Part of the same /// post-message boundary as the selection and document dispatches: runs /// after the mutation settles and pending history scopes closed, never /// inside history replay or interactive picking. @@ -359,11 +359,9 @@ impl OpenCADStudio { if self.tabs[i].is_start { return; } - let key = (self.tabs[i].id, self.tabs[i].document_generation); - if self.last_plugin_document_opened == Some(key) { + if !std::mem::take(&mut self.tabs[i].document_opened_pending) { return; } - self.last_plugin_document_opened = Some(key); crate::plugin::v4_support::publish_document_opened(self.tabs[i].id); }