дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(plugins): notifications targeted at one project (API 1.5) (#2375)
- Add ui.notify-project: it shows only in that project's window (the most recently focused one if there are several) and works without a calling window - Button commands run in the window showing the notification; a closed project returns not-found, with nothing shown and no fallback to another window - Bump the plugin API to 1.5.0 (wit/, SDK copy, ENGINE_VERSION, API_VERSION) and rebuild all components - Add Project::notify and its mock to the SDK; update plugins/README.md Co-authored-by: LU XIANGRUI <[email protected]>
Цей коміт міститься в:
1 джерело
5f970d97d5
коміт
ff17c3753f
149 files changed
+449
-165
No files matched your search
@@ -32,7 +32,7 @@ use std::path::{Component, Path, PathBuf};
|
||||
|
||||
pub const MANIFEST_FILE: &str = "plugin.toml";
|
||||
/// The plugin API (engine) version this host implements.
|
||||
pub const ENGINE_VERSION: &str = "1.4.0";
|
||||
pub const ENGINE_VERSION: &str = "1.5.0";
|
||||
/// The 0.1 API, still accepted for contribution-only packages and 0.1 components.
|
||||
pub const LEGACY_API: &str = "0.1";
|
||||
/// Marketplace categories, in the order they are shown.
|
||||
|
||||
@@ -28,6 +28,8 @@ pub struct Reports(RwLock<State>);
|
||||
struct State {
|
||||
windows: HashMap<String, Window>,
|
||||
focused: Option<String>,
|
||||
/// Windows in the order they were last focused, the most recent last.
|
||||
focus_order: Vec<String>,
|
||||
remote: HashMap<String, PathBuf>,
|
||||
libraries: HashMap<String, Vec<PathBuf>>,
|
||||
}
|
||||
@@ -46,8 +48,16 @@ impl Reports {
|
||||
state.windows.entry(window.into()).or_default().focused = editor;
|
||||
if focused {
|
||||
state.focused = Some(window.into());
|
||||
state.focus_order.retain(|w| w != window);
|
||||
state.focus_order.push(window.into());
|
||||
}
|
||||
}
|
||||
/// Sorts `windows` by when they were last focused, the most recent first; windows
|
||||
/// never focused keep their order after them.
|
||||
pub(crate) fn most_recently_focused_first(&self, windows: &mut [String]) {
|
||||
let order = &self.0.read().focus_order;
|
||||
windows.sort_by_key(|w| std::cmp::Reverse(order.iter().position(|o| o == w)));
|
||||
}
|
||||
pub fn report_problems(&self, window: &str, reset: bool, files: Vec<FileProblems>) {
|
||||
let mut state = self.0.write();
|
||||
let problems = &mut state.windows.entry(window.into()).or_default().problems;
|
||||
@@ -68,6 +78,7 @@ impl Reports {
|
||||
state.windows.remove(window);
|
||||
state.remote.remove(window);
|
||||
state.libraries.remove(window);
|
||||
state.focus_order.retain(|w| w != window);
|
||||
if state.focused.as_deref() == Some(window) {
|
||||
state.focused = None;
|
||||
}
|
||||
@@ -122,3 +133,24 @@ impl Reports {
|
||||
files.into_iter().map(|(path, items)| FileProblems { path: path.into(), items }).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn windows_sort_by_their_last_focus() {
|
||||
let reports = Reports::default();
|
||||
for window in ["a", "b", "a", "c"] {
|
||||
reports.set_focused_editor(window, None, true);
|
||||
}
|
||||
reports.set_focused_editor("b", None, false);
|
||||
let mut windows: Vec<String> = ["a", "b", "c", "never"].map(String::from).into();
|
||||
reports.most_recently_focused_first(&mut windows);
|
||||
assert_eq!(windows, ["c", "a", "b", "never"]);
|
||||
reports.forget_window("c");
|
||||
let mut windows: Vec<String> = ["never", "c", "b", "a"].map(String::from).into();
|
||||
reports.most_recently_focused_first(&mut windows);
|
||||
assert_eq!(windows, ["a", "b", "never", "c"]);
|
||||
}
|
||||
}
|
||||
@@ -110,6 +110,10 @@ pub trait PluginWorkspace: Send + Sync {
|
||||
fn trusted(&self, root: &Path) -> bool;
|
||||
/// Root of the project open in `window`.
|
||||
fn project_of(&self, window: &str) -> Option<PathBuf>;
|
||||
/// Windows showing the open project at `root`, the most recently focused first.
|
||||
fn project_windows(&self, _root: &Path) -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
/// Shows `items` as producer `source`'s diagnostics of `path` (replacing earlier ones).
|
||||
fn publish_diagnostics(&self, source: &str, path: &Path, items: Vec<ide_core::Diagnostic>)
|
||||
-> Result<(), HostError>;
|
||||
@@ -1758,17 +1762,31 @@ impl HostApi for PluginApi {
|
||||
body: String,
|
||||
actions: Vec<ide_plugin::NotificationAction>,
|
||||
) -> Result<(), HostError> {
|
||||
if !actions.is_empty() {
|
||||
let commands = self.shared.ui.items("commands", Some(self.id()));
|
||||
if let Some(a) = actions.iter().find(|a| !commands.iter().any(|c| c.key == a.command)) {
|
||||
return Err(HostError::NotFound(format!("the plugin declares no command {:?}", a.command)));
|
||||
}
|
||||
}
|
||||
check_actions(self, &actions)?;
|
||||
let plugin_id = self.id().to_owned();
|
||||
self.shared.sink.emit(client, PluginEvent::Notification { plugin_id, level, title, body, actions });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn notify_project(
|
||||
&self,
|
||||
root: &Path,
|
||||
level: NotificationLevel,
|
||||
title: String,
|
||||
body: String,
|
||||
actions: Vec<ide_plugin::NotificationAction>,
|
||||
) -> Result<(), HostError> {
|
||||
check_actions(self, &actions)?;
|
||||
let windows = self.shared.workspace().map(|w| w.project_windows(root)).unwrap_or_default();
|
||||
let window = windows
|
||||
.into_iter()
|
||||
.find(|w| !self.shared.client_closed(w))
|
||||
.ok_or_else(|| HostError::NotFound(format!("the project {} is not open", root.display())))?;
|
||||
let plugin_id = self.id().to_owned();
|
||||
self.shared.sink.emit(Some(&window), PluginEvent::Notification { plugin_id, level, title, body, actions });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_to_clipboard(&self, client: Option<&str>, text: String) -> Result<(), HostError> {
|
||||
let client = from_window(client, "copy to the clipboard")?;
|
||||
self.shared.sink.emit(Some(client), PluginEvent::CopyToClipboard { plugin_id: self.id().to_owned(), text });
|
||||
@@ -2718,6 +2736,15 @@ fn not_writable(path: &Path, e: ide_core::error::IdeError) -> HostError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Notification buttons must name commands the plugin declares.
|
||||
fn check_actions(api: &PluginApi, actions: &[ide_plugin::NotificationAction]) -> Result<(), HostError> {
|
||||
let commands = if actions.is_empty() { Vec::new() } else { api.shared.ui.items("commands", Some(api.id())) };
|
||||
match actions.iter().find(|a| !commands.iter().any(|c| c.key == a.command)) {
|
||||
Some(a) => Err(HostError::NotFound(format!("the plugin declares no command {:?}", a.command))),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`PluginWorkspace`] over the desktop host's open projects.
|
||||
pub struct HostWorkspace(pub std::sync::Weak<crate::Host>);
|
||||
|
||||
@@ -3016,6 +3043,13 @@ impl PluginWorkspace for HostWorkspace {
|
||||
self.0.upgrade()?.project(window).ok().map(|p| p.root.clone())
|
||||
}
|
||||
|
||||
fn project_windows(&self, root: &Path) -> Vec<String> {
|
||||
let Some(host) = self.0.upgrade() else { return Vec::new() };
|
||||
let mut windows = host.windows_for_root(root);
|
||||
host.mcp_reports.most_recently_focused_first(&mut windows);
|
||||
windows
|
||||
}
|
||||
|
||||
fn publish_diagnostics(
|
||||
&self,
|
||||
source: &str,
|
||||
|
||||
@@ -126,6 +126,10 @@ impl PluginWorkspace for TestWorkspace {
|
||||
fn trusted(&self, _root: &Path) -> bool {
|
||||
self.trusted
|
||||
}
|
||||
fn project_windows(&self, root: &Path) -> Vec<String> {
|
||||
// `main` shows the fixture project; other open roots have no window left.
|
||||
if root == self.root { vec!["closing".into(), "main".into()] } else { Vec::new() }
|
||||
}
|
||||
fn project_of(&self, _window: &str) -> Option<PathBuf> {
|
||||
if let Some(left) = self.project_queries_left.lock().as_mut() {
|
||||
if *left == 0 {
|
||||
@@ -4801,6 +4805,40 @@ fn notification_actions_keep_commands_and_window_scope() {
|
||||
assert_eq!(f.status("jobs"), PluginStatus::Active, "a refused notification leaves the plugin running");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_notifications_go_to_a_window_of_that_project_only() {
|
||||
let f = fixture(Limits::default());
|
||||
f.install(&sample("jobs"));
|
||||
f.host.set_capability("jobs", "project.read", false).unwrap();
|
||||
f.host.forget_client("closing");
|
||||
let other = f.project.parent().unwrap().join("other");
|
||||
std::fs::create_dir(&other).unwrap();
|
||||
f.workspace.other_roots.lock().push(other.clone());
|
||||
f.take_events();
|
||||
let notifications = |f: &Fixture| {
|
||||
f.take_events()
|
||||
.into_iter()
|
||||
.filter_map(|(client, event)| match event {
|
||||
PluginEvent::Notification { title, actions, .. } => Some((client, title, actions)),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
// A scheduled call has no invoking window; a closing window is skipped.
|
||||
let path = f.project.join("notes.txt").to_string_lossy().into_owned();
|
||||
f.host.run_command("jobs", "notify-project", Some(&path), Invocation::default()).unwrap();
|
||||
let sent = notifications(&f);
|
||||
assert_eq!(sent.len(), 1);
|
||||
assert_eq!((sent[0].0.as_deref(), sent[0].1.as_str()), (Some("main"), "Project update"));
|
||||
assert_eq!((sent[0].2[0].command.as_str(), sent[0].2[0].argument.as_deref()), ("context", Some(path.as_str())));
|
||||
// A project without a window fails without falling back to another one, even when invoked from one.
|
||||
let path = other.join("notes.txt").to_string_lossy().into_owned();
|
||||
let error = f.host.run_command("jobs", "notify-project", Some(&path), f.inv()).unwrap_err().message;
|
||||
assert!(error.contains("not open"), "{error}");
|
||||
assert!(notifications(&f).is_empty());
|
||||
assert_eq!(f.status("jobs"), PluginStatus::Active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn directory_prompts_and_project_requests_use_the_calling_window_without_a_capability() {
|
||||
use ide_plugin::Prompt;
|
||||
|
||||
@@ -54,7 +54,7 @@ use views_bindings::exports::openidea::plugin::view_provider as wit_view;
|
||||
use views_bindings::openidea::plugin::view_types as wit_vt;
|
||||
|
||||
/// The API version of the world above, `major.minor` of [`host_api`].
|
||||
pub const API_VERSION: &str = "1.4";
|
||||
pub const API_VERSION: &str = "1.5";
|
||||
const EPOCH_TICK: Duration = Duration::from_millis(10);
|
||||
pub(crate) const MAX_TEXT: usize = 1 << 20;
|
||||
pub(crate) const MAX_TITLE: usize = 256;
|
||||
@@ -259,6 +259,19 @@ pub trait HostApi: Send + Sync {
|
||||
body: String,
|
||||
actions: Vec<crate::NotificationAction>,
|
||||
) -> Result<(), HostError>;
|
||||
/// [`HostApi::notify`] in the most recently focused window of the open project at
|
||||
/// `root`; not-found (and nothing shown) once it has closed.
|
||||
fn notify_project(
|
||||
&self,
|
||||
root: &Path,
|
||||
level: NotificationLevel,
|
||||
title: String,
|
||||
body: String,
|
||||
actions: Vec<crate::NotificationAction>,
|
||||
) -> Result<(), HostError> {
|
||||
let _ = (root, level, title, body, actions);
|
||||
Err(HostError::NotFound("project notifications unavailable".into()))
|
||||
}
|
||||
fn copy_to_clipboard(&self, client: Option<&str>, text: String) -> Result<(), HostError> {
|
||||
let _ = (client, text);
|
||||
Ok(())
|
||||
|
||||
@@ -362,6 +362,25 @@ impl StoreData {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks `ui.notify` buttons; the host checks that their commands are declared.
|
||||
fn notification_actions(actions: Vec<ui::NotificationAction>) -> Result<Vec<crate::NotificationAction>, wit::Error> {
|
||||
const MAX_ACTIONS: usize = 3;
|
||||
if actions.len() > MAX_ACTIONS {
|
||||
return Err(wit::Error::Invalid(format!("a notification has at most {MAX_ACTIONS} actions")));
|
||||
}
|
||||
let mut checked = Vec::with_capacity(actions.len());
|
||||
for a in actions {
|
||||
if a.label.trim().is_empty() || a.label.len() > 256 || a.label.chars().any(char::is_control) {
|
||||
return Err(wit::Error::Invalid(format!("invalid action label {:?}", a.label)));
|
||||
}
|
||||
if a.argument.as_ref().is_some_and(|arg| arg.len() > 64 << 10) {
|
||||
return Err(wit::Error::Invalid("an action argument is limited to 64 KiB".into()));
|
||||
}
|
||||
checked.push(crate::NotificationAction { label: a.label, command: a.command, argument: a.argument });
|
||||
}
|
||||
Ok(checked)
|
||||
}
|
||||
|
||||
impl ui::Host for StoreData {
|
||||
fn notify(
|
||||
&mut self,
|
||||
@@ -370,22 +389,29 @@ impl ui::Host for StoreData {
|
||||
body: String,
|
||||
actions: Vec<ui::NotificationAction>,
|
||||
) -> Fallible<()> {
|
||||
const MAX_ACTIONS: usize = 3;
|
||||
if actions.len() > MAX_ACTIONS {
|
||||
return Ok(Err(wit::Error::Invalid(format!("a notification has at most {MAX_ACTIONS} actions"))));
|
||||
}
|
||||
let mut checked = Vec::with_capacity(actions.len());
|
||||
for a in actions {
|
||||
if a.label.trim().is_empty() || a.label.len() > 256 || a.label.chars().any(char::is_control) {
|
||||
return Ok(Err(wit::Error::Invalid(format!("invalid action label {:?}", a.label))));
|
||||
}
|
||||
if a.argument.as_ref().is_some_and(|arg| arg.len() > 64 << 10) {
|
||||
return Ok(Err(wit::Error::Invalid("an action argument is limited to 64 KiB".into())));
|
||||
}
|
||||
checked.push(crate::NotificationAction { label: a.label, command: a.command, argument: a.argument });
|
||||
}
|
||||
let actions = match notification_actions(actions) {
|
||||
Ok(actions) => actions,
|
||||
Err(e) => return Ok(Err(e)),
|
||||
};
|
||||
let (title, body) = (clip(title, MAX_TITLE), clip(body, 16 << 10));
|
||||
Ok(self.host.notify(self.client(), notification_level(level), title, body, checked).map_err(to_wit))
|
||||
Ok(self.host.notify(self.client(), notification_level(level), title, body, actions).map_err(to_wit))
|
||||
}
|
||||
|
||||
fn notify_project(
|
||||
&mut self,
|
||||
target: Resource<ProjectRes>,
|
||||
level: wit::NotificationLevel,
|
||||
title: String,
|
||||
body: String,
|
||||
actions: Vec<ui::NotificationAction>,
|
||||
) -> Fallible<()> {
|
||||
let actions = match notification_actions(actions) {
|
||||
Ok(actions) => actions,
|
||||
Err(e) => return Ok(Err(e)),
|
||||
};
|
||||
let root = self.table.get(&target)?.root.clone();
|
||||
let (title, body) = (clip(title, MAX_TITLE), clip(body, 16 << 10));
|
||||
Ok(self.host.notify_project(&root, notification_level(level), title, body, actions).map_err(to_wit))
|
||||
}
|
||||
|
||||
fn show_notification(
|
||||
|
||||
@@ -91,15 +91,15 @@ fn the_sdk_carries_the_hosts_wit() {
|
||||
|
||||
#[test]
|
||||
fn the_version_check_runs_compatible_components_and_explains_the_rest() {
|
||||
let host = v("1.4.0");
|
||||
for ok in ["1.0.0", "1.3.0", "1.3.7", "1.4.0", "1.0.0-rc.1"] {
|
||||
let host = v("1.5.0");
|
||||
for ok in ["1.0.0", "1.3.0", "1.4.7", "1.5.0", "1.0.0-rc.1"] {
|
||||
assert_eq!(check_api(&v(ok), &host), Ok(()), "{ok}");
|
||||
}
|
||||
let newer = check_api(&v("1.5.0"), &host).unwrap_err();
|
||||
assert!(newer.contains("1.5") && newer.contains("update OpenIDEA"), "{newer}");
|
||||
let newer = check_api(&v("1.6.0"), &host).unwrap_err();
|
||||
assert!(newer.contains("1.6") && newer.contains("update OpenIDEA"), "{newer}");
|
||||
assert!(check_api(&v("2.0.0"), &host).unwrap_err().contains("update OpenIDEA"));
|
||||
let older = check_api(&v("0.2.0"), &host).unwrap_err();
|
||||
assert!(older.contains("rebuild it with the 1.4 SDK"), "{older}");
|
||||
assert!(older.contains("rebuild it with the 1.5 SDK"), "{older}");
|
||||
// 0.x: every minor is its own API.
|
||||
assert_eq!(check_api(&v("0.2.0"), &v("0.2.5")), Ok(()));
|
||||
assert!(check_api(&v("0.1.0"), &v("0.2.0")).is_err());
|
||||
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
4647fa9aec9732e3dd824efccd61b1c6b9c36645a3586f402f6a7a47419db61f
|
||||
9963d6606bb6b859bebe1f55892488944f6d5e3308e6c0109f38f4b1a67b01f5
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
6787b195df1057176ea9fcf11d0fcaf8cd347fc182fab94fe3a229063deed831
|
||||
7ab9937fdb2e78d0d62d9b85b78a0a6d9511224f85908c3a5a5e9c0d4b5e7e14
|
||||
Бінарний файл не відображається.
+1
-1
@@ -1 +1 @@
|
||||
c2d17076337b1225ef8b0323b335a3b03bf022485088957c520e0cb797d7df7d
|
||||
cf1f91d8accf8339bf6e22bef5067bfe3cba75d38cd30d8748b0aa451ee2495b
|
||||
Бінарний файл не відображається.
+1
-1
@@ -1 +1 @@
|
||||
415a4cd17c573aeeb1b45d4b1af55834eab5e686c4f624495323e4fb63c3ac71
|
||||
12ad7d649d752f2a40b28dee718a3e04b64597496ab43787c084e3cb774ccfe6
|
||||
Бінарний файл не відображається.
+1
-1
@@ -1 +1 @@
|
||||
5f502b7650acd5d07bffba8016680248b5ceac0926013916ff351562e3c6d831
|
||||
4bfce353b3c94490c5f95a046cd2371042b243c326bd63c65619e1489d7ab889
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
19e8d460269999aeac7b4021bfedb803ad0d68c24ce1c22f058070fb406a64c3
|
||||
cfbd6aac217a7baea907affe4e1e7f89d1330843398e913e68b71f3a99cfb71b
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
63809abb0f8a4f40b0c0f465062cc8ba0ebfd03d41a108ff19866e6565eda931
|
||||
dcf00138820979f523bd93e1f6b08c627c6f061ae5b6e91b340646f905593cf3
|
||||
Бінарний файл не відображається.
+1
-1
@@ -1 +1 @@
|
||||
99c968b98d8f1d895c428b4048451275bd322e7070aa0a020737097efea17eb1
|
||||
98e0e01e8405ae7501e89c0dab06e6672b5240a831b19e8fbf0604a3171b31e9
|
||||
Бінарний файл не відображається.
+1
-1
@@ -1 +1 @@
|
||||
c40a0fd8c6cf791e70ba0335c6cf0b2c82b250670ebad59b185e3e8719172409
|
||||
fcee11f27ff9ae0038e8067712e269ccd49423372f692e9351c24f82906aafad
|
||||
Бінарний файл не відображається.
+1
-1
@@ -1 +1 @@
|
||||
162774155d8b49d53b0c0c055736bd098723c66b02c0e1dc4389985982c57282
|
||||
5c777d7b146277fa803587d1788a7359b08478def00c4574dd565d784200e39a
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
39e3de11f69c09348906878367318157fca9a777f9ac1c4fb6ed303fa058e5a2
|
||||
825b9ee483796efd24fcfe358b94f4667d87e311475769ec9e41ba9af6d03a6a
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
4a3f595d2a6a9b79da7e316a3da86c287e9761ea670319c331467e7b10639663
|
||||
45b8cbd2ed7635ce04651937574dde9cf441dd619d76b8996f522c671950eaee
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
bede1f0eaec356e3e35302e65e85531870625560bc9a3197f1acb58969beb4bd
|
||||
e456b7ddf087ffbf2df62431bf9df47fad1929079c99a941a5deaf30599109f9
|
||||
Бінарний файл не відображається.
+1
-1
@@ -1 +1 @@
|
||||
ad2dc0ad12adb9c6b25017b2cef78b7496e96d1bb6e88b24ea9b7b682120e786
|
||||
e0d8b590ad6ffd9569c6362802b2be909cb34b37b18cc8cc8492f9c460610e20
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
2c329cf7af36c9d65ed5b6be7c2a017c7a5a6214a4893cab7ae574acb76fe62f
|
||||
d2512203db3e5c6df93e9ccd5ff8daed8f1ebd28462b8caf7d500e814d3b6671
|
||||
@@ -75,6 +75,14 @@ impl Plugin for Jobs {
|
||||
)?;
|
||||
Ok(None)
|
||||
}
|
||||
"notify-project" => {
|
||||
// The argument is a path in the target project, as a scheduled check would have.
|
||||
let path = arg.unwrap_or_default();
|
||||
let project = host::project_of(path).ok_or("no project contains the path")?;
|
||||
let action = openidea_plugin::NotificationAction::new("Disable", "context").argument(path);
|
||||
project.notify(NotificationLevel::Info, "Project update", path, &[action])?;
|
||||
Ok(None)
|
||||
}
|
||||
"copy" => {
|
||||
host::copy_to_clipboard(arg.unwrap_or_default())?;
|
||||
Ok(None)
|
||||
|
||||
Бінарний файл не відображається.
+1
-1
@@ -1 +1 @@
|
||||
39c85942177e937e48f32bcde86a0068dac484ba028a8d6785a5a2728db9e11b
|
||||
b32da5137b552cfad2557716343ed8ac9236b34fec018e3f2facc7b78b67e72a
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
30a8cc05a40c7b99a78700430ae87d025ad7567b44384fdb696119aa421f8174
|
||||
ae58467cdb6e98a491d739884eee96d239f80451de749950d81f4fd480edba58
|
||||
@@ -5,9 +5,9 @@ wit_bindgen::generate!({
|
||||
inline: "
|
||||
package fixture:misbehave;
|
||||
world misbehave {
|
||||
include openidea:plugin/plugin-syntax@1.4.0;
|
||||
export openidea:plugin/providers@1.4.0;
|
||||
export openidea:plugin/file-actions@1.4.0;
|
||||
include openidea:plugin/plugin-syntax@1.5.0;
|
||||
export openidea:plugin/providers@1.5.0;
|
||||
export openidea:plugin/file-actions@1.5.0;
|
||||
}
|
||||
",
|
||||
path: "../../../../../wit",
|
||||
|
||||
Бінарний файл не відображається.
+1
-1
@@ -1 +1 @@
|
||||
5f9c92b23c00269ec2b5dc8829ce526a8fc4c0d76ecb2925ec9a06ffeac5f0b4
|
||||
e378c85a4fe395c7b172065a33a2f15ad76852ac42d4099d2cf1f64540e13143
|
||||
Бінарний файл не відображається.
+1
-1
@@ -1 +1 @@
|
||||
d695409029a66c8a44f524072be1d47ed974fc8810f6fe1d8d068765184d4028
|
||||
23dc9ab1affb5b0c0166a5bb2bffb8a273a5c9eed0c06d402f16a1440b835639
|
||||
Бінарний файл не відображається.
+1
-1
@@ -1 +1 @@
|
||||
d5e81a43dc2c5298d0aaafd70c0bd91ab6cc040d70224d2936349de41253735f
|
||||
d82fa182c8d5a029451addbf3b9792419446849911949e61052fca181d7758ce
|
||||
Бінарний файл не відображається.
+1
-1
@@ -1 +1 @@
|
||||
c29076023ccedf2da8fd9e5a851a47bbe7131b97713c8140a20cd55608a1cc0d
|
||||
12337d77078aeb678567c0dc87c746a78454656ed52a2e51dd0bb81f19d90da3
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
616157bd77dc272f9e2dfed4c54c9d65fca110e55ad01f92b7c4f8a6136537b4
|
||||
9048b66162a39a518d117cd473f6192827bc05737ff5132324c54eb8295b060b
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
022f216f0f4ca5da56f4b2fabf5ee4a987d5e9f9a752feecbb438f9b00b1acf4
|
||||
274f7e74f19aabf9368c2b9a107494d82a2013d4adda645bde6d9ccda4007769
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
626f0e0c83e69704c0365c273b6f63d43278ee37786f38ed467ce908c30eadf7
|
||||
e9082dc0f4c9c4fc682e16821d36f7fa42be9c82b75ac28edbee6a869dc86b8b
|
||||
Бінарний файл не відображається.
+1
-1
@@ -1 +1 @@
|
||||
aa141701049f5c6c0d045341c3337344ed7a368800f08237b1b7812cce216814
|
||||
0028a0439e8bb234fb0cc5138e1c27d5c2c8443bed12963a69f0bd8b3818ec7b
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
d6c40099cc959ef2dead996beeb4acd73c1327597cd029674026c6f2bc878300
|
||||
f3b633e9e34a5343f318e4e451ffe1c87a6d1ac6d0dc01a57588ea99380dd8d4
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
3201f643cf868f7a358ba812f9c90e06b982d864ed141b6b4c3fdeee13cb1559
|
||||
02fba523571a945b16055e83ee72400f69ecb64238556e933cfd5c2c92c8ea10
|
||||
@@ -37,7 +37,7 @@ sdk/rust/openidea-plugin/ # the Rust SDK (host API, Plugin trait, mock host)
|
||||
crates/ide-plugin/tests/fixtures/ # test plugins: hello, jobs, language-toy, misbehave, semantic-tokens, hover, commit, inlay-hints
|
||||
```
|
||||
|
||||
The plugin API is defined in [`wit/`](../wit/) (`openidea:plugin@1.4.0`, world
|
||||
The plugin API is defined in [`wit/`](../wit/) (`openidea:plugin@1.5.0`, world
|
||||
`plugin`).
|
||||
|
||||
## 1. Quick start
|
||||
@@ -365,9 +365,19 @@ are always available. Calling a gated function without its capability returns
|
||||
|
||||
## 3. Writing code (Rust SDK)
|
||||
|
||||
### API 1.5
|
||||
|
||||
API 1.5.0 is planned for the next OpenIDEA release. Use
|
||||
`engines.openidea = "^1.5"` and the matching SDK. Released 1.4 plugins keep
|
||||
working. New in 1.5:
|
||||
|
||||
- [Notifications for a project](#notifications-with-commands):
|
||||
`Project::notify` shows a notification in a window of that project only,
|
||||
also from scheduled or background work.
|
||||
|
||||
### API 1.4
|
||||
|
||||
API 1.4.0 is planned for the next OpenIDEA release. Use
|
||||
API 1.4.0 shipped with OpenIDEA 0.1.40. Use
|
||||
`engines.openidea = "^1.4"` and the matching SDK. Released 1.3 plugins keep
|
||||
working. New in 1.4:
|
||||
|
||||
@@ -525,6 +535,7 @@ then run `openidea-plugin check`.
|
||||
| `host::output_append(channel, text)`, `output_clear`, `output_reveal` | `output.{append,clear,reveal}` | API 1.4: read-only Output tabs; 32 named channels per plugin, each a 1 MiB UTF-8 ring buffer. Reveal requires the invoking window. |
|
||||
| `host::set_context(key, value)`, `remove_context` | `ui.{set,remove}-context` | Sets `plugin.<id>.<key>` for `when` clauses in the window the call came from (`true`, `3.5`, `"text"`; `ContextValue`). Key 1–64 of `A-Za-z0-9_-`, 100 per plugin; gone when the plugin stops. |
|
||||
| `host::notify(level, title, body)` | `ui.show-notification` | Balloon in the window the command or events came from (every window otherwise). |
|
||||
| `Project::notify(level, title, body, actions)` | `ui.notify-project` | API 1.5: balloon only in a window of that project (the most recently focused one), with or without an invoking window; see _Notifications with commands_. |
|
||||
| `host::copy_to_clipboard(text)` | `ui.copy-to-clipboard` | Copies `text` to the system clipboard. Needs `clipboard`. |
|
||||
| `host::open_url(url)` | `ui.open-url` | Opens an `http://` or `https://` URL in the user's default browser. |
|
||||
| `host::pick(title, items)`, `pick_many` | `ui.pick`, `ui.pick-many` | Picks key(s) from `PickItem`s, or `None` when dismissed. From a command/event or code-action resolution in its editor window (else `invalid`); waiting is excluded from the budget. |
|
||||
@@ -2254,8 +2265,8 @@ the API itself follows _Evolving the plugin API_ below.
|
||||
## 10. Evolving the plugin API
|
||||
|
||||
Released versions are frozen: 1.0.0 shipped with OpenIDEA 0.1.36, 1.1.0
|
||||
with 0.1.37, 1.2.0 with 0.1.38 and 1.3.0 with 0.1.39. API 1.4.0 is planned
|
||||
with the next release. wasmtime decides what stays compatible: it resolves interface names
|
||||
with 0.1.37, 1.2.0 with 0.1.38, 1.3.0 with 0.1.39 and 1.4.0 with 0.1.40.
|
||||
API 1.5.0 is planned with the next release. wasmtime decides what stays compatible: it resolves interface names
|
||||
by semver (a component importing `openidea:plugin/[email protected]` links against a
|
||||
host defining `@1.1.0`, and the host finds its exports the same way), but
|
||||
function types must match exactly — a record with another field count, or a
|
||||
@@ -2382,6 +2393,29 @@ its normal editor and project context. No capability is needed. Invalid or
|
||||
undeclared actions reject the whole notification. Labels are limited to 256
|
||||
bytes and arguments to 64 KiB. Plain `host::notify` behaves as before.
|
||||
|
||||
Both go to the window the command or event came from, or to every window when
|
||||
there is none (a scheduled call, for instance). To reach one project from such
|
||||
work, call `notify` on that [`Project`](#the-api) (API 1.5, `ui.notify-project`),
|
||||
with the same actions and limits:
|
||||
|
||||
```rust
|
||||
for project in host::projects()? {
|
||||
if let Some(update) = check_for_update(&project)? {
|
||||
let disable = NotificationAction::new("Disable automatic checks", "disable-checks")
|
||||
.argument(project.root()?);
|
||||
project.notify(NotificationLevel::Info, "Update available", &update, &[disable])?;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- It shows only in a window of that project: the most recently focused one
|
||||
when the project is open in several.
|
||||
- A button runs its command with an invocation from that window, so
|
||||
`ctx.project()` is the notification's project.
|
||||
- If the project has closed by the time of the call, the call fails with
|
||||
`Error::NotFound` and shows nothing; it never falls back to another window.
|
||||
A balloon already shown goes away with its window.
|
||||
|
||||
### Selecting plugin tests (API 1.4, unreleased)
|
||||
|
||||
A plugin using `framework = "plugin"` can implement `TestResults::select_tests`
|
||||
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
e3c11d8969a1b8b52e7e745b558d12f804ebf533e7f0f3fe614ffe6f7b9c9468
|
||||
bb67a1b003840ecc2d0c056a0b0af3474b08ac095239c1f4d02b7a7aab5b526c
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
b28ccfd442c6348e0826680ec97391cbe4ec6a93a27ff02e40f47341e21586b4
|
||||
a6289739f5b644ea559ba18298741205ea0bedce7bd1dd2b367309101e8ffb1e
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
d669a2600b0c59e91ac8a9b875a24bddf0eeddd404ce7b67da31733861a846b8
|
||||
767ff8329456fe3a1f324f571b22124707244a61ff5b6ea2c7ed88f909d202e8
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
a5f0ebce6d30e0dc03769ebe5cb82d3704c5ccc32207aa2b3f5500de7b1a9152
|
||||
edb2a28419861c6f2d7cd5c300fa9f3120df5c17fb94596814635a9d97bf56d7
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
ee3ccb648ca4b98a0359521c4cd895b5e7336fd65101563f2ca82b8cbaeedc3d
|
||||
debc0eccd6eb6a93bafecb0d6eca8feb26a3436d9154a278b877eaee567dce80
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
51f7167dd5ed21ed37e227bd3b100a5be5a57e7890a494c942fdd036b1307993
|
||||
6e7cea8633d117d65b293c35bf332d8f4474ab1fdea88aedb37aa189e130857d
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
cd860e7a652951aaa42029da23fdeee7a3c09ab1bfcbfb0d1bb6c89036fc8817
|
||||
e3a0ef77e0c7b54307751d24cc899a0cf782149ebf98833fc1d7b990b369cced
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
db3ee94b5cf4234cee7d61e08a9f8629d7e7f6adbf11208b54cd4ea44cf1fc13
|
||||
39e75e8fc6d9227dce2efc7e29ffc03d4b1a2364e4710d74a4b255850bef1928
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
76d13e772046b192485ea59c7720177b563986e0167475c449a9a5c7430ef908
|
||||
bd06b9b59b6515a1fd8a71271a4d8bb5280b685f400e141eba7365e7daabc7ea
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
a22c9e7ebf79b2d5926424a028edc09aef40293db9ebc959d9ececa1f0ebe4ca
|
||||
3e04301385edee3c3693a89860e49b9d933ee3ad22046cd9cda320c10e64cec4
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
fcc3d666fd1e89363fa8668dd10f89efc49f2376766282b734de4b719fdc9aff
|
||||
e60ead150d753c9824eeed0279ebe0a14f7ad226a2b90977ebeba646397b407b
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
2eb742315f877e3251f992884160ed4d9a2940ee12a7d4d7a52669238d495930
|
||||
b8d20ee5966943cd298ab16a61b1552ddc523b2669ec2d11ad97bb1a86045ce6
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
c2feb6a4cd0e9908f078a3d0074732cfce653cc57cce9d1907838598515048ee
|
||||
65fa71854546a7b19f7afe86642bc1e2c4eee9a7a08a71f87d307c68c95410d0
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
590b46034f6c39592a27cabd5b0b76809e7f7133d258f322604614ecb3479317
|
||||
ce6fdce3010c8d836b452ebac7d0c7e42bf507b35f814ff448696a0541788d4a
|
||||
Бінарний файл не відображається.
@@ -1 +1 @@
|
||||
0d068ea0f0851e6e3166af0a91f290da8dc3f4a852b86050e7f7ced214a39124
|
||||
1d3689b3b18946e41753390f5e37cd6870374884831c5c5e0fd66606a1bfb659
|
||||
@@ -82,6 +82,29 @@ mod imp {
|
||||
wit::ui::notify(level, title, body, &actions).map_err(error)
|
||||
}
|
||||
|
||||
pub fn notify_project(
|
||||
p: &ProjectRes,
|
||||
level: NotificationLevel,
|
||||
title: &str,
|
||||
body: &str,
|
||||
actions: &[NotificationAction],
|
||||
) -> Result<(), Error> {
|
||||
let level = match level {
|
||||
NotificationLevel::Info => wit::types::NotificationLevel::Info,
|
||||
NotificationLevel::Warning => wit::types::NotificationLevel::Warning,
|
||||
NotificationLevel::Error => wit::types::NotificationLevel::Error,
|
||||
};
|
||||
let actions: Vec<_> = actions
|
||||
.iter()
|
||||
.map(|a| wit::ui::NotificationAction {
|
||||
label: a.label.clone(),
|
||||
command: a.command.clone(),
|
||||
argument: a.argument.clone(),
|
||||
})
|
||||
.collect();
|
||||
wit::ui::notify_project(p, level, title, body, &actions).map_err(error)
|
||||
}
|
||||
|
||||
pub fn set_status_item(id: &str, item: &StatusItem) {
|
||||
let item = wit::ui::StatusItem {
|
||||
text: item.text.clone(),
|
||||
@@ -939,6 +962,20 @@ impl Project {
|
||||
imp::vcs_diff(&self.inner, path, against)
|
||||
}
|
||||
|
||||
/// [`notify_with_actions`] in a window of this project only (API 1.5), also from
|
||||
/// scheduled or background work: the most recently focused one when it is open in
|
||||
/// several, and its buttons run there. Once the project has closed this is
|
||||
/// [`Error::NotFound`] and nothing is shown. Needs no capability.
|
||||
pub fn notify(
|
||||
&self,
|
||||
level: NotificationLevel,
|
||||
title: &str,
|
||||
body: &str,
|
||||
actions: &[NotificationAction],
|
||||
) -> Result<(), Error> {
|
||||
imp::notify_project(&self.inner, level, title, body, actions)
|
||||
}
|
||||
|
||||
/// Absolute path of the root. Needs `project.read`.
|
||||
pub fn root(&self) -> Result<String, Error> {
|
||||
imp::project_root(&self.inner)
|
||||
|
||||
@@ -98,7 +98,7 @@ pub mod providers_bindings {
|
||||
export_macro_name: "__export_editor_providers_world",
|
||||
default_bindings_module: "openidea_plugin::providers_bindings",
|
||||
with: {
|
||||
"openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types,
|
||||
"openidea:plugin/types@1.5.0": crate::bindings::openidea::plugin::types,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -115,10 +115,10 @@ pub mod views_bindings {
|
||||
export_macro_name: "__export_views_world",
|
||||
default_bindings_module: "openidea_plugin::views_bindings",
|
||||
with: {
|
||||
"openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types,
|
||||
"openidea:plugin/workspace@1.4.0": crate::bindings::openidea::plugin::workspace,
|
||||
"openidea:plugin/editor@1.4.0": crate::bindings::openidea::plugin::editor,
|
||||
"openidea:plugin/invocation@1.4.0": crate::bindings::openidea::plugin::invocation,
|
||||
"openidea:plugin/types@1.5.0": crate::bindings::openidea::plugin::types,
|
||||
"openidea:plugin/workspace@1.5.0": crate::bindings::openidea::plugin::workspace,
|
||||
"openidea:plugin/editor@1.5.0": crate::bindings::openidea::plugin::editor,
|
||||
"openidea:plugin/invocation@1.5.0": crate::bindings::openidea::plugin::invocation,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -130,7 +130,7 @@ pub mod indexing_bindings {
|
||||
world: "indexing", path: "wit", pub_export_macro: true,
|
||||
export_macro_name: "__export_indexing_world",
|
||||
default_bindings_module: "openidea_plugin::indexing_bindings",
|
||||
with: { "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types },
|
||||
with: { "openidea:plugin/types@1.5.0": crate::bindings::openidea::plugin::types },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -142,10 +142,10 @@ pub mod details_bindings {
|
||||
export_macro_name: "__export_details_world",
|
||||
default_bindings_module: "openidea_plugin::details_bindings",
|
||||
with: {
|
||||
"openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types,
|
||||
"openidea:plugin/workspace@1.4.0": crate::bindings::openidea::plugin::workspace,
|
||||
"openidea:plugin/editor@1.4.0": crate::bindings::openidea::plugin::editor,
|
||||
"openidea:plugin/invocation@1.4.0": crate::bindings::openidea::plugin::invocation,
|
||||
"openidea:plugin/types@1.5.0": crate::bindings::openidea::plugin::types,
|
||||
"openidea:plugin/workspace@1.5.0": crate::bindings::openidea::plugin::workspace,
|
||||
"openidea:plugin/editor@1.5.0": crate::bindings::openidea::plugin::editor,
|
||||
"openidea:plugin/invocation@1.5.0": crate::bindings::openidea::plugin::invocation,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -157,7 +157,7 @@ pub mod semantic_bindings {
|
||||
world: "semantic-tokens", path: "wit", pub_export_macro: true,
|
||||
export_macro_name: "__export_semantic_tokens_world",
|
||||
default_bindings_module: "openidea_plugin::semantic_bindings",
|
||||
with: { "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types },
|
||||
with: { "openidea:plugin/types@1.5.0": crate::bindings::openidea::plugin::types },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -169,8 +169,8 @@ pub mod diagnostics_bindings {
|
||||
export_macro_name: "__export_diagnostics_world",
|
||||
default_bindings_module: "openidea_plugin::diagnostics_bindings",
|
||||
with: {
|
||||
"openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types,
|
||||
"openidea:plugin/diagnostics@1.4.0": crate::bindings::openidea::plugin::diagnostics,
|
||||
"openidea:plugin/types@1.5.0": crate::bindings::openidea::plugin::types,
|
||||
"openidea:plugin/diagnostics@1.5.0": crate::bindings::openidea::plugin::diagnostics,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -182,7 +182,7 @@ pub mod hover_bindings {
|
||||
world: "hover", path: "wit", pub_export_macro: true,
|
||||
export_macro_name: "__export_hover_world",
|
||||
default_bindings_module: "openidea_plugin::hover_bindings",
|
||||
with: { "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types },
|
||||
with: { "openidea:plugin/types@1.5.0": crate::bindings::openidea::plugin::types },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -194,11 +194,11 @@ pub mod commit_checks_bindings {
|
||||
export_macro_name: "__export_commit_checks_world",
|
||||
default_bindings_module: "openidea_plugin::commit_checks_bindings",
|
||||
with: {
|
||||
"openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types,
|
||||
"openidea:plugin/workspace@1.4.0": crate::bindings::openidea::plugin::workspace,
|
||||
"openidea:plugin/editor@1.4.0": crate::bindings::openidea::plugin::editor,
|
||||
"openidea:plugin/invocation@1.4.0": crate::bindings::openidea::plugin::invocation,
|
||||
"openidea:plugin/commit@1.4.0": crate::bindings::openidea::plugin::commit,
|
||||
"openidea:plugin/types@1.5.0": crate::bindings::openidea::plugin::types,
|
||||
"openidea:plugin/workspace@1.5.0": crate::bindings::openidea::plugin::workspace,
|
||||
"openidea:plugin/editor@1.5.0": crate::bindings::openidea::plugin::editor,
|
||||
"openidea:plugin/invocation@1.5.0": crate::bindings::openidea::plugin::invocation,
|
||||
"openidea:plugin/commit@1.5.0": crate::bindings::openidea::plugin::commit,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -210,7 +210,7 @@ pub mod inlay_bindings {
|
||||
world: "inlay-hints", path: "wit", pub_export_macro: true,
|
||||
export_macro_name: "__export_inlay_hints_world",
|
||||
default_bindings_module: "openidea_plugin::inlay_bindings",
|
||||
with: { "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types },
|
||||
with: { "openidea:plugin/types@1.5.0": crate::bindings::openidea::plugin::types },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ pub mod file_actions_bindings {
|
||||
world: "file-actions-provider", path: "wit", pub_export_macro: true,
|
||||
export_macro_name: "__export_file_actions_world",
|
||||
default_bindings_module: "openidea_plugin::file_actions_bindings",
|
||||
with: { "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types },
|
||||
with: { "openidea:plugin/types@1.5.0": crate::bindings::openidea::plugin::types },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -233,16 +233,16 @@ pub mod endpoints_bindings {
|
||||
export_macro_name: "__export_endpoints_world",
|
||||
default_bindings_module: "openidea_plugin::endpoints_bindings",
|
||||
with: {
|
||||
"openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types,
|
||||
"openidea:plugin/workspace@1.4.0": crate::bindings::openidea::plugin::workspace,
|
||||
"openidea:plugin/editor@1.4.0": crate::bindings::openidea::plugin::editor,
|
||||
"openidea:plugin/invocation@1.4.0": crate::bindings::openidea::plugin::invocation,
|
||||
"openidea:plugin/types@1.5.0": crate::bindings::openidea::plugin::types,
|
||||
"openidea:plugin/workspace@1.5.0": crate::bindings::openidea::plugin::workspace,
|
||||
"openidea:plugin/editor@1.5.0": crate::bindings::openidea::plugin::editor,
|
||||
"openidea:plugin/invocation@1.5.0": crate::bindings::openidea::plugin::invocation,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/// The plugin API version this SDK targets.
|
||||
pub const API_VERSION: &str = "1.3";
|
||||
pub const API_VERSION: &str = "1.5";
|
||||
|
||||
/// A plugin. One instance lives as long as the plugin's Wasm instance (until
|
||||
/// it is disabled, crashes or is reclaimed when idle).
|
||||
@@ -1304,7 +1304,7 @@ pub mod run_configurations_bindings {
|
||||
pub mod test_results_bindings {
|
||||
wit_bindgen::generate!({ world: "test-results-provider", path: "wit", pub_export_macro: true,
|
||||
export_macro_name: "__export_tests_world", default_bindings_module: "openidea_plugin::test_results_bindings",
|
||||
with: { "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types },
|
||||
with: { "openidea:plugin/types@1.5.0": crate::bindings::openidea::plugin::types },
|
||||
});
|
||||
}
|
||||
#[doc(hidden)]
|
||||
@@ -1342,7 +1342,7 @@ pub mod language_features;
|
||||
pub mod language_features_bindings {
|
||||
wit_bindgen::generate!({world: "editor-language-features", path: "wit", pub_export_macro: true,
|
||||
export_macro_name: "__export_language_features_world", default_bindings_module: "openidea_plugin::language_features_bindings",
|
||||
with: { "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types },
|
||||
with: { "openidea:plugin/types@1.5.0": crate::bindings::openidea::plugin::types },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1374,7 +1374,7 @@ fn gutter_actions(doc: $crate::language_features_bindings::exports::openidea::pl
|
||||
pub mod injections_bindings {
|
||||
wit_bindgen::generate!({world: "language-injections", path: "wit", pub_export_macro: true,
|
||||
export_macro_name: "__export_injections_world", default_bindings_module: "openidea_plugin::injections_bindings",
|
||||
with: { "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types },
|
||||
with: { "openidea:plugin/types@1.5.0": crate::bindings::openidea::plugin::types },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1404,8 +1404,8 @@ pub mod mcp_bindings {
|
||||
wit_bindgen::generate!({ world: "mcp-tools", path: "wit", pub_export_macro: true,
|
||||
export_macro_name: "__export_mcp_world", default_bindings_module: "openidea_plugin::mcp_bindings",
|
||||
with: {
|
||||
"openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types,
|
||||
"openidea:plugin/invocation@1.4.0": crate::bindings::openidea::plugin::invocation,
|
||||
"openidea:plugin/types@1.5.0": crate::bindings::openidea::plugin::types,
|
||||
"openidea:plugin/invocation@1.5.0": crate::bindings::openidea::plugin::invocation,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ struct State {
|
||||
selected_paths: Vec<String>,
|
||||
notifications: Vec<Notification>,
|
||||
action_notifications: Vec<ActionNotification>,
|
||||
project_notifications: Vec<(String, ActionNotification)>,
|
||||
logs: Vec<(LogLevel, String)>,
|
||||
status: BTreeMap<String, StatusItem>,
|
||||
window_status: BTreeMap<String, StatusItem>,
|
||||
@@ -341,6 +342,11 @@ impl MockHost {
|
||||
self.state.borrow().action_notifications.clone()
|
||||
}
|
||||
|
||||
/// Notifications sent to a project (`Project::notify`), with its root, in publication order.
|
||||
pub fn project_notifications(&self) -> Vec<(String, ActionNotification)> {
|
||||
self.state.borrow().project_notifications.clone()
|
||||
}
|
||||
|
||||
pub fn logs(&self) -> Vec<(LogLevel, String)> {
|
||||
self.state.borrow().logs.clone()
|
||||
}
|
||||
@@ -773,6 +779,35 @@ pub mod current {
|
||||
body: &str,
|
||||
actions: &[NotificationAction],
|
||||
) -> Result<(), Error> {
|
||||
check_actions(actions)?;
|
||||
with(|s| {
|
||||
let notification = Notification { level, title: title.into(), body: body.into() };
|
||||
s.notifications.push(notification.clone());
|
||||
s.action_notifications.push(ActionNotification { notification, actions: actions.to_vec() });
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The checks of `notify_with_actions`; a project no longer open is `NotFound`.
|
||||
pub fn notify_project(
|
||||
p: &ProjectRes,
|
||||
level: NotificationLevel,
|
||||
title: &str,
|
||||
body: &str,
|
||||
actions: &[NotificationAction],
|
||||
) -> Result<(), Error> {
|
||||
let open = with(|s| s.root.iter().chain(&s.other_projects).any(|root| *root == p.root));
|
||||
if !open {
|
||||
return Err(Error::NotFound(format!("the project {} is not open", p.root)));
|
||||
}
|
||||
check_actions(actions)?;
|
||||
let notification = Notification { level, title: title.into(), body: body.into() };
|
||||
let sent = ActionNotification { notification, actions: actions.to_vec() };
|
||||
with(|s| s.project_notifications.push((p.root.clone(), sent)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_actions(actions: &[NotificationAction]) -> Result<(), Error> {
|
||||
if actions.len() > 3 {
|
||||
return Err(Error::Invalid("a notification has at most 3 actions".into()));
|
||||
}
|
||||
@@ -784,11 +819,6 @@ pub mod current {
|
||||
return Err(Error::Invalid("an action argument is limited to 64 KiB".into()));
|
||||
}
|
||||
}
|
||||
with(|s| {
|
||||
let notification = Notification { level, title: title.into(), body: body.into() };
|
||||
s.notifications.push(notification.clone());
|
||||
s.action_notifications.push(ActionNotification { notification, actions: actions.to_vec() });
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2371,6 +2401,24 @@ mod tests {
|
||||
assert_eq!(mock.notifications().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_notifications_need_an_open_project() {
|
||||
let mock = MockHost::install();
|
||||
mock.add_project("/other");
|
||||
let action = NotificationAction::new("Disable", "disable").argument("/other");
|
||||
let other = host::project_of("/other/a.txt").unwrap();
|
||||
other.notify(NotificationLevel::Info, "Update", "", std::slice::from_ref(&action)).unwrap();
|
||||
let sent = mock.project_notifications();
|
||||
assert_eq!((sent[0].0.as_str(), &sent[0].1.actions), ("/other", &vec![action.clone()]));
|
||||
assert!(mock.notifications().is_empty() && mock.notifications_with_actions().is_empty());
|
||||
let too_many: [_; 4] = std::array::from_fn(|_| action.clone());
|
||||
assert!(matches!(other.notify(NotificationLevel::Info, "Update", "", &too_many), Err(Error::Invalid(_))));
|
||||
let project = host::project_of("/project/a.txt").unwrap();
|
||||
mock.set_project_root(None);
|
||||
assert!(matches!(project.notify(NotificationLevel::Info, "Update", "", &[]), Err(Error::NotFound(_))));
|
||||
assert_eq!(mock.project_notifications().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn files_outside_the_project_need_an_fs_read_grant() {
|
||||
let mock = MockHost::install().with_capabilities(&["fs.read:/opt/tool"]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package openidea:plugin@1.4.0;
|
||||
package openidea:plugin@1.5.0;
|
||||
|
||||
/// The commit being prepared in the window the current command or event came
|
||||
/// from (the Commit tool window, or the Commit dialog while it is open): the
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package openidea:plugin@1.4.0;
|
||||
package openidea:plugin@1.5.0;
|
||||
|
||||
/// Plugin-owned project data sources. Requires database.sources and project trust.
|
||||
/// Runs on the Database subsystem's host, including on remote project servers.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package openidea:plugin@1.4.0;
|
||||
package openidea:plugin@1.5.0;
|
||||
|
||||
/// Project-scoped decorations (API 1.4). Requires project.read; paths must be inside
|
||||
/// an open project. Sets belong to the plugin and disappear when it stops.
|
||||
|
||||
Loaded 100 of 149 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Заблокувати користувача