1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
use serde::{Deserialize, Serialize};
/// Source of a command (builtin or from a plugin)
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
pub enum CommandSource {
/// Built-in editor command
Builtin,
/// Command registered by a plugin (contains plugin filename without extension)
Plugin(String),
}
/// A command registered by a plugin via the service bridge.
/// This is a simplified version that the editor converts to its internal Command type.
#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
pub struct Command {
/// Command name (e.g., "Open File")
pub name: String,
/// Command description
pub description: String,
/// The action name to trigger (for plugin commands, this is the function name)
pub action_name: String,
/// Plugin that registered this command
pub plugin_name: String,
/// Custom contexts required for this command (plugin-defined contexts like "vi-mode")
pub custom_contexts: Vec<String>,
/// When `true`, a key bound to this command bypasses terminal
/// keyboard capture — the action fires instead of the keystroke
/// being forwarded to the PTY child. Use for commands the user
/// must always be able to reach (e.g. the orchestrator picker,
/// "switch to other session", a global panic-exit) so a focused
/// terminal pane doesn't trap them. Default `false` — keys
/// bound to non-bypassing commands still go to the PTY when
/// keyboard capture is on, matching the existing UX.
#[serde(default)]
pub terminal_bypass: bool,
}
/// A single suggestion item for autocomplete
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
#[serde(deny_unknown_fields)]
#[ts(export, rename = "PromptSuggestion")]
pub struct Suggestion {
/// The text to display
pub text: String,
/// Optional description
#[serde(default)]
#[ts(optional)]
pub description: Option<String>,
/// The value to use when selected (defaults to text if None)
#[serde(default)]
#[ts(optional)]
pub value: Option<String>,
/// Whether this suggestion is disabled (greyed out, defaults to false)
#[serde(default)]
#[ts(optional)]
pub disabled: Option<bool>,
/// Optional keyboard shortcut
#[serde(default)]
#[ts(optional)]
pub keybinding: Option<String>,
/// Source of the command (for command palette) - internal, not settable by plugins
#[serde(skip)]
#[ts(skip)]
pub source: Option<CommandSource>,
}
#[cfg(feature = "plugins")]
impl<'js> rquickjs::FromJs<'js> for Suggestion {
fn from_js(_ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result<Self> {
rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
from: "object",
to: "Suggestion",
message: Some(e.to_string()),
})
}
}
impl Suggestion {
pub fn new(text: String) -> Self {
Self {
text,
description: None,
value: None,
disabled: None,
keybinding: None,
source: None,
}
}
/// Check if this suggestion is disabled
pub fn is_disabled(&self) -> bool {
self.disabled.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
/// `is_disabled` mirrors the `disabled` option and treats `None` as enabled.
#[test]
fn is_disabled_reflects_disabled_field() {
let mut s = Suggestion::new("foo".into());
assert!(!s.is_disabled(), "None defaults to enabled");
s.disabled = Some(false);
assert!(!s.is_disabled());
s.disabled = Some(true);
assert!(s.is_disabled());
}
/// `Suggestion::from_js` round-trips user-visible fields through
/// `rquickjs_serde`. A mutant that returns `Ok(Default::default())`
/// would drop `text` (becoming empty) and `description`.
#[cfg(feature = "plugins")]
#[test]
fn suggestion_from_js_decodes_distinguishing_fields() {
use rquickjs::{Context, FromJs, Runtime, Value};
let rt = Runtime::new().unwrap();
let ctx = Context::full(&rt).unwrap();
ctx.with(|ctx| {
let v: Value = ctx
.eval::<Value, _>(b"({text: 'hello', description: 'world'})".as_slice())
.unwrap();
let got = Suggestion::from_js(&ctx, v).unwrap();
assert_eq!(got.text, "hello");
assert_eq!(got.description.as_deref(), Some("world"));
});
}
}