From 09ba67bfd127df64a0b24f0a5213c96a524cba87 Mon Sep 17 00:00:00 2001 From: Sebastian <106036+schoeller@users.noreply.github.com> Date: Sat, 29 Aug 2026 11:09:31 +0200 Subject: [PATCH 1/2] feat(plugins): expose document path to plugins via HostApi OpenCADStudio/plugins#901 (comment) - Add HostApi::document_path(tab_id) returning Option. - Add PluginRequest::DocumentPath and PluginResponse::DocumentPath for out-of-process plugins. - Implement the request in the IPC server and in both V3/V4 plugin-side HostApi proxies. - Implement HostSession::document_path by looking up the tab by id and returning its current_path. --- crates/ocs_plugin_api/src/host.rs | 10 ++++++++++ crates/ocs_plugin_api/src/ipc/client.rs | 15 +++++++++++++++ crates/ocs_plugin_api/src/ipc/protocol.rs | 4 ++++ crates/ocs_plugin_api/src/ipc/server.rs | 3 +++ crates/ocs_plugin_api/src/ipc/v4/client.rs | 15 +++++++++++++++ src/app/plugin_host.rs | 11 +++++++++++ 6 files changed, 58 insertions(+) diff --git a/crates/ocs_plugin_api/src/host.rs b/crates/ocs_plugin_api/src/host.rs index f146c23ab..8a78042a0 100644 --- a/crates/ocs_plugin_api/src/host.rs +++ b/crates/ocs_plugin_api/src/host.rs @@ -12,6 +12,7 @@ //! [`ensure_plugin_state`] helpers for the ergonomic typed access. use std::any::Any; +use std::path::PathBuf; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -531,6 +532,15 @@ pub trait HostApi { fn add_entities(&mut self, entities: Vec) -> Vec { entities.into_iter().map(|e| self.add_entity(e)).collect() } + + /// Return the filesystem path of the document in tab `tab_id`, if any. + /// Added after API v4; appended at the very end so older plugins compiled + /// without it keep stable vtable indices. The default returns `None`; + /// in-process hosts should override it to expose the real path. + fn document_path(&self, tab_id: u64) -> Option { + let _ = tab_id; + None + } } /// Simplified, read-only entity kind exposed by [`DocumentReader`]. diff --git a/crates/ocs_plugin_api/src/ipc/client.rs b/crates/ocs_plugin_api/src/ipc/client.rs index 4fb050e1a..3a617fc02 100644 --- a/crates/ocs_plugin_api/src/ipc/client.rs +++ b/crates/ocs_plugin_api/src/ipc/client.rs @@ -401,6 +401,21 @@ impl HostApi for PluginHostApi { None => Box::new(EmptyDocumentReader), } } + + fn document_path(&self, tab_id: u64) -> Option { + match self.client.request(PluginRequest::DocumentPath { tab_id }) { + Ok(PluginResponse::DocumentPath(Some(path))) => Some(std::path::PathBuf::from(path)), + Ok(PluginResponse::DocumentPath(None)) => None, + Ok(other) => { + eprintln!("[plugin] unexpected DocumentPath response: {other:?}"); + None + } + Err(e) => { + eprintln!("[plugin] DocumentPath request failed: {e}"); + None + } + } + } } /// Sentinel reader used when the shared-memory view could not be initialized. diff --git a/crates/ocs_plugin_api/src/ipc/protocol.rs b/crates/ocs_plugin_api/src/ipc/protocol.rs index 5b494026a..d599fdd13 100644 --- a/crates/ocs_plugin_api/src/ipc/protocol.rs +++ b/crates/ocs_plugin_api/src/ipc/protocol.rs @@ -136,6 +136,8 @@ pub enum PluginRequest { CloseDocumentViewV4 { tab_id: u64 }, /// V4: ask the host for the stable tab identifier of the active tab. GetTabId, + /// V4: ask the host for the filesystem path of the document in `tab_id`. + DocumentPath { tab_id: u64 }, } /// Responses the host sends back for `PluginRequest`. @@ -160,6 +162,8 @@ pub enum PluginResponse { }, /// V4: stable tab identifier of the active tab. TabId(u64), + /// V4: filesystem path of the document in the requested tab, if any. + DocumentPath(Option), } /// Messages sent from the host to the plugin runner. diff --git a/crates/ocs_plugin_api/src/ipc/server.rs b/crates/ocs_plugin_api/src/ipc/server.rs index 51ad9ac8a..2285b0968 100644 --- a/crates/ocs_plugin_api/src/ipc/server.rs +++ b/crates/ocs_plugin_api/src/ipc/server.rs @@ -74,5 +74,8 @@ pub fn handle_plugin_request( PluginResponse::Ok } GetTabId => PluginResponse::TabId(host.tab_id()), + DocumentPath { tab_id } => { + PluginResponse::DocumentPath(host.document_path(tab_id).map(|p| p.to_string_lossy().to_string())) + } } } diff --git a/crates/ocs_plugin_api/src/ipc/v4/client.rs b/crates/ocs_plugin_api/src/ipc/v4/client.rs index a491a6ebc..03cbf20ae 100644 --- a/crates/ocs_plugin_api/src/ipc/v4/client.rs +++ b/crates/ocs_plugin_api/src/ipc/v4/client.rs @@ -715,6 +715,21 @@ impl HostApi for V4PluginHostApi { view.take(); } } + + fn document_path(&self, tab_id: u64) -> Option { + match self.request(PluginRequest::DocumentPath { tab_id }) { + Ok(PluginResponse::DocumentPath(Some(path))) => Some(std::path::PathBuf::from(path)), + Ok(PluginResponse::DocumentPath(None)) => None, + Ok(other) => { + eprintln!("[plugin] unexpected DocumentPath response: {other:?}"); + None + } + Err(e) => { + eprintln!("[plugin] DocumentPath request failed: {e}"); + None + } + } + } } #[cfg(test)] diff --git a/src/app/plugin_host.rs b/src/app/plugin_host.rs index 272e89039..d60992822 100644 --- a/src/app/plugin_host.rs +++ b/src/app/plugin_host.rs @@ -35,6 +35,14 @@ impl<'a> HostSession<'a> { self.app.tabs[self.tab].id } + pub fn document_path(&self, tab_id: u64) -> Option { + self.app + .tabs + .iter() + .find(|t| t.id == tab_id) + .and_then(|t| t.current_path.clone()) + } + #[cfg(not(target_arch = "wasm32"))] pub fn document_view_v4(&mut self, tab_id: u64) -> Option { if tab_id != self.tab_id() { @@ -341,6 +349,9 @@ impl HostApi for HostSession<'_> { fn tab_id(&self) -> u64 { self.tab_id() } + fn document_path(&self, tab_id: u64) -> Option { + self.document_path(tab_id) + } #[cfg(not(target_arch = "wasm32"))] fn document_view_v4(&mut self, tab_id: u64) -> Option { self.document_view_v4(tab_id) From 0d766be3df9970d052c3ab22a64cafec0109b5c0 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sat, 29 Aug 2026 16:00:18 +0300 Subject: [PATCH 2/2] fix(plugins): preserve document path encoding Keep OS-native path data intact across IPC instead of replacing non-UTF-8 bytes. Refs #901 --- crates/ocs_plugin_api/ARCHITECTURE.md | 2 +- crates/ocs_plugin_api/README.md | 3 ++- crates/ocs_plugin_api/src/host.rs | 5 ++--- crates/ocs_plugin_api/src/ipc/protocol.rs | 6 +++--- crates/ocs_plugin_api/src/ipc/server.rs | 6 +++--- docs/plugin-architecture.md | 2 ++ 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/crates/ocs_plugin_api/ARCHITECTURE.md b/crates/ocs_plugin_api/ARCHITECTURE.md index 08761e7db..1a8e49ebe 100644 --- a/crates/ocs_plugin_api/ARCHITECTURE.md +++ b/crates/ocs_plugin_api/ARCHITECTURE.md @@ -49,7 +49,7 @@ The host and each plugin run in separate OS processes. The host re-executes itse - `OCS_PLUGIN_MAX_API_VERSION` can cap the accepted major at runtime (e.g. `4` to disable V5). - A plugin built against major `N` runs on a host whose major is `>= N` because new vtable entries and enum variants are appended at the end. - V4 introduces the **acadrust gate**: plugins targeting API v4 or later must resolve the same `acadrust` source as the host (see [`src/version_info.rs`](src/version_info.rs)). -- V5 introduces `BuiltinPlugin::on_load`, invoked once after the runner connects. +- V5 introduces `BuiltinPlugin::on_load` and tab-keyed document paths. The runtime enforces three gates: diff --git a/crates/ocs_plugin_api/README.md b/crates/ocs_plugin_api/README.md index 1e01c6438..f15d41d04 100644 --- a/crates/ocs_plugin_api/README.md +++ b/crates/ocs_plugin_api/README.md @@ -18,7 +18,7 @@ enum variants at the end, so older plugins continue to load on newer hosts. | **V2** | Out-of-process runner | Synchronous commands + interactive point collection (`HostApi::start_interactive`) | Baseline | | **V3** | Out-of-process runner | Local cached `document()`/`document_mut()` + shared-memory `DocumentReader`/`document_view` for large reads | New vtable entries appended; new `PluginRequest`/`PluginResponse` variants appended | | **V4** | Out-of-process runner over a multiplexed local socket | Full-duplex notifications + asynchronous REPL `ExecuteCode` | New `HostToPluginV4`/`PluginToHostV4` frame layer; `BuiltinPlugin::on_notification` and `start_execute_code` appended to trait | -| **V5** | V4 multiplexed runner | One-shot `BuiltinPlugin::on_load` initialization | Lifecycle callback appended to `BuiltinPlugin` | +| **V5** | V4 multiplexed runner | One-shot initialization + tab-keyed document paths | `BuiltinPlugin::on_load` and `HostApi::document_path` appended to the traits | The host controls which generations are accepted at runtime through `OCS_PLUGIN_MAX_API_VERSION` (e.g. `2` for V2-only mode, `4` to disable V5). The @@ -28,6 +28,7 @@ back to `API_VERSION_MIN_SUPPORTED = 2`. V5 calls `BuiltinPlugin::on_load` once after the runner connects and before the first user command. Plugins can cache `HostApi::plugin_request_sender()` there for worker-thread access. A panic in the callback fails plugin loading. +`HostApi::document_path(tab_id)` returns the saved path for a document tab. ### High-level layout diff --git a/crates/ocs_plugin_api/src/host.rs b/crates/ocs_plugin_api/src/host.rs index 13bde0353..3cecde5d3 100644 --- a/crates/ocs_plugin_api/src/host.rs +++ b/crates/ocs_plugin_api/src/host.rs @@ -542,9 +542,8 @@ pub trait HostApi { } /// Return the filesystem path of the document in tab `tab_id`, if any. - /// Added after API v4; appended at the very end so older plugins compiled - /// without it keep stable vtable indices. The default returns `None`; - /// in-process hosts should override it to expose the real path. + /// Added in API v5. The default returns `None`; in-process hosts should + /// override it to expose the real path. fn document_path(&self, tab_id: u64) -> Option { let _ = tab_id; None diff --git a/crates/ocs_plugin_api/src/ipc/protocol.rs b/crates/ocs_plugin_api/src/ipc/protocol.rs index d599fdd13..812e91ebd 100644 --- a/crates/ocs_plugin_api/src/ipc/protocol.rs +++ b/crates/ocs_plugin_api/src/ipc/protocol.rs @@ -136,7 +136,7 @@ pub enum PluginRequest { CloseDocumentViewV4 { tab_id: u64 }, /// V4: ask the host for the stable tab identifier of the active tab. GetTabId, - /// V4: ask the host for the filesystem path of the document in `tab_id`. + /// V5: ask the host for the filesystem path of the document in `tab_id`. DocumentPath { tab_id: u64 }, } @@ -162,8 +162,8 @@ pub enum PluginResponse { }, /// V4: stable tab identifier of the active tab. TabId(u64), - /// V4: filesystem path of the document in the requested tab, if any. - DocumentPath(Option), + /// V5: filesystem path of the document in the requested tab, if any. + DocumentPath(Option), } /// Messages sent from the host to the plugin runner. diff --git a/crates/ocs_plugin_api/src/ipc/server.rs b/crates/ocs_plugin_api/src/ipc/server.rs index 2285b0968..055880ef0 100644 --- a/crates/ocs_plugin_api/src/ipc/server.rs +++ b/crates/ocs_plugin_api/src/ipc/server.rs @@ -74,8 +74,8 @@ pub fn handle_plugin_request( PluginResponse::Ok } GetTabId => PluginResponse::TabId(host.tab_id()), - DocumentPath { tab_id } => { - PluginResponse::DocumentPath(host.document_path(tab_id).map(|p| p.to_string_lossy().to_string())) - } + DocumentPath { tab_id } => PluginResponse::DocumentPath( + host.document_path(tab_id).map(|path| path.into_os_string()), + ), } } diff --git a/docs/plugin-architecture.md b/docs/plugin-architecture.md index d27612f24..19b1a0968 100644 --- a/docs/plugin-architecture.md +++ b/docs/plugin-architecture.md @@ -130,6 +130,7 @@ pub trait BuiltinPlugin: Send + Sync { fn manifest(&self) -> &'static PluginManifest; fn ribbon(&self) -> Box; // the ribbon tab fn dispatch(&self, host: &mut dyn HostApi, cmd: &str) -> bool; + fn on_load(&mut self, host: &mut dyn HostApi) {} } ``` @@ -146,6 +147,7 @@ concrete types: | Command line | `push_info`, `push_output`, `push_error` | | Undo / dirty | `push_undo`, `set_dirty` | | Tab | `tab_index()` | +| Document identity (V5) | `document_path(tab_id)` returns the saved path for a document tab. | | Notifications (V4) | `on_notification` receives `HostNotification::SelectionChangedV4 { tab_id, handles }` when the active tab's selection changes, plus `DocumentChangedV4` / `DocumentTabClosed`. | ### `export_plugin!` — the C-ABI export