forked from 1b-agent/opencadstudio-upstream
style: apply cargo fmt across workspace
This commit is contained in:
1 parent
85710e5454
commit
9cf9ff2c5c
531 files changed
+57740
-33906
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,4 @@
|
||||
/// Stable, language-neutral entity capability schema.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
@@ -65,15 +65,37 @@ pub enum LogLevel {
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum SolidPrimitive {
|
||||
/// Axis-aligned box from its center and full extents.
|
||||
Box { center: [f64; 3], size: [f64; 3] },
|
||||
Box {
|
||||
center: [f64; 3],
|
||||
size: [f64; 3],
|
||||
},
|
||||
/// Right triangular prism from its minimum corner and extents.
|
||||
Wedge { origin: [f64; 3], size: [f64; 3] },
|
||||
Wedge {
|
||||
origin: [f64; 3],
|
||||
size: [f64; 3],
|
||||
},
|
||||
/// Cylinder standing on the plane through `center`.
|
||||
Cylinder { center: [f64; 3], radius: f64, height: f64 },
|
||||
Sphere { center: [f64; 3], radius: f64 },
|
||||
Torus { center: [f64; 3], major: f64, minor: f64 },
|
||||
Cylinder {
|
||||
center: [f64; 3],
|
||||
radius: f64,
|
||||
height: f64,
|
||||
},
|
||||
Sphere {
|
||||
center: [f64; 3],
|
||||
radius: f64,
|
||||
},
|
||||
Torus {
|
||||
center: [f64; 3],
|
||||
major: f64,
|
||||
minor: f64,
|
||||
},
|
||||
/// Regular pyramid with `sides` base edges.
|
||||
Pyramid { center: [f64; 3], radius: f64, height: f64, sides: u32 },
|
||||
Pyramid {
|
||||
center: [f64; 3],
|
||||
radius: f64,
|
||||
height: f64,
|
||||
sides: u32,
|
||||
},
|
||||
}
|
||||
|
||||
/// How two solids combine.
|
||||
@@ -159,12 +181,20 @@ pub enum TableOperation {
|
||||
/// Create a dimension style, optionally copying an existing one first.
|
||||
/// `properties` is a JSON object of DimStyle fields (see the Python docs);
|
||||
/// handles and xref fields are managed by the host and refused.
|
||||
DimStyleCreate { name: String, copy_from: Option<String>, properties: String },
|
||||
DimStyleCreate {
|
||||
name: String,
|
||||
copy_from: Option<String>,
|
||||
properties: String,
|
||||
},
|
||||
/// Change only the DimStyle fields the JSON object `properties` names.
|
||||
DimStyleModify { name: String, properties: String },
|
||||
/// Rename a text or dimension style; references to it follow. `Standard`
|
||||
/// is never renamed and a case-only change is refused.
|
||||
StyleRename { kind: TableStyleKind, from: String, to: String },
|
||||
StyleRename {
|
||||
kind: TableStyleKind,
|
||||
from: String,
|
||||
to: String,
|
||||
},
|
||||
/// Delete a text or dimension style that is neither current nor in use.
|
||||
/// `Standard` is never deleted.
|
||||
StyleDelete { kind: TableStyleKind, name: String },
|
||||
@@ -200,9 +230,17 @@ pub enum TableOperation {
|
||||
/// Create a simple linetype. `pattern` is signed lengths in drawing units:
|
||||
/// positive is a dash, negative a gap, zero a dot (2-12 elements with at
|
||||
/// least one dash or dot and one gap). Returns the linetype's handle.
|
||||
LinetypeCreate { name: String, description: String, pattern: Vec<f64> },
|
||||
LinetypeCreate {
|
||||
name: String,
|
||||
description: String,
|
||||
pattern: Vec<f64>,
|
||||
},
|
||||
/// Change a simple linetype's description or pattern in place.
|
||||
LinetypeModify { name: String, description: Option<String>, pattern: Option<Vec<f64>> },
|
||||
LinetypeModify {
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
pattern: Option<Vec<f64>>,
|
||||
},
|
||||
/// Rename a linetype; layers and entities that use it follow.
|
||||
LinetypeRename { from: String, to: String },
|
||||
/// Delete a linetype that no layer, entity, dimension style or the current
|
||||
@@ -257,7 +295,10 @@ pub struct TextStyleConfig {
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum SolidOperation {
|
||||
/// Build a new solid on `layer` (layer `0` when `None`).
|
||||
Create { primitive: SolidPrimitive, layer: Option<String> },
|
||||
Create {
|
||||
primitive: SolidPrimitive,
|
||||
layer: Option<String>,
|
||||
},
|
||||
/// Apply a rigid transform, given as a column-major 4x4 matrix, to an
|
||||
/// existing solid. Scaling and shear are refused.
|
||||
Transform { handle: Handle, matrix: [f64; 16] },
|
||||
@@ -265,12 +306,25 @@ pub enum SolidOperation {
|
||||
/// ellipse, closed polyline or spline, ...). The new region goes on
|
||||
/// `layer`, or on the source's layer when `None`; `delete_source` erases
|
||||
/// the profile afterwards, as the REGION command does.
|
||||
RegionFromProfile { source: Handle, layer: Option<String>, delete_source: bool },
|
||||
RegionFromProfile {
|
||||
source: Handle,
|
||||
layer: Option<String>,
|
||||
delete_source: bool,
|
||||
},
|
||||
/// Build a plane surface from one closed planar profile entity.
|
||||
SurfaceFromProfile { source: Handle, layer: Option<String>, delete_source: bool },
|
||||
SurfaceFromProfile {
|
||||
source: Handle,
|
||||
layer: Option<String>,
|
||||
delete_source: bool,
|
||||
},
|
||||
/// Extrude a planar profile along `direction`: a solid when the profile is
|
||||
/// closed, a surface when it is open.
|
||||
Extrude { source: Handle, direction: [f64; 3], layer: Option<String>, delete_source: bool },
|
||||
Extrude {
|
||||
source: Handle,
|
||||
direction: [f64; 3],
|
||||
layer: Option<String>,
|
||||
delete_source: bool,
|
||||
},
|
||||
/// Combine two solids into a new one. Both operands are consumed unless
|
||||
/// `keep_operands`; the result goes on `layer` or the first operand's
|
||||
/// layer. The kernel refuses some cases (coincident faces, cuts it has no
|
||||
@@ -280,7 +334,12 @@ pub enum SolidOperation {
|
||||
/// `origin`, `width` drawing units wide, keeping the picture's aspect.
|
||||
/// PNG, JPEG and BMP are stored as read; other formats are re-encoded as
|
||||
/// PNG. The frame goes on `layer` (layer `0` when `None`).
|
||||
EmbedPicture { path: String, origin: [f64; 3], width: f64, layer: Option<String> },
|
||||
EmbedPicture {
|
||||
path: String,
|
||||
origin: [f64; 3],
|
||||
width: f64,
|
||||
layer: Option<String>,
|
||||
},
|
||||
Boolean {
|
||||
first: Handle,
|
||||
second: Handle,
|
||||
@@ -302,21 +361,41 @@ pub enum HostSettingValue {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum HostNotification {
|
||||
InputLine { line: String },
|
||||
InputLine {
|
||||
line: String,
|
||||
},
|
||||
Cancel,
|
||||
DocumentChanged { version: u64 },
|
||||
SelectionChanged { handles: Vec<Handle> },
|
||||
DocumentChanged {
|
||||
version: u64,
|
||||
},
|
||||
SelectionChanged {
|
||||
handles: Vec<Handle>,
|
||||
},
|
||||
Raw(Vec<u8>),
|
||||
/// V4 snapshot changed for a specific tab. Discriminant 5.
|
||||
DocumentChangedV4 { tab_id: u64, version: u64 },
|
||||
DocumentChangedV4 {
|
||||
tab_id: u64,
|
||||
version: u64,
|
||||
},
|
||||
/// V4 tab closed notification. Discriminant 6.
|
||||
DocumentTabClosed { tab_id: u64 },
|
||||
DocumentTabClosed {
|
||||
tab_id: u64,
|
||||
},
|
||||
/// V4 selection changed for a specific tab. Discriminant 7.
|
||||
SelectionChangedV4 { tab_id: u64, handles: Vec<Handle> },
|
||||
SelectionChangedV4 {
|
||||
tab_id: u64,
|
||||
handles: Vec<Handle>,
|
||||
},
|
||||
/// V7 active interactive-command state for a tab. Discriminant 8.
|
||||
CommandStateChanged { tab_id: u64, command: Option<String> },
|
||||
CommandStateChanged {
|
||||
tab_id: u64,
|
||||
command: Option<String>,
|
||||
},
|
||||
/// V7 drawing epoch, broadcast even without an open shared view. Discriminant 9.
|
||||
DrawingChanged { tab_id: u64, epoch: u64 },
|
||||
DrawingChanged {
|
||||
tab_id: u64,
|
||||
epoch: u64,
|
||||
},
|
||||
/// Fallback for notification variants added in future minor revisions.
|
||||
/// Carries the raw bincode payload so an older peer can ignore it without
|
||||
/// failing deserialization.
|
||||
@@ -329,57 +408,44 @@ impl Serialize for HostNotification {
|
||||
match self {
|
||||
HostNotification::InputLine { line } => {
|
||||
bytes.push(0);
|
||||
bincode::serialize_into(&mut bytes, line)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, line).map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
HostNotification::Cancel => bytes.push(1),
|
||||
HostNotification::DocumentChanged { version } => {
|
||||
bytes.push(2);
|
||||
bincode::serialize_into(&mut bytes, version)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, version).map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
HostNotification::SelectionChanged { handles } => {
|
||||
bytes.push(3);
|
||||
bincode::serialize_into(&mut bytes, handles)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, handles).map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
HostNotification::Raw(data) => {
|
||||
bytes.push(4);
|
||||
bincode::serialize_into(&mut bytes, data)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, data).map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
HostNotification::DocumentChangedV4 { tab_id, version } => {
|
||||
bytes.push(5);
|
||||
bincode::serialize_into(&mut bytes, tab_id)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, version)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, tab_id).map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, version).map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
HostNotification::DocumentTabClosed { tab_id } => {
|
||||
bytes.push(6);
|
||||
bincode::serialize_into(&mut bytes, tab_id)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, tab_id).map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
HostNotification::SelectionChangedV4 { tab_id, handles } => {
|
||||
bytes.push(7);
|
||||
bincode::serialize_into(&mut bytes, tab_id)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, handles)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, tab_id).map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, handles).map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
HostNotification::CommandStateChanged { tab_id, command } => {
|
||||
bytes.push(8);
|
||||
bincode::serialize_into(&mut bytes, tab_id)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, command)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, tab_id).map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, command).map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
HostNotification::DrawingChanged { tab_id, epoch } => {
|
||||
bytes.push(9);
|
||||
bincode::serialize_into(&mut bytes, tab_id)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, epoch)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, tab_id).map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, epoch).map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
HostNotification::Unknown(raw) => bytes.extend_from_slice(raw),
|
||||
}
|
||||
@@ -434,14 +500,28 @@ impl<'de> Deserialize<'de> for HostNotification {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum PluginNotification {
|
||||
Output { text: String },
|
||||
Error { text: String },
|
||||
Prompt { text: String },
|
||||
Progress { percent: u8 },
|
||||
Log { level: LogLevel, text: String },
|
||||
Output {
|
||||
text: String,
|
||||
},
|
||||
Error {
|
||||
text: String,
|
||||
},
|
||||
Prompt {
|
||||
text: String,
|
||||
},
|
||||
Progress {
|
||||
percent: u8,
|
||||
},
|
||||
Log {
|
||||
level: LogLevel,
|
||||
text: String,
|
||||
},
|
||||
Raw(Vec<u8>),
|
||||
/// V4 REPL status update. Discriminant 6.
|
||||
ReplStatus { status: String, message: String },
|
||||
ReplStatus {
|
||||
status: String,
|
||||
message: String,
|
||||
},
|
||||
/// Fallback for notification variants added in future minor revisions.
|
||||
/// Carries the raw bincode payload so an older peer can ignore it without
|
||||
/// failing deserialization.
|
||||
@@ -454,42 +534,33 @@ impl Serialize for PluginNotification {
|
||||
match self {
|
||||
PluginNotification::Output { text } => {
|
||||
bytes.push(0);
|
||||
bincode::serialize_into(&mut bytes, text)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, text).map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
PluginNotification::Error { text } => {
|
||||
bytes.push(1);
|
||||
bincode::serialize_into(&mut bytes, text)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, text).map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
PluginNotification::Prompt { text } => {
|
||||
bytes.push(2);
|
||||
bincode::serialize_into(&mut bytes, text)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, text).map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
PluginNotification::Progress { percent } => {
|
||||
bytes.push(3);
|
||||
bincode::serialize_into(&mut bytes, percent)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, percent).map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
PluginNotification::Log { level, text } => {
|
||||
bytes.push(4);
|
||||
bincode::serialize_into(&mut bytes, level)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, text)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, level).map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, text).map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
PluginNotification::Raw(data) => {
|
||||
bytes.push(5);
|
||||
bincode::serialize_into(&mut bytes, data)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, data).map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
PluginNotification::ReplStatus { status, message } => {
|
||||
bytes.push(6);
|
||||
bincode::serialize_into(&mut bytes, status)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, message)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, status).map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, message).map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
PluginNotification::Unknown(raw) => bytes.extend_from_slice(raw),
|
||||
}
|
||||
@@ -519,8 +590,8 @@ impl<'de> Deserialize<'de> for PluginNotification {
|
||||
.map(|percent| PluginNotification::Progress { percent })
|
||||
.map_err(serde::de::Error::custom),
|
||||
4 => {
|
||||
let (level, text): (LogLevel, String) = bincode::deserialize(rest)
|
||||
.map_err(serde::de::Error::custom)?;
|
||||
let (level, text): (LogLevel, String) =
|
||||
bincode::deserialize(rest).map_err(serde::de::Error::custom)?;
|
||||
Ok(PluginNotification::Log { level, text })
|
||||
}
|
||||
5 => bincode::deserialize(rest)
|
||||
@@ -641,7 +712,10 @@ pub enum PreviewWire {
|
||||
impl PreviewWire {
|
||||
/// Create a polyline preview wire with default host cyan color.
|
||||
pub fn new(points: Vec<[f64; 3]>) -> Self {
|
||||
Self::Polyline { points, color: None }
|
||||
Self::Polyline {
|
||||
points,
|
||||
color: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional RGBA color (0.0 to 1.0) specified for this preview wire.
|
||||
@@ -869,9 +943,7 @@ pub trait HostApi {
|
||||
/// Returns the optional `command_id` used to correlate the notification
|
||||
/// with a running command, and the notification payload. Long-running
|
||||
/// plugins should call this periodically to drain the bounded queue.
|
||||
fn try_recv_notification(
|
||||
&mut self,
|
||||
) -> Option<(Option<u64>, HostNotification)> {
|
||||
fn try_recv_notification(&mut self) -> Option<(Option<u64>, HostNotification)> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -949,7 +1021,9 @@ pub trait HostApi {
|
||||
}
|
||||
|
||||
/// Current ordered selection in this session's tab (API v7).
|
||||
fn selection(&self) -> Vec<Handle> { Vec::new() }
|
||||
fn selection(&self) -> Vec<Handle> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Replace this tab's selection with exactly these handles, in order.
|
||||
/// Missing or duplicate handles reject the request (API v7).
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::ipc::protocol::{
|
||||
HostResponse, HostToPlugin, PluginRequest, PluginResponse, PluginToHost, RunnerHandshake,
|
||||
};
|
||||
use crate::ipc::transport::{recv, send};
|
||||
use crate::shm::{DocumentViewInfo, SharedDocumentReader, DocumentViewData};
|
||||
use crate::shm::{DocumentViewData, DocumentViewInfo, SharedDocumentReader};
|
||||
|
||||
/// Shared registry of active interactive commands, keyed by host-assigned id.
|
||||
pub type InteractiveRegistry = Rc<RefCell<HashMap<u64, Box<dyn InteractiveCommand>>>>;
|
||||
@@ -60,7 +60,10 @@ impl IpcClient {
|
||||
&self,
|
||||
req: PluginRequest,
|
||||
) -> Result<PluginResponse, crate::ipc::transport::TransportError> {
|
||||
send(&mut self.stream.borrow_mut(), &PluginToHost::Request(Box::new(req)))?;
|
||||
send(
|
||||
&mut self.stream.borrow_mut(),
|
||||
&PluginToHost::Request(Box::new(req)),
|
||||
)?;
|
||||
loop {
|
||||
match recv::<HostToPlugin>(&mut self.stream.borrow_mut())? {
|
||||
HostToPlugin::Response(resp) => return Ok(*resp),
|
||||
@@ -389,16 +392,18 @@ impl HostApi for PluginHostApi {
|
||||
}
|
||||
}
|
||||
match self.doc_view.borrow().as_ref() {
|
||||
Some(info) => match SharedDocumentReader::<DocumentViewData>::open(Path::new(&info.path)) {
|
||||
Ok(reader) => Box::new(reader),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"[plugin] failed to open document view at {}: {e}",
|
||||
info.path
|
||||
);
|
||||
Box::new(EmptyDocumentReader)
|
||||
Some(info) => {
|
||||
match SharedDocumentReader::<DocumentViewData>::open(Path::new(&info.path)) {
|
||||
Ok(reader) => Box::new(reader),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"[plugin] failed to open document view at {}: {e}",
|
||||
info.path
|
||||
);
|
||||
Box::new(EmptyDocumentReader)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
None => Box::new(EmptyDocumentReader),
|
||||
}
|
||||
}
|
||||
@@ -419,7 +424,9 @@ impl HostApi for PluginHostApi {
|
||||
}
|
||||
|
||||
fn system_variable(&self, name: &str) -> Option<HostSettingValue> {
|
||||
match self.client.request(PluginRequest::GetSystemVariable { name: name.to_owned() }) {
|
||||
match self.client.request(PluginRequest::GetSystemVariable {
|
||||
name: name.to_owned(),
|
||||
}) {
|
||||
Ok(PluginResponse::SystemVariable(value)) => value,
|
||||
_ => None,
|
||||
}
|
||||
@@ -451,11 +458,16 @@ impl HostApi for PluginHostApi {
|
||||
label: &str,
|
||||
entities: Vec<EntityType>,
|
||||
) -> Result<(), String> {
|
||||
match self.client.request(PluginRequest::UpdateEntitiesTransaction {
|
||||
label: label.to_owned(), entities,
|
||||
}) {
|
||||
match self
|
||||
.client
|
||||
.request(PluginRequest::UpdateEntitiesTransaction {
|
||||
label: label.to_owned(),
|
||||
entities,
|
||||
}) {
|
||||
Ok(PluginResponse::EntityTransactionResult(result)) => {
|
||||
if result.is_ok() { self.document_cache = OnceCell::new(); }
|
||||
if result.is_ok() {
|
||||
self.document_cache = OnceCell::new();
|
||||
}
|
||||
result
|
||||
}
|
||||
Ok(PluginResponse::Error(error)) => Err(error),
|
||||
@@ -471,10 +483,18 @@ impl HostApi for PluginHostApi {
|
||||
}
|
||||
}
|
||||
|
||||
fn solid_operation(&mut self, operation: crate::host::SolidOperation) -> Result<Handle, String> {
|
||||
match self.client.request(PluginRequest::SolidOperation { operation }) {
|
||||
fn solid_operation(
|
||||
&mut self,
|
||||
operation: crate::host::SolidOperation,
|
||||
) -> Result<Handle, String> {
|
||||
match self
|
||||
.client
|
||||
.request(PluginRequest::SolidOperation { operation })
|
||||
{
|
||||
Ok(PluginResponse::SolidResult(result)) => {
|
||||
if result.is_ok() { self.document_cache = OnceCell::new(); }
|
||||
if result.is_ok() {
|
||||
self.document_cache = OnceCell::new();
|
||||
}
|
||||
result
|
||||
}
|
||||
Ok(PluginResponse::Error(error)) => Err(error),
|
||||
@@ -483,7 +503,10 @@ impl HostApi for PluginHostApi {
|
||||
}
|
||||
}
|
||||
|
||||
fn run_command(&mut self, request: crate::host::CommandRequest) -> Result<crate::host::CommandOutcome, String> {
|
||||
fn run_command(
|
||||
&mut self,
|
||||
request: crate::host::CommandRequest,
|
||||
) -> Result<crate::host::CommandOutcome, String> {
|
||||
match self.client.request(PluginRequest::RunCommand { request }) {
|
||||
Ok(PluginResponse::CommandResult(result)) => {
|
||||
self.document_cache = OnceCell::new();
|
||||
@@ -495,10 +518,18 @@ impl HostApi for PluginHostApi {
|
||||
}
|
||||
}
|
||||
|
||||
fn table_operation(&mut self, operation: crate::host::TableOperation) -> Result<Handle, String> {
|
||||
match self.client.request(PluginRequest::TableOperation { operation }) {
|
||||
fn table_operation(
|
||||
&mut self,
|
||||
operation: crate::host::TableOperation,
|
||||
) -> Result<Handle, String> {
|
||||
match self
|
||||
.client
|
||||
.request(PluginRequest::TableOperation { operation })
|
||||
{
|
||||
Ok(PluginResponse::TableResult(result)) => {
|
||||
if result.is_ok() { self.document_cache = OnceCell::new(); }
|
||||
if result.is_ok() {
|
||||
self.document_cache = OnceCell::new();
|
||||
}
|
||||
result
|
||||
}
|
||||
Ok(PluginResponse::Error(error)) => Err(error),
|
||||
@@ -508,7 +539,9 @@ impl HostApi for PluginHostApi {
|
||||
}
|
||||
|
||||
fn set_selection(&mut self, handles: &[Handle]) -> Result<(), String> {
|
||||
match self.client.request(PluginRequest::SetSelection { handles: handles.to_vec() }) {
|
||||
match self.client.request(PluginRequest::SetSelection {
|
||||
handles: handles.to_vec(),
|
||||
}) {
|
||||
Ok(PluginResponse::SelectionResult(result)) => result,
|
||||
Ok(PluginResponse::Error(error)) => Err(error),
|
||||
Ok(other) => Err(format!("unexpected selection response: {other:?}")),
|
||||
@@ -555,7 +588,10 @@ impl HostApi for PluginHostApi {
|
||||
}
|
||||
|
||||
fn execute_command(&mut self, cmd: &str) -> bool {
|
||||
match self.client.request(PluginRequest::ExecuteCommand(cmd.to_string())) {
|
||||
match self
|
||||
.client
|
||||
.request(PluginRequest::ExecuteCommand(cmd.to_string()))
|
||||
{
|
||||
Ok(PluginResponse::Bool(b)) => b,
|
||||
Ok(other) => {
|
||||
eprintln!("[plugin] unexpected ExecuteCommand response: {other:?}");
|
||||
@@ -662,7 +698,11 @@ mod tests {
|
||||
},
|
||||
other => panic!("unexpected: {other:?}"),
|
||||
}
|
||||
send(&mut peer, &HostToPlugin::Response(Box::new(PluginResponse::Ok))).unwrap();
|
||||
send(
|
||||
&mut peer,
|
||||
&HostToPlugin::Response(Box::new(PluginResponse::Ok)),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
api.push_info("hello host");
|
||||
peer_handle.join().unwrap();
|
||||
@@ -719,7 +759,10 @@ mod tests {
|
||||
EntityType::Point(Point::new()),
|
||||
]);
|
||||
peer_handle.join().unwrap();
|
||||
assert_eq!(handles, vec![Handle::new(10), Handle::new(11), Handle::new(12)]);
|
||||
assert_eq!(
|
||||
handles,
|
||||
vec![Handle::new(10), Handle::new(11), Handle::new(12)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -734,7 +777,11 @@ mod tests {
|
||||
},
|
||||
other => panic!("unexpected: {other:?}"),
|
||||
}
|
||||
send(&mut peer, &HostToPlugin::Response(Box::new(PluginResponse::Bool(true)))).unwrap();
|
||||
send(
|
||||
&mut peer,
|
||||
&HostToPlugin::Response(Box::new(PluginResponse::Bool(true))),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
assert!(api.update_entity(EntityType::Point(Point::new())));
|
||||
peer_handle.join().unwrap();
|
||||
@@ -754,7 +801,11 @@ mod tests {
|
||||
},
|
||||
other => panic!("unexpected: {other:?}"),
|
||||
}
|
||||
send(&mut peer, &HostToPlugin::Response(Box::new(PluginResponse::Bool(true)))).unwrap();
|
||||
send(
|
||||
&mut peer,
|
||||
&HostToPlugin::Response(Box::new(PluginResponse::Bool(true))),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
assert!(api.remove_entity(Handle::new(7)));
|
||||
peer_handle.join().unwrap();
|
||||
|
||||
@@ -23,13 +23,13 @@ pub mod client;
|
||||
#[cfg(feature = "host")]
|
||||
pub mod protocol;
|
||||
#[cfg(feature = "host")]
|
||||
pub mod proxy;
|
||||
#[cfg(feature = "host")]
|
||||
pub mod server;
|
||||
#[cfg(feature = "host")]
|
||||
pub mod transport;
|
||||
#[cfg(feature = "host")]
|
||||
pub mod v4;
|
||||
#[cfg(feature = "host")]
|
||||
pub mod proxy;
|
||||
|
||||
#[cfg(all(test, feature = "host"))]
|
||||
mod tests {
|
||||
|
||||
@@ -21,9 +21,9 @@ use crate::host::{CommandSource, CommandStep, HostSettingValue};
|
||||
use crate::manifest::ApiVersion;
|
||||
use crate::ribbon::owned::{OwnedPluginManifest, OwnedRibbonGroup};
|
||||
|
||||
pub use crate::host::PreviewWire;
|
||||
pub use codec::xdata::{ExtendedDataRecord, XDataValue};
|
||||
pub use codec::{CadDocument, EntityType, Handle};
|
||||
pub use crate::host::PreviewWire;
|
||||
|
||||
/// Events the host forwards to an active plugin `InteractiveCommand`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -50,7 +50,10 @@ pub enum InteractiveEvent {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum RunnerHandshake {
|
||||
Token(String),
|
||||
TokenV4 { token: String, protocol_version: u32 },
|
||||
TokenV4 {
|
||||
token: String,
|
||||
protocol_version: u32,
|
||||
},
|
||||
}
|
||||
|
||||
/// Environment variable through which the host passes the pre-shared
|
||||
@@ -83,7 +86,9 @@ pub enum HostRequest {
|
||||
tab_index: usize,
|
||||
},
|
||||
/// V7: release an interactive command after completion or cancellation.
|
||||
DropInteractive { command_id: u64 },
|
||||
DropInteractive {
|
||||
command_id: u64,
|
||||
},
|
||||
/// V7: cursor move preview update during an interactive command.
|
||||
CursorMove {
|
||||
command_id: u64,
|
||||
@@ -145,13 +150,19 @@ pub enum PluginRequest {
|
||||
AddEntities(Vec<EntityType>),
|
||||
/// V4: ask the host to create/refresh a tab-keyed shared-memory document
|
||||
/// view and return the file path + current version.
|
||||
OpenDocumentViewV4 { tab_id: u64 },
|
||||
OpenDocumentViewV4 {
|
||||
tab_id: u64,
|
||||
},
|
||||
/// V4: ask the host to close the tab-keyed shared-memory document view.
|
||||
CloseDocumentViewV4 { tab_id: u64 },
|
||||
CloseDocumentViewV4 {
|
||||
tab_id: u64,
|
||||
},
|
||||
/// V4: ask the host for the stable tab identifier of the active tab.
|
||||
GetTabId,
|
||||
/// V5: ask the host for the filesystem path of the document in `tab_id`.
|
||||
DocumentPath { tab_id: u64 },
|
||||
DocumentPath {
|
||||
tab_id: u64,
|
||||
},
|
||||
/// Add a layer to the active document with full initial properties.
|
||||
AddLayer(crate::host::LayerConfig),
|
||||
/// Modify specified properties of an existing layer in the active document.
|
||||
@@ -159,20 +170,36 @@ pub enum PluginRequest {
|
||||
/// Run a command on the active document tab's command line (AutoLISP style).
|
||||
ExecuteCommand(String),
|
||||
/// Read a host-managed setting without nested command dispatch.
|
||||
GetSystemVariable { name: String },
|
||||
GetSystemVariable {
|
||||
name: String,
|
||||
},
|
||||
/// Change a host-managed setting without nested command dispatch.
|
||||
SetSystemVariable { name: String, value: HostSettingValue },
|
||||
SetSystemVariable {
|
||||
name: String,
|
||||
value: HostSettingValue,
|
||||
},
|
||||
/// V7: validate and replace existing entities in a single undo step.
|
||||
UpdateEntitiesTransaction { label: String, entities: Vec<EntityType> },
|
||||
UpdateEntitiesTransaction {
|
||||
label: String,
|
||||
entities: Vec<EntityType>,
|
||||
},
|
||||
/// V7: synchronous selection read/write for the dispatch tab.
|
||||
GetSelection,
|
||||
SetSelection { handles: Vec<Handle> },
|
||||
SetSelection {
|
||||
handles: Vec<Handle>,
|
||||
},
|
||||
/// V7 (additive): kernel-backed solid create or transform.
|
||||
SolidOperation { operation: crate::host::SolidOperation },
|
||||
SolidOperation {
|
||||
operation: crate::host::SolidOperation,
|
||||
},
|
||||
/// V7 (additive): drawing table record create/modify/rename/delete.
|
||||
TableOperation { operation: crate::host::TableOperation },
|
||||
TableOperation {
|
||||
operation: crate::host::TableOperation,
|
||||
},
|
||||
/// V7 (additive): drive an OCS command.
|
||||
RunCommand { request: crate::host::CommandRequest },
|
||||
RunCommand {
|
||||
request: crate::host::CommandRequest,
|
||||
},
|
||||
}
|
||||
|
||||
/// Responses the host sends back for `PluginRequest`.
|
||||
|
||||
@@ -50,7 +50,10 @@ impl From<ProxyError> for PluginRequestError {
|
||||
}
|
||||
|
||||
/// Send a length-framed bincode message over a generic stream.
|
||||
pub fn send_framed<W: Write>(writer: &mut W, msg: &impl serde::Serialize) -> Result<(), ProxyError> {
|
||||
pub fn send_framed<W: Write>(
|
||||
writer: &mut W,
|
||||
msg: &impl serde::Serialize,
|
||||
) -> Result<(), ProxyError> {
|
||||
let bytes = bincode::serialize(msg)?;
|
||||
if bytes.len() > MAX_MESSAGE_SIZE {
|
||||
return Err(ProxyError::TooLarge(bytes.len()));
|
||||
@@ -63,7 +66,9 @@ pub fn send_framed<W: Write>(writer: &mut W, msg: &impl serde::Serialize) -> Res
|
||||
}
|
||||
|
||||
/// Receive a length-framed bincode message from a generic stream.
|
||||
pub fn recv_framed<R: Read, T: serde::de::DeserializeOwned>(reader: &mut R) -> Result<T, ProxyError> {
|
||||
pub fn recv_framed<R: Read, T: serde::de::DeserializeOwned>(
|
||||
reader: &mut R,
|
||||
) -> Result<T, ProxyError> {
|
||||
let mut len_buf = [0u8; 8];
|
||||
reader.read_exact(&mut len_buf)?;
|
||||
let len = u64::from_le_bytes(len_buf) as usize;
|
||||
@@ -114,10 +119,12 @@ fn read_with_poll<R: Read>(
|
||||
let mut off = 0;
|
||||
while off < buf.len() {
|
||||
match reader.read(&mut buf[off..]) {
|
||||
Ok(0) => return Err(PollError::Io(ProxyError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::UnexpectedEof,
|
||||
"proxy disconnected",
|
||||
)))),
|
||||
Ok(0) => {
|
||||
return Err(PollError::Io(ProxyError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::UnexpectedEof,
|
||||
"proxy disconnected",
|
||||
))))
|
||||
}
|
||||
Ok(n) => off += n,
|
||||
Err(e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => {
|
||||
if let Err(err) = poll() {
|
||||
@@ -386,7 +393,8 @@ mod tests {
|
||||
run_request_proxy(listener, sender, token).ok();
|
||||
});
|
||||
|
||||
let sender = ProxyPluginRequestSender::connect_with_token("127.0.0.1", port, &token).unwrap();
|
||||
let sender =
|
||||
ProxyPluginRequestSender::connect_with_token("127.0.0.1", port, &token).unwrap();
|
||||
let resp = sender
|
||||
.request(PluginRequest::PushInfo("hello proxy".to_string()))
|
||||
.unwrap();
|
||||
|
||||
@@ -110,8 +110,12 @@ pub fn handle_plugin_request(
|
||||
),
|
||||
GetSelection => PluginResponse::Selection(host.selection()),
|
||||
SetSelection { handles } => PluginResponse::SelectionResult(host.set_selection(&handles)),
|
||||
SolidOperation { operation } => PluginResponse::SolidResult(host.solid_operation(operation)),
|
||||
TableOperation { operation } => PluginResponse::TableResult(host.table_operation(operation)),
|
||||
SolidOperation { operation } => {
|
||||
PluginResponse::SolidResult(host.solid_operation(operation))
|
||||
}
|
||||
TableOperation { operation } => {
|
||||
PluginResponse::TableResult(host.table_operation(operation))
|
||||
}
|
||||
RunCommand { request } => PluginResponse::CommandResult(host.run_command(request)),
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,8 @@ use interprocess::local_socket::{GenericNamespaced, Stream, ToNsName};
|
||||
use interprocess::TryClone;
|
||||
|
||||
use crate::host::{
|
||||
DocumentReader, ExecutionResult, HostApi, HostNotification, InteractiveCommand,
|
||||
PluginNotification, PluginRequestSender, PluginRequestError, ReaderEntity, HostSettingValue,
|
||||
DocumentReader, ExecutionResult, HostApi, HostNotification, HostSettingValue,
|
||||
InteractiveCommand, PluginNotification, PluginRequestError, PluginRequestSender, ReaderEntity,
|
||||
};
|
||||
use crate::ipc::protocol::{
|
||||
HostRequest, HostResponse, PluginRequest, PluginResponse, RunnerHandshake,
|
||||
@@ -26,7 +26,7 @@ use crate::ipc::transport::{recv, send, TransportError};
|
||||
use crate::ipc::v4::protocol::{
|
||||
HostToPluginV4, NotificationEnvelope, PluginToHostV4, V4_PROTOCOL_VERSION,
|
||||
};
|
||||
use crate::shm::{DocumentViewInfo, SharedDocumentReader, DocumentViewData};
|
||||
use crate::shm::{DocumentViewData, DocumentViewInfo, SharedDocumentReader};
|
||||
|
||||
/// Default capacity of the bounded host→plugin notification queue.
|
||||
const DEFAULT_NOTIFY_QUEUE_SIZE: usize = 1024;
|
||||
@@ -89,8 +89,7 @@ impl V4Client {
|
||||
)))
|
||||
})?;
|
||||
|
||||
let (notify_tx, notify_rx) =
|
||||
mpsc::sync_channel(notify_queue_size());
|
||||
let (notify_tx, notify_rx) = mpsc::sync_channel(notify_queue_size());
|
||||
let (runner_tx, runner_rx) = mpsc::channel();
|
||||
|
||||
let shared = Arc::new(Shared {
|
||||
@@ -101,14 +100,7 @@ impl V4Client {
|
||||
|
||||
let shared_for_reader = Arc::clone(&shared);
|
||||
let notifications = Arc::new(Mutex::new(notify_rx));
|
||||
std::thread::spawn(move || {
|
||||
reader_thread(
|
||||
reader,
|
||||
shared_for_reader,
|
||||
notify_tx,
|
||||
runner_tx,
|
||||
)
|
||||
});
|
||||
std::thread::spawn(move || reader_thread(reader, shared_for_reader, notify_tx, runner_tx));
|
||||
|
||||
Ok(Self {
|
||||
shared,
|
||||
@@ -133,7 +125,11 @@ impl V4Client {
|
||||
|
||||
/// Non-blocking poll for host-to-plugin notifications.
|
||||
pub fn try_recv_notification(&self) -> Option<(Option<u64>, HostNotification)> {
|
||||
self.notifications.lock().unwrap_or_else(|e| e.into_inner()).try_recv().ok()
|
||||
self.notifications
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.try_recv()
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Send a plugin response for a host request.
|
||||
@@ -143,18 +139,11 @@ impl V4Client {
|
||||
resp: HostResponse,
|
||||
) -> Result<(), crate::ipc::transport::TransportError> {
|
||||
let mut writer = self.shared.writer.lock().unwrap_or_else(|e| e.into_inner());
|
||||
send(
|
||||
&mut writer,
|
||||
&PluginToHostV4::Response { id, payload: resp },
|
||||
)
|
||||
send(&mut writer, &PluginToHostV4::Response { id, payload: resp })
|
||||
}
|
||||
|
||||
/// Send a plugin-to-host notification.
|
||||
pub fn notify_plugin(
|
||||
&self,
|
||||
command_id: Option<u64>,
|
||||
notification: PluginNotification,
|
||||
) {
|
||||
pub fn notify_plugin(&self, command_id: Option<u64>, notification: PluginNotification) {
|
||||
let envelope = NotificationEnvelope {
|
||||
command_id,
|
||||
payload: notification,
|
||||
@@ -168,10 +157,7 @@ impl V4Client {
|
||||
/// Build a callback that sends a delayed `CodeExecutionResult` response for
|
||||
/// the given request id. Used by `start_execute_code` implementations that
|
||||
/// run REPL code on a background thread.
|
||||
pub fn execute_code_responder(
|
||||
&self,
|
||||
id: u64,
|
||||
) -> Box<dyn FnOnce(ExecutionResult) + Send> {
|
||||
pub fn execute_code_responder(&self, id: u64) -> Box<dyn FnOnce(ExecutionResult) + Send> {
|
||||
let shared = Arc::clone(&self.shared);
|
||||
Box::new(move |result| {
|
||||
let mut writer = shared.writer.lock().unwrap_or_else(|e| e.into_inner());
|
||||
@@ -235,7 +221,10 @@ fn reader_thread(
|
||||
}
|
||||
}
|
||||
Ok(HostToPluginV4::Notification(envelope)) => {
|
||||
if notify_tx.try_send((envelope.command_id, envelope.payload)).is_err() {
|
||||
if notify_tx
|
||||
.try_send((envelope.command_id, envelope.payload))
|
||||
.is_err()
|
||||
{
|
||||
let now = Instant::now();
|
||||
let should_log = last_drop_log
|
||||
.map(|t| now.duration_since(t) >= DROP_LOG_INTERVAL)
|
||||
@@ -247,7 +236,10 @@ fn reader_thread(
|
||||
}
|
||||
}
|
||||
Ok(HostToPluginV4::Request { id, payload }) => {
|
||||
if runner_tx.send(RunnerFrame::Request { id, payload }).is_err() {
|
||||
if runner_tx
|
||||
.send(RunnerFrame::Request { id, payload })
|
||||
.is_err()
|
||||
{
|
||||
// Runner loop has exited; stop reading.
|
||||
break;
|
||||
}
|
||||
@@ -510,9 +502,7 @@ impl HostApi for V4PluginHostApi {
|
||||
|
||||
fn write_record(&mut self, handle: Handle, record: ExtendedDataRecord) -> bool {
|
||||
let app = record.application_name.clone();
|
||||
match self
|
||||
.request(PluginRequest::WriteRecord { handle, record })
|
||||
{
|
||||
match self.request(PluginRequest::WriteRecord { handle, record }) {
|
||||
Ok(PluginResponse::Bool(b)) => {
|
||||
if b {
|
||||
self.record_cache.borrow_mut().remove(&(handle, app));
|
||||
@@ -631,16 +621,18 @@ impl HostApi for V4PluginHostApi {
|
||||
}
|
||||
}
|
||||
match self.doc_view.borrow().as_ref() {
|
||||
Some(info) => match SharedDocumentReader::<DocumentViewData>::open(Path::new(&info.path)) {
|
||||
Ok(reader) => Box::new(reader),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"[plugin] failed to open document view at {}: {e}",
|
||||
info.path
|
||||
);
|
||||
Box::new(EmptyDocumentReader)
|
||||
Some(info) => {
|
||||
match SharedDocumentReader::<DocumentViewData>::open(Path::new(&info.path)) {
|
||||
Ok(reader) => Box::new(reader),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"[plugin] failed to open document view at {}: {e}",
|
||||
info.path
|
||||
);
|
||||
Box::new(EmptyDocumentReader)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
None => Box::new(EmptyDocumentReader),
|
||||
}
|
||||
}
|
||||
@@ -657,7 +649,11 @@ impl HostApi for V4PluginHostApi {
|
||||
}
|
||||
|
||||
fn try_recv_notification(&mut self) -> Option<(Option<u64>, HostNotification)> {
|
||||
self.notifications.lock().unwrap_or_else(|e| e.into_inner()).try_recv().ok()
|
||||
self.notifications
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.try_recv()
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn plugin_request_sender(&self) -> Option<Box<dyn PluginRequestSender>> {
|
||||
@@ -734,7 +730,9 @@ impl HostApi for V4PluginHostApi {
|
||||
}
|
||||
|
||||
fn system_variable(&self, name: &str) -> Option<HostSettingValue> {
|
||||
match self.request(PluginRequest::GetSystemVariable { name: name.to_owned() }) {
|
||||
match self.request(PluginRequest::GetSystemVariable {
|
||||
name: name.to_owned(),
|
||||
}) {
|
||||
Ok(PluginResponse::SystemVariable(value)) => value,
|
||||
_ => None,
|
||||
}
|
||||
@@ -767,10 +765,13 @@ impl HostApi for V4PluginHostApi {
|
||||
entities: Vec<EntityType>,
|
||||
) -> Result<(), String> {
|
||||
match self.request(PluginRequest::UpdateEntitiesTransaction {
|
||||
label: label.to_owned(), entities,
|
||||
label: label.to_owned(),
|
||||
entities,
|
||||
}) {
|
||||
Ok(PluginResponse::EntityTransactionResult(result)) => {
|
||||
if result.is_ok() { self.document_cache = OnceCell::new(); }
|
||||
if result.is_ok() {
|
||||
self.document_cache = OnceCell::new();
|
||||
}
|
||||
result
|
||||
}
|
||||
Ok(PluginResponse::Error(error)) => Err(error),
|
||||
@@ -786,10 +787,15 @@ impl HostApi for V4PluginHostApi {
|
||||
}
|
||||
}
|
||||
|
||||
fn solid_operation(&mut self, operation: crate::host::SolidOperation) -> Result<Handle, String> {
|
||||
fn solid_operation(
|
||||
&mut self,
|
||||
operation: crate::host::SolidOperation,
|
||||
) -> Result<Handle, String> {
|
||||
match self.request(PluginRequest::SolidOperation { operation }) {
|
||||
Ok(PluginResponse::SolidResult(result)) => {
|
||||
if result.is_ok() { self.document_cache = OnceCell::new(); }
|
||||
if result.is_ok() {
|
||||
self.document_cache = OnceCell::new();
|
||||
}
|
||||
result
|
||||
}
|
||||
Ok(PluginResponse::Error(error)) => Err(error),
|
||||
@@ -798,7 +804,10 @@ impl HostApi for V4PluginHostApi {
|
||||
}
|
||||
}
|
||||
|
||||
fn run_command(&mut self, request: crate::host::CommandRequest) -> Result<crate::host::CommandOutcome, String> {
|
||||
fn run_command(
|
||||
&mut self,
|
||||
request: crate::host::CommandRequest,
|
||||
) -> Result<crate::host::CommandOutcome, String> {
|
||||
match self.request(PluginRequest::RunCommand { request }) {
|
||||
Ok(PluginResponse::CommandResult(result)) => {
|
||||
self.document_cache = OnceCell::new();
|
||||
@@ -810,10 +819,15 @@ impl HostApi for V4PluginHostApi {
|
||||
}
|
||||
}
|
||||
|
||||
fn table_operation(&mut self, operation: crate::host::TableOperation) -> Result<Handle, String> {
|
||||
fn table_operation(
|
||||
&mut self,
|
||||
operation: crate::host::TableOperation,
|
||||
) -> Result<Handle, String> {
|
||||
match self.request(PluginRequest::TableOperation { operation }) {
|
||||
Ok(PluginResponse::TableResult(result)) => {
|
||||
if result.is_ok() { self.document_cache = OnceCell::new(); }
|
||||
if result.is_ok() {
|
||||
self.document_cache = OnceCell::new();
|
||||
}
|
||||
result
|
||||
}
|
||||
Ok(PluginResponse::Error(error)) => Err(error),
|
||||
@@ -823,7 +837,9 @@ impl HostApi for V4PluginHostApi {
|
||||
}
|
||||
|
||||
fn set_selection(&mut self, handles: &[Handle]) -> Result<(), String> {
|
||||
match self.request(PluginRequest::SetSelection { handles: handles.to_vec() }) {
|
||||
match self.request(PluginRequest::SetSelection {
|
||||
handles: handles.to_vec(),
|
||||
}) {
|
||||
Ok(PluginResponse::SelectionResult(result)) => result,
|
||||
Ok(PluginResponse::Error(error)) => Err(error),
|
||||
Ok(other) => Err(format!("unexpected selection response: {other:?}")),
|
||||
@@ -899,9 +915,7 @@ impl V4Client {
|
||||
});
|
||||
let shared_for_reader = Arc::clone(&shared);
|
||||
let notifications = Arc::new(Mutex::new(notify_rx));
|
||||
std::thread::spawn(move || {
|
||||
reader_thread(reader, shared_for_reader, notify_tx, runner_tx)
|
||||
});
|
||||
std::thread::spawn(move || reader_thread(reader, shared_for_reader, notify_tx, runner_tx));
|
||||
Self {
|
||||
shared,
|
||||
notifications,
|
||||
@@ -1081,7 +1095,10 @@ mod tests {
|
||||
});
|
||||
|
||||
assert!(matches!(first.join().unwrap(), PluginResponse::Bool(true)));
|
||||
assert!(matches!(second.join().unwrap(), PluginResponse::Bool(false)));
|
||||
assert!(matches!(
|
||||
second.join().unwrap(),
|
||||
PluginResponse::Bool(false)
|
||||
));
|
||||
runner.join().unwrap();
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,10 @@ pub enum PluginToHostV4 {
|
||||
tab_id: Option<u64>,
|
||||
payload: PluginRequest,
|
||||
},
|
||||
Response { id: u64, payload: HostResponse },
|
||||
Response {
|
||||
id: u64,
|
||||
payload: HostResponse,
|
||||
},
|
||||
Notification(NotificationEnvelope<PluginNotification>),
|
||||
}
|
||||
|
||||
@@ -95,11 +98,17 @@ mod tests {
|
||||
let bytes = bincode::serialize(¬ification).unwrap();
|
||||
let decoded: HostNotification = bincode::deserialize(&bytes).unwrap();
|
||||
assert_eq!(decoded, notification);
|
||||
let stopped = HostNotification::CommandStateChanged { tab_id: 42, command: None };
|
||||
let stopped = HostNotification::CommandStateChanged {
|
||||
tab_id: 42,
|
||||
command: None,
|
||||
};
|
||||
let bytes = bincode::serialize(&stopped).unwrap();
|
||||
let decoded: HostNotification = bincode::deserialize(&bytes).unwrap();
|
||||
assert_eq!(decoded, stopped);
|
||||
let changed = HostNotification::DrawingChanged { tab_id: 42, epoch: 7 };
|
||||
let changed = HostNotification::DrawingChanged {
|
||||
tab_id: 42,
|
||||
epoch: 7,
|
||||
};
|
||||
let bytes = bincode::serialize(&changed).unwrap();
|
||||
let decoded: HostNotification = bincode::deserialize(&bytes).unwrap();
|
||||
assert_eq!(decoded, changed);
|
||||
@@ -109,19 +118,33 @@ mod tests {
|
||||
fn command_request_and_outcome_roundtrip() {
|
||||
use crate::host::{CommandOutcome, CommandRequest};
|
||||
for request in [
|
||||
CommandRequest::Run { line: "LINE 0,0 1,1".into() },
|
||||
CommandRequest::Start { name: "OFFSET".into() },
|
||||
CommandRequest::Point { point: [1.0, 2.0, 3.0] },
|
||||
CommandRequest::Run {
|
||||
line: "LINE 0,0 1,1".into(),
|
||||
},
|
||||
CommandRequest::Start {
|
||||
name: "OFFSET".into(),
|
||||
},
|
||||
CommandRequest::Point {
|
||||
point: [1.0, 2.0, 3.0],
|
||||
},
|
||||
CommandRequest::Text { text: "2".into() },
|
||||
CommandRequest::Token { text: "R".into() },
|
||||
CommandRequest::Entity { handle: codec::Handle::new(9), point: [0.0; 3] },
|
||||
CommandRequest::Entity {
|
||||
handle: codec::Handle::new(9),
|
||||
point: [0.0; 3],
|
||||
},
|
||||
CommandRequest::Selection,
|
||||
CommandRequest::Enter,
|
||||
CommandRequest::Cancel,
|
||||
] {
|
||||
let bytes = bincode::serialize(&PluginRequest::RunCommand { request: request.clone() }).unwrap();
|
||||
assert!(matches!(bincode::deserialize::<PluginRequest>(&bytes).unwrap(),
|
||||
PluginRequest::RunCommand { request: decoded } if decoded == request));
|
||||
let bytes = bincode::serialize(&PluginRequest::RunCommand {
|
||||
request: request.clone(),
|
||||
})
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(bincode::deserialize::<PluginRequest>(&bytes).unwrap(),
|
||||
PluginRequest::RunCommand { request: decoded } if decoded == request)
|
||||
);
|
||||
}
|
||||
let outcome = CommandOutcome {
|
||||
status: "waiting_input".into(),
|
||||
@@ -137,8 +160,10 @@ mod tests {
|
||||
};
|
||||
for result in [Ok(outcome), Err("refused".to_owned())] {
|
||||
let bytes = bincode::serialize(&PluginResponse::CommandResult(result.clone())).unwrap();
|
||||
assert!(matches!(bincode::deserialize::<PluginResponse>(&bytes).unwrap(),
|
||||
PluginResponse::CommandResult(decoded) if decoded == result));
|
||||
assert!(
|
||||
matches!(bincode::deserialize::<PluginResponse>(&bytes).unwrap(),
|
||||
PluginResponse::CommandResult(decoded) if decoded == result)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,19 +180,60 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
TableOperation::LayerModify { config: LayerConfig { name: "Walls".into(), off: Some(true), ..Default::default() } },
|
||||
TableOperation::LayerRename { from: "A".into(), to: "B".into() },
|
||||
TableOperation::LayerDelete { name: "A".into(), erase_objects: true },
|
||||
TableOperation::LayerModify {
|
||||
config: LayerConfig {
|
||||
name: "Walls".into(),
|
||||
off: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
TableOperation::LayerRename {
|
||||
from: "A".into(),
|
||||
to: "B".into(),
|
||||
},
|
||||
TableOperation::LayerDelete {
|
||||
name: "A".into(),
|
||||
erase_objects: true,
|
||||
},
|
||||
TableOperation::LayerSetCurrent { name: "A".into() },
|
||||
TableOperation::TextStyleCreate {
|
||||
config: crate::host::TextStyleConfig { name: "T".into(), height: Some(2.5), oblique_angle: Some(0.2), backward: Some(true), ..Default::default() },
|
||||
config: crate::host::TextStyleConfig {
|
||||
name: "T".into(),
|
||||
height: Some(2.5),
|
||||
oblique_angle: Some(0.2),
|
||||
backward: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
TableOperation::TextStyleModify {
|
||||
config: crate::host::TextStyleConfig {
|
||||
name: "T".into(),
|
||||
font_file: Some("romans".into()),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
TableOperation::DimStyleCreate {
|
||||
name: "D".into(),
|
||||
copy_from: Some("Standard".into()),
|
||||
properties: "{\"dimscale\":2}".into(),
|
||||
},
|
||||
TableOperation::DimStyleModify {
|
||||
name: "D".into(),
|
||||
properties: "{}".into(),
|
||||
},
|
||||
TableOperation::StyleRename {
|
||||
kind: crate::host::TableStyleKind::Text,
|
||||
from: "A".into(),
|
||||
to: "B".into(),
|
||||
},
|
||||
TableOperation::StyleDelete {
|
||||
kind: crate::host::TableStyleKind::Dim,
|
||||
name: "A".into(),
|
||||
},
|
||||
TableOperation::StyleSetCurrent {
|
||||
kind: crate::host::TableStyleKind::Dim,
|
||||
name: "A".into(),
|
||||
},
|
||||
TableOperation::TextStyleModify { config: crate::host::TextStyleConfig { name: "T".into(), font_file: Some("romans".into()), ..Default::default() } },
|
||||
TableOperation::DimStyleCreate { name: "D".into(), copy_from: Some("Standard".into()), properties: "{\"dimscale\":2}".into() },
|
||||
TableOperation::DimStyleModify { name: "D".into(), properties: "{}".into() },
|
||||
TableOperation::StyleRename { kind: crate::host::TableStyleKind::Text, from: "A".into(), to: "B".into() },
|
||||
TableOperation::StyleDelete { kind: crate::host::TableStyleKind::Dim, name: "A".into() },
|
||||
TableOperation::StyleSetCurrent { kind: crate::host::TableStyleKind::Dim, name: "A".into() },
|
||||
TableOperation::BlockCreate {
|
||||
name: "B".into(),
|
||||
entities: vec![codec::Handle::new(4), codec::Handle::new(5)],
|
||||
@@ -175,31 +241,67 @@ mod tests {
|
||||
erase_originals: true,
|
||||
description: Some("d".into()),
|
||||
},
|
||||
TableOperation::BlockModify { name: "B".into(), description: None, explodable: Some(false), scale_uniformly: Some(true) },
|
||||
TableOperation::BlockRename { from: "B".into(), to: "C".into() },
|
||||
TableOperation::BlockModify {
|
||||
name: "B".into(),
|
||||
description: None,
|
||||
explodable: Some(false),
|
||||
scale_uniformly: Some(true),
|
||||
},
|
||||
TableOperation::BlockRename {
|
||||
from: "B".into(),
|
||||
to: "C".into(),
|
||||
},
|
||||
TableOperation::BlockDelete { name: "C".into() },
|
||||
TableOperation::BlockEntityAdd {
|
||||
block: "C".into(),
|
||||
entity: codec::EntityType::Line(codec::entities::Line::new()),
|
||||
},
|
||||
TableOperation::LinetypeCreate { name: "L".into(), description: "d".into(), pattern: vec![1.0, -1.0] },
|
||||
TableOperation::LinetypeModify { name: "L".into(), description: None, pattern: Some(vec![2.0, -2.0]) },
|
||||
TableOperation::LinetypeRename { from: "L".into(), to: "M".into() },
|
||||
TableOperation::LinetypeCreate {
|
||||
name: "L".into(),
|
||||
description: "d".into(),
|
||||
pattern: vec![1.0, -1.0],
|
||||
},
|
||||
TableOperation::LinetypeModify {
|
||||
name: "L".into(),
|
||||
description: None,
|
||||
pattern: Some(vec![2.0, -2.0]),
|
||||
},
|
||||
TableOperation::LinetypeRename {
|
||||
from: "L".into(),
|
||||
to: "M".into(),
|
||||
},
|
||||
TableOperation::LinetypeDelete { name: "M".into() },
|
||||
TableOperation::LayoutCreate { name: "S".into() },
|
||||
TableOperation::LayoutRename { from: "S".into(), to: "T".into() },
|
||||
TableOperation::LayoutRename {
|
||||
from: "S".into(),
|
||||
to: "T".into(),
|
||||
},
|
||||
TableOperation::LayoutDelete { name: "T".into() },
|
||||
TableOperation::LayoutSetCurrent { name: "Model".into() },
|
||||
TableOperation::LayoutSetPage { name: "S".into(), paper_size: Some([420.0, 297.0]), rotation: Some(90), scale: Some([1.0, 50.0]) },
|
||||
TableOperation::LayoutSetCurrent {
|
||||
name: "Model".into(),
|
||||
},
|
||||
TableOperation::LayoutSetPage {
|
||||
name: "S".into(),
|
||||
paper_size: Some([420.0, 297.0]),
|
||||
rotation: Some(90),
|
||||
scale: Some([1.0, 50.0]),
|
||||
},
|
||||
] {
|
||||
let bytes = bincode::serialize(&PluginRequest::TableOperation { operation: operation.clone() }).unwrap();
|
||||
assert!(matches!(bincode::deserialize::<PluginRequest>(&bytes).unwrap(),
|
||||
PluginRequest::TableOperation { operation: decoded } if decoded == operation));
|
||||
let bytes = bincode::serialize(&PluginRequest::TableOperation {
|
||||
operation: operation.clone(),
|
||||
})
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(bincode::deserialize::<PluginRequest>(&bytes).unwrap(),
|
||||
PluginRequest::TableOperation { operation: decoded } if decoded == operation)
|
||||
);
|
||||
}
|
||||
for result in [Ok(codec::Handle::new(9)), Err("refused".to_owned())] {
|
||||
let bytes = bincode::serialize(&PluginResponse::TableResult(result.clone())).unwrap();
|
||||
assert!(matches!(bincode::deserialize::<PluginResponse>(&bytes).unwrap(),
|
||||
PluginResponse::TableResult(decoded) if decoded == result));
|
||||
assert!(
|
||||
matches!(bincode::deserialize::<PluginResponse>(&bytes).unwrap(),
|
||||
PluginResponse::TableResult(decoded) if decoded == result)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,12 +310,29 @@ mod tests {
|
||||
use crate::host::{SolidOperation, SolidPrimitive};
|
||||
for operation in [
|
||||
SolidOperation::Create {
|
||||
primitive: SolidPrimitive::Pyramid { center: [1.0, 2.0, 3.0], radius: 4.0, height: 5.0, sides: 6 },
|
||||
primitive: SolidPrimitive::Pyramid {
|
||||
center: [1.0, 2.0, 3.0],
|
||||
radius: 4.0,
|
||||
height: 5.0,
|
||||
sides: 6,
|
||||
},
|
||||
layer: Some("SOLIDS".into()),
|
||||
},
|
||||
SolidOperation::Transform { handle: codec::Handle::new(7), matrix: [1.0; 16] },
|
||||
SolidOperation::RegionFromProfile { source: codec::Handle::new(3), layer: None, delete_source: true },
|
||||
SolidOperation::EmbedPicture { path: "p.png".into(), origin: [0.0; 3], width: 5.0, layer: None },
|
||||
SolidOperation::Transform {
|
||||
handle: codec::Handle::new(7),
|
||||
matrix: [1.0; 16],
|
||||
},
|
||||
SolidOperation::RegionFromProfile {
|
||||
source: codec::Handle::new(3),
|
||||
layer: None,
|
||||
delete_source: true,
|
||||
},
|
||||
SolidOperation::EmbedPicture {
|
||||
path: "p.png".into(),
|
||||
origin: [0.0; 3],
|
||||
width: 5.0,
|
||||
layer: None,
|
||||
},
|
||||
SolidOperation::Boolean {
|
||||
first: codec::Handle::new(1),
|
||||
second: codec::Handle::new(2),
|
||||
@@ -221,30 +340,52 @@ mod tests {
|
||||
layer: None,
|
||||
keep_operands: false,
|
||||
},
|
||||
SolidOperation::SurfaceFromProfile { source: codec::Handle::new(4), layer: Some("S".into()), delete_source: false },
|
||||
SolidOperation::Extrude { source: codec::Handle::new(5), direction: [0.0, 0.0, 2.0], layer: None, delete_source: true },
|
||||
SolidOperation::SurfaceFromProfile {
|
||||
source: codec::Handle::new(4),
|
||||
layer: Some("S".into()),
|
||||
delete_source: false,
|
||||
},
|
||||
SolidOperation::Extrude {
|
||||
source: codec::Handle::new(5),
|
||||
direction: [0.0, 0.0, 2.0],
|
||||
layer: None,
|
||||
delete_source: true,
|
||||
},
|
||||
] {
|
||||
let bytes = bincode::serialize(&PluginRequest::SolidOperation { operation: operation.clone() }).unwrap();
|
||||
assert!(matches!(bincode::deserialize::<PluginRequest>(&bytes).unwrap(),
|
||||
PluginRequest::SolidOperation { operation: decoded } if decoded == operation));
|
||||
let bytes = bincode::serialize(&PluginRequest::SolidOperation {
|
||||
operation: operation.clone(),
|
||||
})
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(bincode::deserialize::<PluginRequest>(&bytes).unwrap(),
|
||||
PluginRequest::SolidOperation { operation: decoded } if decoded == operation)
|
||||
);
|
||||
}
|
||||
for result in [Ok(codec::Handle::new(9)), Err("refused".to_owned())] {
|
||||
let bytes = bincode::serialize(&PluginResponse::SolidResult(result.clone())).unwrap();
|
||||
assert!(matches!(bincode::deserialize::<PluginResponse>(&bytes).unwrap(),
|
||||
PluginResponse::SolidResult(decoded) if decoded == result));
|
||||
assert!(
|
||||
matches!(bincode::deserialize::<PluginResponse>(&bytes).unwrap(),
|
||||
PluginResponse::SolidResult(decoded) if decoded == result)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_request_and_response_roundtrip() {
|
||||
let request = PluginRequest::SetSelection { handles: vec![codec::Handle::new(9)] };
|
||||
let request = PluginRequest::SetSelection {
|
||||
handles: vec![codec::Handle::new(9)],
|
||||
};
|
||||
let bytes = bincode::serialize(&request).unwrap();
|
||||
assert!(matches!(bincode::deserialize::<PluginRequest>(&bytes).unwrap(),
|
||||
PluginRequest::SetSelection { handles } if handles == vec![codec::Handle::new(9)]));
|
||||
assert!(
|
||||
matches!(bincode::deserialize::<PluginRequest>(&bytes).unwrap(),
|
||||
PluginRequest::SetSelection { handles } if handles == vec![codec::Handle::new(9)])
|
||||
);
|
||||
let response = PluginResponse::Selection(vec![codec::Handle::new(9)]);
|
||||
let bytes = bincode::serialize(&response).unwrap();
|
||||
assert!(matches!(bincode::deserialize::<PluginResponse>(&bytes).unwrap(),
|
||||
PluginResponse::Selection(handles) if handles == vec![codec::Handle::new(9)]));
|
||||
assert!(
|
||||
matches!(bincode::deserialize::<PluginResponse>(&bytes).unwrap(),
|
||||
PluginResponse::Selection(handles) if handles == vec![codec::Handle::new(9)])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -26,7 +26,10 @@ macro_rules! vlog {
|
||||
|
||||
/// A frame delivered from the V4 reader thread to the host main thread.
|
||||
pub enum HostIncoming {
|
||||
Response { id: u64, payload: crate::ipc::protocol::HostResponse },
|
||||
Response {
|
||||
id: u64,
|
||||
payload: crate::ipc::protocol::HostResponse,
|
||||
},
|
||||
Request {
|
||||
id: u64,
|
||||
tab_id: Option<u64>,
|
||||
|
||||
@@ -32,12 +32,12 @@
|
||||
//! - `runner` — child-process runner entry point.
|
||||
//! - `shm` — shared-memory document views.
|
||||
|
||||
pub mod entity_coverage;
|
||||
pub mod entity_coverage_types;
|
||||
pub mod manifest;
|
||||
pub mod ribbon;
|
||||
pub mod type_registry;
|
||||
pub mod type_registry_types;
|
||||
pub mod entity_coverage;
|
||||
pub mod entity_coverage_types;
|
||||
pub mod version_info;
|
||||
|
||||
/// Runtime host surface — only built with the `host` feature (pulls `opencadcodec`).
|
||||
@@ -66,17 +66,19 @@ pub mod shm;
|
||||
#[cfg(feature = "host")]
|
||||
pub mod runner;
|
||||
|
||||
pub use entity_coverage::{
|
||||
get_embedded_entity_coverage_json, EntityCoverageCatalog, EntityKindCoverage, EntityScope,
|
||||
ModelAccess, PropertyCoverage,
|
||||
};
|
||||
pub use manifest::{
|
||||
effective_max_api_version, host_accepts_plugin_version, ApiVersion, PluginManifest,
|
||||
API_VERSION, API_VERSION_MIN_SUPPORTED, MAX_API_VERSION_ENV,
|
||||
};
|
||||
pub use ribbon::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, StyleKey, ToolDef};
|
||||
pub use type_registry::{
|
||||
get_embedded_type_registry_json, EnumVariantInfo, FieldInfo, MethodInfo, ParameterInfo,
|
||||
TypeId, TypeInfo, TypeKind, TypeRegistry,
|
||||
get_embedded_type_registry_json, EnumVariantInfo, FieldInfo, MethodInfo, ParameterInfo, TypeId,
|
||||
TypeInfo, TypeKind, TypeRegistry,
|
||||
};
|
||||
pub use entity_coverage::{get_embedded_entity_coverage_json, EntityCoverageCatalog,
|
||||
EntityKindCoverage, EntityScope, ModelAccess, PropertyCoverage};
|
||||
pub use version_info::get_embedded_version_info_json;
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -28,7 +28,9 @@ use std::time::{Duration, Instant};
|
||||
use interprocess::local_socket::traits::Listener;
|
||||
use interprocess::local_socket::{GenericNamespaced, ListenerOptions, Stream, ToNsName};
|
||||
|
||||
use crate::host::{CommandSource, CommandStep, ExecutionResult, HostApi, HostNotification, PluginNotification};
|
||||
use crate::host::{
|
||||
CommandSource, CommandStep, ExecutionResult, HostApi, HostNotification, PluginNotification,
|
||||
};
|
||||
use crate::ipc::protocol::{
|
||||
CadDocument, EntityType, Handle, HostRequest, HostResponse, HostToPlugin, InteractiveEvent,
|
||||
PluginToHost, RunnerHandshake, PLUGIN_TOKEN_ENV,
|
||||
@@ -100,7 +102,11 @@ impl HostApi for NullHost {
|
||||
) -> Option<&crate::ipc::protocol::ExtendedDataRecord> {
|
||||
None
|
||||
}
|
||||
fn write_record(&mut self, _handle: Handle, _record: crate::ipc::protocol::ExtendedDataRecord) -> bool {
|
||||
fn write_record(
|
||||
&mut self,
|
||||
_handle: Handle,
|
||||
_record: crate::ipc::protocol::ExtendedDataRecord,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
fn remove_record(&mut self, _handle: Handle, _app_name: &str) -> bool {
|
||||
@@ -112,10 +118,7 @@ impl HostApi for NullHost {
|
||||
fn push_output(&mut self, _msg: &str) {}
|
||||
fn push_error(&mut self, _msg: &str) {}
|
||||
fn start_interactive(&mut self, _command: Box<dyn crate::host::InteractiveCommand>) {}
|
||||
fn plugin_state_any(
|
||||
&self,
|
||||
_plugin_id: &str,
|
||||
) -> Option<&(dyn std::any::Any + Send + Sync)> {
|
||||
fn plugin_state_any(&self, _plugin_id: &str) -> Option<&(dyn std::any::Any + Send + Sync)> {
|
||||
None
|
||||
}
|
||||
fn plugin_state_any_mut(
|
||||
@@ -419,7 +422,11 @@ impl PluginProcess {
|
||||
// the V4 reader thread. The notification handler receives an empty
|
||||
// plugin id until we read the manifest below; that brief window is
|
||||
// fine because the plugin has not yet been asked to do work.
|
||||
let stream = stream.lock().unwrap_or_else(|e| e.into_inner()).take().expect("V4 stream");
|
||||
let stream = stream
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.take()
|
||||
.expect("V4 stream");
|
||||
let plugin_id_placeholder = Arc::new(std::sync::Mutex::new(String::new()));
|
||||
let handler_for_v4: Arc<dyn Fn(Option<u64>, PluginNotification) + Send + Sync> = {
|
||||
let plugin_id = Arc::clone(&plugin_id_placeholder);
|
||||
@@ -434,7 +441,9 @@ impl PluginProcess {
|
||||
HostResponse::Manifest(m) => m,
|
||||
other => return Err(PluginError::UnexpectedResponse(Box::new(other))),
|
||||
};
|
||||
*plugin_id_placeholder.lock().unwrap_or_else(|e| e.into_inner()) = manifest.id.clone();
|
||||
*plugin_id_placeholder
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = manifest.id.clone();
|
||||
ribbon = match v4_conn.call(host, HostRequest::GetRibbon, no_op)? {
|
||||
HostResponse::Ribbon(r) => r,
|
||||
other => return Err(PluginError::UnexpectedResponse(Box::new(other))),
|
||||
@@ -626,8 +635,14 @@ impl PluginProcess {
|
||||
|
||||
/// Release a completed or abandoned V4 interactive command in the runner.
|
||||
pub fn drop_interactive(&self, command_id: u64) -> Result<(), PluginError> {
|
||||
let Some(v4) = &self.v4 else { return Ok(()); };
|
||||
match v4.call(&mut NullHost, HostRequest::DropInteractive { command_id }, &mut |_| {})? {
|
||||
let Some(v4) = &self.v4 else {
|
||||
return Ok(());
|
||||
};
|
||||
match v4.call(
|
||||
&mut NullHost,
|
||||
HostRequest::DropInteractive { command_id },
|
||||
&mut |_| {},
|
||||
)? {
|
||||
HostResponse::Bool(true) => Ok(()),
|
||||
other => Err(PluginError::UnexpectedResponse(Box::new(other))),
|
||||
}
|
||||
@@ -886,10 +901,7 @@ fn format_exit_status(status: Option<std::process::ExitStatus>) -> String {
|
||||
|
||||
/// Describe why a runner failed before/during connection, including any stderr
|
||||
/// it produced.
|
||||
fn spawn_failure_status(
|
||||
child: &Mutex<Option<Child>>,
|
||||
last_stderr: &Mutex<String>,
|
||||
) -> String {
|
||||
fn spawn_failure_status(child: &Mutex<Option<Child>>, last_stderr: &Mutex<String>) -> String {
|
||||
let mut parts = vec![format_exit_status(child_status(child))];
|
||||
if let Ok(line) = last_stderr.lock() {
|
||||
if !line.is_empty() {
|
||||
@@ -1259,7 +1271,10 @@ mod timeout_tests {
|
||||
fn push_undo(&mut self, _label: &str) {}
|
||||
fn set_dirty(&mut self) {}
|
||||
fn push_info(&mut self, msg: &str) {
|
||||
self.push_info_messages.lock().unwrap().push(msg.to_string());
|
||||
self.push_info_messages
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(msg.to_string());
|
||||
}
|
||||
fn push_output(&mut self, _msg: &str) {}
|
||||
fn push_error(&mut self, _msg: &str) {}
|
||||
@@ -1433,7 +1448,9 @@ mod timeout_tests {
|
||||
)
|
||||
.expect("send nested request");
|
||||
let resp = recv::<HostToPlugin>(&mut peer).expect("read nested response");
|
||||
assert!(matches!(resp, HostToPlugin::Response(ref r) if matches!(**r, PluginResponse::Ok)));
|
||||
assert!(
|
||||
matches!(resp, HostToPlugin::Response(ref r) if matches!(**r, PluginResponse::Ok))
|
||||
);
|
||||
send(&mut peer, &PluginToHost::Response(HostResponse::Bool(true)))
|
||||
.expect("send final response");
|
||||
});
|
||||
@@ -1443,7 +1460,11 @@ mod timeout_tests {
|
||||
assert!(result.expect("dispatch succeeds"));
|
||||
assert!(process.is_alive(), "process should still be alive");
|
||||
let infos = host.take_push_info();
|
||||
assert_eq!(infos, vec!["hello".to_string()], "push_info should be delivered to host");
|
||||
assert_eq!(
|
||||
infos,
|
||||
vec!["hello".to_string()],
|
||||
"push_info should be delivered to host"
|
||||
);
|
||||
|
||||
// Clean up the helper child so it does not outlive the test.
|
||||
if let Some(mut child) = process
|
||||
@@ -1482,7 +1503,10 @@ mod timeout_tests {
|
||||
RunnerHandshake::Token("correct-token".to_string()),
|
||||
"correct-token",
|
||||
);
|
||||
assert!(result.is_ok(), "expected authentication success, got {result:?}");
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"expected authentication success, got {result:?}"
|
||||
);
|
||||
assert!(!result.unwrap(), "V3 token should select V3 path");
|
||||
}
|
||||
|
||||
@@ -1497,7 +1521,10 @@ mod timeout_tests {
|
||||
},
|
||||
"correct-token",
|
||||
);
|
||||
assert!(result.is_ok(), "expected authentication success, got {result:?}");
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"expected authentication success, got {result:?}"
|
||||
);
|
||||
assert!(result.unwrap(), "TokenV4 should select V4 path");
|
||||
}
|
||||
|
||||
|
||||
@@ -97,11 +97,7 @@ impl PluginManager {
|
||||
|
||||
/// Fan out a host notification to every alive V4 plugin. Per-process errors
|
||||
/// are logged; this is best-effort.
|
||||
pub fn broadcast_notification(
|
||||
&self,
|
||||
command_id: Option<u64>,
|
||||
notification: HostNotification,
|
||||
) {
|
||||
pub fn broadcast_notification(&self, command_id: Option<u64>, notification: HostNotification) {
|
||||
for p in &self.plugins {
|
||||
if !p.process.is_alive() {
|
||||
continue;
|
||||
|
||||
@@ -145,12 +145,7 @@ impl V4Connection {
|
||||
payload,
|
||||
}) => {
|
||||
if tab_id.is_none_or(|request_tab| request_tab == host.tab_id()) {
|
||||
self.respond_to_plugin_request(
|
||||
host,
|
||||
rid,
|
||||
payload,
|
||||
on_start_interactive,
|
||||
)?;
|
||||
self.respond_to_plugin_request(host, rid, payload, on_start_interactive)?;
|
||||
} else {
|
||||
self.deferred
|
||||
.lock()
|
||||
@@ -167,9 +162,7 @@ impl V4Connection {
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||
self.shared.alive.store(false, Ordering::SeqCst);
|
||||
return Err(PluginError::Runner(
|
||||
"V4 reader thread disconnected".into(),
|
||||
));
|
||||
return Err(PluginError::Runner("V4 reader thread disconnected".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,12 +210,7 @@ impl V4Connection {
|
||||
payload,
|
||||
}) => {
|
||||
if tab_id.is_none_or(|request_tab| request_tab == current_tab_id) {
|
||||
self.respond_to_plugin_request(
|
||||
host,
|
||||
id,
|
||||
payload,
|
||||
on_start_interactive,
|
||||
)?;
|
||||
self.respond_to_plugin_request(host, id, payload, on_start_interactive)?;
|
||||
} else {
|
||||
self.deferred
|
||||
.lock()
|
||||
@@ -238,9 +226,7 @@ impl V4Connection {
|
||||
Err(mpsc::TryRecvError::Empty) => return Ok(()),
|
||||
Err(mpsc::TryRecvError::Disconnected) => {
|
||||
self.shared.alive.store(false, Ordering::SeqCst);
|
||||
return Err(PluginError::Runner(
|
||||
"V4 reader thread disconnected".into(),
|
||||
));
|
||||
return Err(PluginError::Runner("V4 reader thread disconnected".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -324,7 +310,11 @@ impl V4Connection {
|
||||
while self.shared.alive.load(Ordering::SeqCst) && Instant::now() < deadline {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
let _ = self.reader_handle.lock().unwrap_or_else(|e| e.into_inner()).take();
|
||||
let _ = self
|
||||
.reader_handle
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.take();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,20 +432,23 @@ mod tests {
|
||||
label: &str,
|
||||
entities: Vec<codec::EntityType>,
|
||||
) -> Result<(), String> {
|
||||
self.transactions.lock().unwrap().push((label.to_owned(), entities));
|
||||
self.transactions
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((label.to_owned(), entities));
|
||||
Ok(())
|
||||
}
|
||||
fn set_dirty(&mut self) {}
|
||||
fn push_info(&mut self, msg: &str) {
|
||||
self.push_info_messages.lock().unwrap().push(msg.to_string());
|
||||
self.push_info_messages
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(msg.to_string());
|
||||
}
|
||||
fn push_output(&mut self, _msg: &str) {}
|
||||
fn push_error(&mut self, _msg: &str) {}
|
||||
fn start_interactive(&mut self, _command: Box<dyn crate::host::InteractiveCommand>) {}
|
||||
fn plugin_state_any(
|
||||
&self,
|
||||
_plugin_id: &str,
|
||||
) -> Option<&(dyn std::any::Any + Send + Sync)> {
|
||||
fn plugin_state_any(&self, _plugin_id: &str) -> Option<&(dyn std::any::Any + Send + Sync)> {
|
||||
None
|
||||
}
|
||||
fn plugin_state_any_mut(
|
||||
@@ -537,7 +530,10 @@ mod tests {
|
||||
// Read the host's V4 request and respond.
|
||||
let req = recv::<HostToPluginV4>(&mut runner_stream).unwrap();
|
||||
match req {
|
||||
HostToPluginV4::Request { id, payload: HostRequest::Dispatch { cmd } } => {
|
||||
HostToPluginV4::Request {
|
||||
id,
|
||||
payload: HostRequest::Dispatch { cmd },
|
||||
} => {
|
||||
assert_eq!(cmd, "HELLO");
|
||||
send(
|
||||
&mut runner_stream,
|
||||
@@ -554,7 +550,13 @@ mod tests {
|
||||
|
||||
let mut host = DummyHost::new();
|
||||
let resp = conn
|
||||
.call(&mut host, HostRequest::Dispatch { cmd: "HELLO".to_string() }, &mut |_| {})
|
||||
.call(
|
||||
&mut host,
|
||||
HostRequest::Dispatch {
|
||||
cmd: "HELLO".to_string(),
|
||||
},
|
||||
&mut |_| {},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(resp, HostResponse::Bool(true)));
|
||||
runner.join().unwrap();
|
||||
@@ -573,7 +575,10 @@ mod tests {
|
||||
let runner = thread::spawn(move || {
|
||||
let req = recv::<HostToPluginV4>(&mut runner_stream).unwrap();
|
||||
match req {
|
||||
HostToPluginV4::Request { id, payload: HostRequest::Dispatch { .. } } => {
|
||||
HostToPluginV4::Request {
|
||||
id,
|
||||
payload: HostRequest::Dispatch { .. },
|
||||
} => {
|
||||
// Send a nested plugin request.
|
||||
send(
|
||||
&mut runner_stream,
|
||||
@@ -587,8 +592,10 @@ mod tests {
|
||||
// Read the response to the nested request.
|
||||
let resp = recv::<HostToPluginV4>(&mut runner_stream).unwrap();
|
||||
match resp {
|
||||
HostToPluginV4::Response { id: rid, payload: PluginResponse::Ok }
|
||||
if rid == 99 => {}
|
||||
HostToPluginV4::Response {
|
||||
id: rid,
|
||||
payload: PluginResponse::Ok,
|
||||
} if rid == 99 => {}
|
||||
other => panic!("unexpected nested response: {other:?}"),
|
||||
}
|
||||
// Now respond to the original dispatch.
|
||||
@@ -607,11 +614,21 @@ mod tests {
|
||||
|
||||
let mut host = DummyHost::new();
|
||||
let resp = conn
|
||||
.call(&mut host, HostRequest::Dispatch { cmd: "NESTED".to_string() }, &mut |_| {})
|
||||
.call(
|
||||
&mut host,
|
||||
HostRequest::Dispatch {
|
||||
cmd: "NESTED".to_string(),
|
||||
},
|
||||
&mut |_| {},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(resp, HostResponse::Bool(true)));
|
||||
let infos = host.take_push_info();
|
||||
assert_eq!(infos, vec!["nested".to_string()], "push_info should be delivered to host");
|
||||
assert_eq!(
|
||||
infos,
|
||||
vec!["nested".to_string()],
|
||||
"push_info should be delivered to host"
|
||||
);
|
||||
runner.join().unwrap();
|
||||
restore_test_env();
|
||||
}
|
||||
@@ -632,29 +649,56 @@ mod tests {
|
||||
|
||||
let runner = thread::spawn(move || {
|
||||
let req = recv::<HostToPluginV4>(&mut runner_stream).unwrap();
|
||||
let HostToPluginV4::Request { id, payload: HostRequest::Dispatch { .. } } = req else {
|
||||
let HostToPluginV4::Request {
|
||||
id,
|
||||
payload: HostRequest::Dispatch { .. },
|
||||
} = req
|
||||
else {
|
||||
panic!("unexpected host request: {req:?}");
|
||||
};
|
||||
send(&mut runner_stream, &PluginToHostV4::Request {
|
||||
id: 99,
|
||||
tab_id: Some(0),
|
||||
payload: PluginRequest::UpdateEntitiesTransaction {
|
||||
label: "Move point".into(),
|
||||
entities: vec![sent],
|
||||
send(
|
||||
&mut runner_stream,
|
||||
&PluginToHostV4::Request {
|
||||
id: 99,
|
||||
tab_id: Some(0),
|
||||
payload: PluginRequest::UpdateEntitiesTransaction {
|
||||
label: "Move point".into(),
|
||||
entities: vec![sent],
|
||||
},
|
||||
},
|
||||
}).unwrap();
|
||||
assert!(matches!(recv::<HostToPluginV4>(&mut runner_stream).unwrap(),
|
||||
HostToPluginV4::Response { id: 99, payload: PluginResponse::EntityTransactionResult(Ok(())) }));
|
||||
send(&mut runner_stream, &PluginToHostV4::Response {
|
||||
id,
|
||||
payload: HostResponse::Bool(true),
|
||||
}).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
recv::<HostToPluginV4>(&mut runner_stream).unwrap(),
|
||||
HostToPluginV4::Response {
|
||||
id: 99,
|
||||
payload: PluginResponse::EntityTransactionResult(Ok(()))
|
||||
}
|
||||
));
|
||||
send(
|
||||
&mut runner_stream,
|
||||
&PluginToHostV4::Response {
|
||||
id,
|
||||
payload: HostResponse::Bool(true),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let mut host = DummyHost::new();
|
||||
assert!(matches!(conn.call(&mut host, HostRequest::Dispatch { cmd: "MOVE".into() }, &mut |_| {}).unwrap(),
|
||||
HostResponse::Bool(true)));
|
||||
assert_eq!(host.take_transactions(), vec![("Move point".into(), vec![expected])]);
|
||||
assert!(matches!(
|
||||
conn.call(
|
||||
&mut host,
|
||||
HostRequest::Dispatch { cmd: "MOVE".into() },
|
||||
&mut |_| {}
|
||||
)
|
||||
.unwrap(),
|
||||
HostResponse::Bool(true)
|
||||
));
|
||||
assert_eq!(
|
||||
host.take_transactions(),
|
||||
vec![("Move point".into(), vec![expected])]
|
||||
);
|
||||
runner.join().unwrap();
|
||||
restore_test_env();
|
||||
}
|
||||
@@ -735,7 +779,11 @@ mod tests {
|
||||
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
let got = received.lock().unwrap().clone();
|
||||
assert_eq!(got.len(), 1, "second notification should survive handler panic");
|
||||
assert_eq!(
|
||||
got.len(),
|
||||
1,
|
||||
"second notification should survive handler panic"
|
||||
);
|
||||
assert_eq!(got[0].0, Some(1));
|
||||
runner.join().unwrap();
|
||||
drop(conn);
|
||||
@@ -783,7 +831,10 @@ mod tests {
|
||||
count <= 3,
|
||||
"rate limiter should drop most notifications, got {count}"
|
||||
);
|
||||
assert!(conn.is_alive(), "connection should survive rate-limited flood");
|
||||
assert!(
|
||||
conn.is_alive(),
|
||||
"connection should survive rate-limited flood"
|
||||
);
|
||||
runner.join().unwrap();
|
||||
drop(conn);
|
||||
std::env::remove_var("OCS_PLUGIN_NOTIFY_RATE_LIMIT");
|
||||
@@ -816,7 +867,10 @@ mod tests {
|
||||
assert_eq!(command_id, 1);
|
||||
assert_eq!(source, CommandSource::Editor);
|
||||
assert_eq!(code, "1+1");
|
||||
assert_eq!(tab_index, 0, "ExecuteCode should be tied to the host's tab index");
|
||||
assert_eq!(
|
||||
tab_index, 0,
|
||||
"ExecuteCode should be tied to the host's tab index"
|
||||
);
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
send(
|
||||
&mut runner_stream,
|
||||
|
||||
@@ -110,7 +110,9 @@ fn collect_item_command_ids(item: &OwnedRibbonItem, out: &mut Vec<String>) {
|
||||
}
|
||||
}
|
||||
OwnedRibbonItem::PropertiesGroup { match_prop } => push_tool_command(match_prop, out),
|
||||
OwnedRibbonItem::StyleComboGroup { rows, manager_cmd, .. } => {
|
||||
OwnedRibbonItem::StyleComboGroup {
|
||||
rows, manager_cmd, ..
|
||||
} => {
|
||||
for t in rows.iter().flatten() {
|
||||
push_tool_command(t, out);
|
||||
}
|
||||
@@ -214,17 +216,22 @@ impl From<RibbonItem> for OwnedRibbonItem {
|
||||
.collect(),
|
||||
default: default.to_string(),
|
||||
},
|
||||
RibbonItem::LabeledDropdown { id, label, icon, items, default } => {
|
||||
OwnedRibbonItem::LabeledDropdown {
|
||||
id: id.to_string(),
|
||||
label: label.to_string(),
|
||||
icon: icon.into(),
|
||||
items: items.into_iter().map(|(a, b, i)| {
|
||||
(a.to_string(), b.to_string(), i.into())
|
||||
}).collect(),
|
||||
default: default.to_string(),
|
||||
}
|
||||
}
|
||||
RibbonItem::LabeledDropdown {
|
||||
id,
|
||||
label,
|
||||
icon,
|
||||
items,
|
||||
default,
|
||||
} => OwnedRibbonItem::LabeledDropdown {
|
||||
id: id.to_string(),
|
||||
label: label.to_string(),
|
||||
icon: icon.into(),
|
||||
items: items
|
||||
.into_iter()
|
||||
.map(|(a, b, i)| (a.to_string(), b.to_string(), i.into()))
|
||||
.collect(),
|
||||
default: default.to_string(),
|
||||
},
|
||||
RibbonItem::LargeDropdown {
|
||||
id,
|
||||
label,
|
||||
@@ -242,9 +249,10 @@ impl From<RibbonItem> for OwnedRibbonItem {
|
||||
default: default.to_string(),
|
||||
},
|
||||
RibbonItem::ToolGrid { columns } => OwnedRibbonItem::ToolGrid {
|
||||
columns: columns.into_iter().map(|column| {
|
||||
column.into_iter().map(Into::into).collect()
|
||||
}).collect(),
|
||||
columns: columns
|
||||
.into_iter()
|
||||
.map(|column| column.into_iter().map(Into::into).collect())
|
||||
.collect(),
|
||||
},
|
||||
RibbonItem::LayerComboGroup { row2, row3 } => OwnedRibbonItem::LayerComboGroup {
|
||||
row2: row2.into_iter().map(Into::into).collect(),
|
||||
@@ -297,19 +305,28 @@ impl OwnedRibbonItem {
|
||||
.collect(),
|
||||
default: &*Box::leak(default.into_boxed_str()),
|
||||
},
|
||||
OwnedRibbonItem::LabeledDropdown { id, label, icon, items, default } => {
|
||||
RibbonItem::LabeledDropdown {
|
||||
id: &*Box::leak(id.into_boxed_str()),
|
||||
label: &*Box::leak(label.into_boxed_str()),
|
||||
icon: icon.to_static(),
|
||||
items: items.into_iter().map(|(a, b, i)| (
|
||||
&*Box::leak(a.into_boxed_str()),
|
||||
&*Box::leak(b.into_boxed_str()),
|
||||
i.to_static(),
|
||||
)).collect(),
|
||||
default: &*Box::leak(default.into_boxed_str()),
|
||||
}
|
||||
}
|
||||
OwnedRibbonItem::LabeledDropdown {
|
||||
id,
|
||||
label,
|
||||
icon,
|
||||
items,
|
||||
default,
|
||||
} => RibbonItem::LabeledDropdown {
|
||||
id: &*Box::leak(id.into_boxed_str()),
|
||||
label: &*Box::leak(label.into_boxed_str()),
|
||||
icon: icon.to_static(),
|
||||
items: items
|
||||
.into_iter()
|
||||
.map(|(a, b, i)| {
|
||||
(
|
||||
&*Box::leak(a.into_boxed_str()),
|
||||
&*Box::leak(b.into_boxed_str()),
|
||||
i.to_static(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
default: &*Box::leak(default.into_boxed_str()),
|
||||
},
|
||||
OwnedRibbonItem::LargeDropdown {
|
||||
id,
|
||||
label,
|
||||
@@ -333,9 +350,10 @@ impl OwnedRibbonItem {
|
||||
default: &*Box::leak(default.into_boxed_str()),
|
||||
},
|
||||
OwnedRibbonItem::ToolGrid { columns } => RibbonItem::ToolGrid {
|
||||
columns: columns.into_iter().map(|column| {
|
||||
column.into_iter().map(OwnedToolDef::to_static).collect()
|
||||
}).collect(),
|
||||
columns: columns
|
||||
.into_iter()
|
||||
.map(|column| column.into_iter().map(OwnedToolDef::to_static).collect())
|
||||
.collect(),
|
||||
},
|
||||
OwnedRibbonItem::LayerComboGroup { row2, row3 } => RibbonItem::LayerComboGroup {
|
||||
row2: row2.into_iter().map(|t| t.to_static()).collect(),
|
||||
@@ -382,17 +400,22 @@ impl From<&RibbonItem> for OwnedRibbonItem {
|
||||
.collect(),
|
||||
default: default.to_string(),
|
||||
},
|
||||
RibbonItem::LabeledDropdown { id, label, icon, items, default } => {
|
||||
OwnedRibbonItem::LabeledDropdown {
|
||||
id: id.to_string(),
|
||||
label: label.to_string(),
|
||||
icon: icon.into(),
|
||||
items: items.iter().map(|(a, b, i)| {
|
||||
(a.to_string(), b.to_string(), i.into())
|
||||
}).collect(),
|
||||
default: default.to_string(),
|
||||
}
|
||||
}
|
||||
RibbonItem::LabeledDropdown {
|
||||
id,
|
||||
label,
|
||||
icon,
|
||||
items,
|
||||
default,
|
||||
} => OwnedRibbonItem::LabeledDropdown {
|
||||
id: id.to_string(),
|
||||
label: label.to_string(),
|
||||
icon: icon.into(),
|
||||
items: items
|
||||
.iter()
|
||||
.map(|(a, b, i)| (a.to_string(), b.to_string(), i.into()))
|
||||
.collect(),
|
||||
default: default.to_string(),
|
||||
},
|
||||
RibbonItem::LargeDropdown {
|
||||
id,
|
||||
label,
|
||||
@@ -410,9 +433,10 @@ impl From<&RibbonItem> for OwnedRibbonItem {
|
||||
default: default.to_string(),
|
||||
},
|
||||
RibbonItem::ToolGrid { columns } => OwnedRibbonItem::ToolGrid {
|
||||
columns: columns.iter().map(|column| {
|
||||
column.iter().map(Into::into).collect()
|
||||
}).collect(),
|
||||
columns: columns
|
||||
.iter()
|
||||
.map(|column| column.iter().map(Into::into).collect())
|
||||
.collect(),
|
||||
},
|
||||
RibbonItem::LayerComboGroup { row2, row3 } => OwnedRibbonItem::LayerComboGroup {
|
||||
row2: row2.iter().map(Into::into).collect(),
|
||||
@@ -622,7 +646,10 @@ mod tests {
|
||||
};
|
||||
|
||||
let ids = group.command_ids();
|
||||
assert_eq!(ids, ["SHOW_ALL", "SHOW_SELECTED", "PARALLEL", "PERPENDICULAR"]);
|
||||
assert_eq!(
|
||||
ids,
|
||||
["SHOW_ALL", "SHOW_SELECTED", "PARALLEL", "PERPENDICULAR"]
|
||||
);
|
||||
let round_trip: OwnedRibbonGroup = group.to_static().into();
|
||||
assert_eq!(round_trip.command_ids(), ids);
|
||||
}
|
||||
|
||||
@@ -76,10 +76,8 @@ fn invoke_on_load(
|
||||
interactive: &InteractiveRegistry,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut proxy = client.plugin_host_api(0, interactive.clone());
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
plugin.on_load(&mut proxy)
|
||||
}))
|
||||
.map_err(|_| std::io::Error::other("plugin on_load() panicked"))?;
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| plugin.on_load(&mut proxy)))
|
||||
.map_err(|_| std::io::Error::other("plugin on_load() panicked"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -121,7 +119,9 @@ fn run_v4(
|
||||
|
||||
match client.recv_runner_frame_timeout(std::time::Duration::from_millis(50)) {
|
||||
Ok(RunnerFrame::Request { id, payload }) => {
|
||||
if let Some(resp) = handle_host_request_v4(&mut *plugin, interactive, &client, id, payload) {
|
||||
if let Some(resp) =
|
||||
handle_host_request_v4(&mut *plugin, interactive, &client, id, payload)
|
||||
{
|
||||
client.send_response(id, resp)?;
|
||||
}
|
||||
}
|
||||
@@ -283,7 +283,9 @@ fn handle_host_request_v4(
|
||||
HostRequest::GetManifest => {
|
||||
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| plugin.manifest())) {
|
||||
Ok(m) => Some(HostResponse::Manifest(m.into())),
|
||||
Err(_) => Some(HostResponse::Error("plugin manifest() panicked".to_string())),
|
||||
Err(_) => Some(HostResponse::Error(
|
||||
"plugin manifest() panicked".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
HostRequest::GetRibbon => {
|
||||
@@ -348,7 +350,9 @@ fn handle_host_request_v4(
|
||||
}
|
||||
match step {
|
||||
Ok(s) => Some(HostResponse::CommandStep(Box::new(s))),
|
||||
Err(_) => Some(HostResponse::Error("interactive command panicked".to_string())),
|
||||
Err(_) => Some(HostResponse::Error(
|
||||
"interactive command panicked".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
HostRequest::DropInteractive { command_id } => {
|
||||
@@ -365,7 +369,9 @@ fn handle_host_request_v4(
|
||||
match result {
|
||||
Some(Ok(s)) => Some(HostResponse::Text(s)),
|
||||
Some(Err(_)) => Some(HostResponse::Error("prompt() panicked".to_string())),
|
||||
None => Some(HostResponse::Error(format!("unknown interactive command {command_id}"))),
|
||||
None => Some(HostResponse::Error(format!(
|
||||
"unknown interactive command {command_id}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
HostRequest::NeedsEntityPick { command_id } => {
|
||||
@@ -379,8 +385,12 @@ fn handle_host_request_v4(
|
||||
};
|
||||
match result {
|
||||
Some(Ok(b)) => Some(HostResponse::Bool(b)),
|
||||
Some(Err(_)) => Some(HostResponse::Error("needs_object_pick() panicked".to_string())),
|
||||
None => Some(HostResponse::Error(format!("unknown interactive command {command_id}"))),
|
||||
Some(Err(_)) => Some(HostResponse::Error(
|
||||
"needs_object_pick() panicked".to_string(),
|
||||
)),
|
||||
None => Some(HostResponse::Error(format!(
|
||||
"unknown interactive command {command_id}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
HostRequest::CursorMove { command_id, pt } => {
|
||||
@@ -407,13 +417,7 @@ fn handle_host_request_v4(
|
||||
let mut proxy = client.plugin_host_api(tab_index, interactive.clone());
|
||||
let respond = client.execute_code_responder(id);
|
||||
let started = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
plugin.start_execute_code(
|
||||
&mut proxy,
|
||||
command_id,
|
||||
&code,
|
||||
source,
|
||||
respond,
|
||||
)
|
||||
plugin.start_execute_code(&mut proxy, command_id, &code, source, respond)
|
||||
}));
|
||||
match started {
|
||||
Ok(true) => None,
|
||||
@@ -435,7 +439,9 @@ fn handle_host_request_v4(
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn load_plugin(path: &Path) -> Result<(u32, Box<dyn BuiltinPlugin>), Box<dyn std::error::Error>> {
|
||||
unsafe fn load_plugin(
|
||||
path: &Path,
|
||||
) -> Result<(u32, Box<dyn BuiltinPlugin>), Box<dyn std::error::Error>> {
|
||||
let lib = libloading::Library::new(path)?;
|
||||
|
||||
let version: libloading::Symbol<extern "C" fn() -> u32> = lib
|
||||
|
||||
@@ -147,14 +147,19 @@ mod tests {
|
||||
value["ocs_plugin_api_version"].as_str(),
|
||||
Some(env!("CARGO_PKG_VERSION"))
|
||||
);
|
||||
assert_eq!(value["api_version"].as_i64(), Some(crate::API_VERSION as i64));
|
||||
assert_eq!(
|
||||
value["api_version"].as_i64(),
|
||||
Some(crate::API_VERSION as i64)
|
||||
);
|
||||
assert_eq!(
|
||||
value["api_version_min_supported"].as_i64(),
|
||||
Some(crate::API_VERSION_MIN_SUPPORTED as i64)
|
||||
);
|
||||
|
||||
// acadrust_version should have a patch component (e.g. "0.4.0").
|
||||
let opencadcodec = value["acadrust_version"].as_str().expect("acadrust_version string");
|
||||
let opencadcodec = value["acadrust_version"]
|
||||
.as_str()
|
||||
.expect("acadrust_version string");
|
||||
assert_eq!(opencadcodec.split('.').count(), 3);
|
||||
}
|
||||
|
||||
@@ -223,10 +228,10 @@ mod tests {
|
||||
assert!(acadrust_source_hash("").is_none());
|
||||
assert!(acadrust_source_hash("registry+https://crates.io").is_none());
|
||||
assert!(acadrust_source_hash("git+https://github.com/foo/bar.git#short").is_none());
|
||||
assert!(
|
||||
acadrust_source_hash("git+https://github.com/foo/bar.git#gggggggggggggggggggggggggggggggggggggggg")
|
||||
.is_none()
|
||||
);
|
||||
assert!(acadrust_source_hash(
|
||||
"git+https://github.com/foo/bar.git#gggggggggggggggggggggggggggggggggggggggg"
|
||||
)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user