feat(plugins): deliver batched filesystem events (#2049)

Closes #1948

Plugins receive debounced batches of files created, changed and removed outside the editor in open projects, as SDK `Event::FilesChanged` (delivered through `other("files-changed", …)`, so released WIT is unchanged), under the `events` capability and the plugin's path filters. Activation on these events requires API 1.3. Batches are coalesced per plugin through its actor, survive cancellation without losing deltas, and are dropped when the project or host shuts down.

Co-authored-by: arnaud-ritti <[email protected]>
This commit is contained in:
Arnaud Ritti authored and GitHub committed 2026-11-08 07:25:01 +08:00
1 parent 7ecb452350
commit 63065bab64
123 files changed
+1832 -97

No files matched your search

+2 -2
View File
@@ -26,7 +26,7 @@ pub enum Capability {
Storage,
/// The plugin's own secrets (kept in the client's OS keychain).
Secrets,
/// Workbench events (file opened, saved, focus…), not keystrokes.
/// Workbench events (files, focus, VCS), not keystrokes.
Events,
/// Keystrokes and editor input events.
InputEvents,
@@ -86,7 +86,7 @@ impl Capability {
Self::VcsWrite => "Change version control state (commit, fetch, checkout)".into(),
Self::Storage => "Store its own data".into(),
Self::Secrets => "Store secrets in the system keychain".into(),
Self::Events => "Observe workbench events (files opened, saved, focus)".into(),
Self::Events => "Observe editor, filesystem and version control events".into(),
Self::InputEvents => "Observe keystrokes and editor input".into(),
Self::Clipboard => {
"Copy to the clipboard from its commands, and read and write it from its web views".into()
+22 -1
View File
@@ -144,7 +144,8 @@ impl ActivationEvent {
/// Workbench event kinds (`on-event:<kind>`, WIT `events.event-kind`).
/// `scheduled` comes from `on-schedule` instead.
pub const EVENT_KINDS: &[&str] = &["file-opened", "file-saved", "editor-focused", "action-performed", "vcs-changed"];
pub const EVENT_KINDS: &[&str] =
&["file-opened", "file-saved", "files-changed", "editor-focused", "action-performed", "vcs-changed"];
impl std::fmt::Display for ActivationEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -331,6 +332,9 @@ impl Manifest {
}
let mut seen = BTreeSet::new();
for event in &self.activation {
if matches!(event, ActivationEvent::Event(kind) if kind == "files-changed") {
self.needs_api("`on-event:files-changed`", 3)?;
}
if !seen.insert(event) {
return Err(format!("duplicate activation event {event}"));
}
@@ -495,6 +499,10 @@ mod tests {
fn event_activation() {
assert_eq!(ActivationEvent::parse("on-event:file-saved"), Ok(ActivationEvent::Event("file-saved".into())));
assert_eq!(ActivationEvent::Event("vcs-changed".into()).to_string(), "on-event:vcs-changed");
assert_eq!(
ActivationEvent::parse("on-event:files-changed"),
Ok(ActivationEvent::Event("files-changed".into()))
);
let err = ActivationEvent::parse("on-event:keystroke").unwrap_err();
assert!(err.contains("unknown event kind") && err.contains("action-performed"), "{err}");
assert!(ActivationEvent::parse("on-event:scheduled").is_err(), "declared with on-schedule");
@@ -588,6 +596,19 @@ mod tests {
const BASE: &str = "id = \"ok\"\nname = \"N\"\nversion = \"1.0.0\"\nengines.openidea = \"^1.2\"\n";
#[test]
fn files_changed_activation_needs_api_1_3() {
let activation = "wasm = \"plugin.wasm\"\nactivation = [\"on-event:files-changed\"]\n";
for engine in ["^1.0", "^1.2", ">=1.1, <2"] {
let base = BASE.replace("^1.2", engine);
assert!(err(&format!("{base}{activation}")).contains("new in plugin API 1.3"), "{engine}");
}
for engine in ["^1.3", ">=1.3, <2"] {
Manifest::parse(&format!("{}{activation}", BASE.replace("^1.2", engine))).unwrap();
}
Manifest::parse(&format!("{BASE}{}", activation.replace("files-changed", "file-saved"))).unwrap();
}
#[test]
fn write_capabilities_need_api_1_3() {
for cap in ["project.write", "fs.write:~/tool"] {
+4
View File
@@ -224,6 +224,7 @@ impl Host {
self.cancel_request(request);
}
let project = self.projects.write().remove(window)?;
project.files_events_active.store(false, std::sync::atomic::Ordering::Release);
project.plugin_diagnostics.close();
if let Some(plugins) = plugins::editor_plugin_host() {
plugins.invalidate_client_navigation(&project.root, Some(window), None);
@@ -374,6 +375,9 @@ impl Host {
self.processes.kill_all();
self.terminals.kill_all();
let projects: Vec<_> = self.projects.write().drain().map(|(_, p)| p).collect();
for project in &projects {
project.files_events_active.store(false, std::sync::atomic::Ordering::Release);
}
for project in &projects {
project.plugin_diagnostics.close();
if let Some(bridge) = &project.kotlin_bridge {
+39 -14
View File
@@ -21,8 +21,10 @@ pub(super) struct PathFilter {
impl PathFilter {
pub fn new(kind: &str, globs: Vec<String>) -> Result<Self, HostError> {
if !matches!(kind, "file-opened" | "file-saved") {
return Err(HostError::Invalid("subscribe-paths supports file-opened and file-saved".into()));
if !matches!(kind, "file-opened" | "file-saved" | "files-changed") {
return Err(HostError::Invalid(
"subscribe-paths supports file-opened, file-saved and files-changed".into(),
));
}
if globs.len() > 64 {
return Err(HostError::Invalid("at most 64 event path globs".into()));
@@ -87,26 +89,50 @@ impl Subscriptions {
impl PluginHost {
pub(super) fn filter_path_events(
&self,
shared: &super::api::Shared,
plugin: &LoadedPlugin,
client: Option<&str>,
events: &[WorkbenchEvent],
subs: &Subscriptions,
) -> Vec<WorkbenchEvent> {
let Some(filters) = subs.paths.get(plugin.id()) else { return events.to_vec() };
let workspace = self.shared.workspace();
let workspace = shared.workspace();
events
.iter()
.filter(|event| {
.filter_map(|event| {
if let WorkbenchEvent::FilesChanged(batch) = event {
let Some(filter) = filters.get(event.kind().name()) else { return Some(event.clone()) };
let root = workspace.as_ref()?.project_of(client?)?;
if plugin.dev_root.as_deref().is_some_and(|r| r != root) {
return None;
}
let paths = |paths: &[String]| {
paths
.iter()
.filter(|path| {
let path = Path::new(path);
path.is_absolute() && filter.matches(&root, path)
})
.cloned()
.collect::<Vec<_>>()
};
let batch = ide_plugin::FilesChanged {
created: paths(&batch.created),
modified: paths(&batch.modified),
removed: paths(&batch.removed),
};
return (!(batch.created.is_empty() && batch.modified.is_empty() && batch.removed.is_empty()))
.then_some(WorkbenchEvent::FilesChanged(batch));
}
let path = match event {
WorkbenchEvent::FileOpened(path) | WorkbenchEvent::FileSaved(path) => Path::new(path),
_ => return true,
_ => return Some(event.clone()),
};
let Some(filter) = filters.get(event.kind().name()) else { return true };
let Some(filter) = filters.get(event.kind().name()) else { return Some(event.clone()) };
if !path.is_absolute() {
return false;
return None;
}
let Some(workspace) = &workspace else { return false };
let workspace = workspace.as_ref()?;
let root = match client {
Some(client) => workspace.project_of(client),
None => {
@@ -117,14 +143,12 @@ impl PluginHost {
.filter(|r| real.starts_with(ide_plugin::resolve_real(r)))
.max_by_key(|r| r.components().count())
}
};
let Some(root) = root else { return false };
}?;
if plugin.dev_root.as_deref().is_some_and(|r| r != root) {
return false;
return None;
}
filter.matches(&root, path)
filter.matches(&root, path).then(|| event.clone())
})
.cloned()
.collect()
}
}
@@ -142,6 +166,7 @@ mod tests {
assert!(PathFilter::new("file-saved", vec!["*".into(); 65]).is_err());
assert!(PathFilter::new("file-saved", vec!["./".repeat(513)]).is_err());
assert!(PathFilter::new("file-saved", vec![]).is_ok());
assert!(PathFilter::new("files-changed", vec![]).is_ok());
}
#[test]
@@ -0,0 +1,663 @@
//! Filesystem batches retain their project incarnation until delivery.
use super::{PluginHost, WorkbenchEvent};
use ide_core::ipc_path::to_ipc;
use ide_plugin::FilesChanged;
use std::path::{Path, PathBuf};
use std::sync::{
Arc, Weak,
atomic::{AtomicBool, Ordering},
};
#[derive(Clone)]
pub(super) struct Source {
active: Weak<AtomicBool>,
}
impl Source {
pub fn same(&self, other: &Self) -> bool {
self.active.ptr_eq(&other.active)
}
fn live(&self) -> bool {
self.active.upgrade().is_some_and(|active| active.load(Ordering::Acquire))
}
}
pub(super) fn retain_live(events: &mut Vec<WorkbenchEvent>, source: Option<&Source>) {
if source.is_some_and(|source| !source.live()) {
events.retain(|e| !matches!(e, WorkbenchEvent::FilesChanged(_)));
}
}
pub(super) fn append(events: &mut Vec<WorkbenchEvent>, incoming: Vec<WorkbenchEvent>) {
for event in incoming {
if let WorkbenchEvent::FilesChanged(batch) = &event
&& let Some(WorkbenchEvent::FilesChanged(previous)) =
events.iter_mut().find(|e| matches!(e, WorkbenchEvent::FilesChanged(_)))
{
use std::collections::BTreeMap;
#[derive(Clone, Copy)]
enum Change {
Created,
Modified,
Removed,
}
use Change::*;
let mut paths = BTreeMap::new();
for (kind, entries) in
[(Created, &previous.created), (Modified, &previous.modified), (Removed, &previous.removed)]
{
for path in entries {
paths.insert(path.clone(), kind);
}
}
for (next, entries) in [(Created, &batch.created), (Modified, &batch.modified), (Removed, &batch.removed)] {
for path in entries {
let kind = match (paths.get(path), next) {
(Some(Created), Created | Modified) => Created,
(Some(Removed), Created) | (Some(Modified), Created | Modified) => Modified,
(Some(Removed), Modified) => Removed,
(_, next) => next,
};
paths.insert(path.clone(), kind);
}
}
*previous = FilesChanged::default();
for (path, kind) in paths {
match kind {
Created => previous.created.push(path),
Modified => previous.modified.push(path),
Removed => previous.removed.push(path),
}
}
} else {
events.push(event);
}
}
}
impl PluginHost {
pub(crate) fn post_files_changed(
&self,
client: &str,
root: &Path,
active: &Arc<AtomicBool>,
batch: &ide_vfs::FsBatch,
) {
if !active.load(Ordering::Acquire) || self.shared.project_of(client).as_deref() != Some(root) {
return;
}
let paths =
|paths: &[PathBuf]| paths.iter().filter(|p| p.starts_with(root)).map(|p| to_ipc(p)).collect::<Vec<_>>();
let batch = FilesChanged {
created: paths(&batch.created),
modified: paths(&batch.modified),
removed: paths(&batch.removed),
};
if batch.created.is_empty() && batch.modified.is_empty() && batch.removed.is_empty() {
return;
}
self.post_events_from(
Some(client),
vec![WorkbenchEvent::FilesChanged(batch)],
Some(Source { active: Arc::downgrade(active) }),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn files_changed_host(root: &Path) -> PluginHost {
use super::super::Consent;
let host =
PluginHost::new(root.join("plugins"), Arc::new(|_: Option<&str>, _| {}), Default::default()).unwrap();
host.set_workspace(Some(Arc::new(Workspace(root.to_path_buf()))));
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../ide-plugin/tests/fixtures/files-events");
host.install_from_dir(&fixture, &Consent::all()).unwrap();
host
}
fn delta(kind: usize, path: &str) -> WorkbenchEvent {
let mut batch = FilesChanged::default();
match kind {
0 => batch.created.push(path.into()),
1 => batch.modified.push(path.into()),
_ => batch.removed.push(path.into()),
}
WorkbenchEvent::FilesChanged(batch)
}
#[test]
fn files_changed_preserves_ordered_path_transitions() {
for (first, next, expected) in
[(0, 0, 0), (0, 1, 0), (0, 2, 2), (1, 0, 1), (1, 1, 1), (1, 2, 2), (2, 0, 1), (2, 1, 2), (2, 2, 2)]
{
let mut events = vec![delta(first, "/project/path")];
append(&mut events, vec![delta(next, "/project/path")]);
assert_eq!(events, [delta(expected, "/project/path")], "transition {first} -> {next}");
}
let mut events = vec![delta(0, "/project/transient"), delta(2, "/project/old-name")];
append(&mut events, vec![delta(2, "/project/transient"), delta(0, "/project/new-name")]);
append(&mut events, vec![delta(1, "/project/transient")]);
// Seed through append too: one coalesced batch keeps both sides of a rename.
let mut merged = vec![];
append(&mut merged, events);
assert_eq!(
merged,
[WorkbenchEvent::FilesChanged(FilesChanged {
created: vec!["/project/new-name".into()],
removed: vec!["/project/old-name".into(), "/project/transient".into()],
..Default::default()
})]
);
}
#[test]
fn files_changed_stale_admission_cannot_replace_reopened_queue() {
let tmp = tempfile::tempdir().unwrap();
let root = dunce::canonicalize(tmp.path()).unwrap();
let host = files_changed_host(&root);
let old = Arc::new(AtomicBool::new(true));
let new = Arc::new(AtomicBool::new(true));
let (entered, waiting) = std::sync::mpsc::channel();
let (release, released) = std::sync::mpsc::channel();
std::thread::scope(|scope| {
let host = &host;
let old = &old;
let root = &root;
scope.spawn(move || {
assert!(old.load(Ordering::Acquire));
entered.send(()).unwrap();
released.recv().unwrap();
host.post_events_from(
Some("main"),
vec![delta(0, &to_ipc(&root.join("stale.rs")))],
Some(Source { active: Arc::downgrade(old) }),
);
});
waiting.recv().unwrap();
old.store(false, Ordering::Release);
host.post_files_changed(
"main",
root,
&new,
&ide_vfs::FsBatch { created: vec![root.join("new.rs")], ..Default::default() },
);
release.send(()).unwrap();
});
let queues = host.queues.lock();
let queue = queues.values().next().expect("new session queue");
assert!(queue.files_source.as_ref().unwrap().same(&Source { active: Arc::downgrade(&new) }));
assert_eq!(queue.events, [delta(0, &to_ipc(&root.join("new.rs")))]);
}
#[test]
fn files_changed_restarts_after_an_actor_drops_queued_work() {
use std::time::Duration;
let tmp = tempfile::tempdir().unwrap();
let root = dunce::canonicalize(tmp.path()).unwrap();
let host = files_changed_host(&root);
let plugin = host.get("files-events").unwrap();
let (actor, _) = host.ensure_active(&plugin, Some("main")).unwrap();
let (entered, waiting) = std::sync::mpsc::channel();
let (release, released) = std::sync::mpsc::channel();
let crash = actor
.submit(None, move |_| {
entered.send(()).unwrap();
released.recv().unwrap();
Err::<(), _>(ide_plugin::CallError::Trap(ide_plugin::CANCELLED.into()))
})
.unwrap();
waiting.recv_timeout(Duration::from_secs(10)).unwrap();
host.post_events(Some("main"), vec![delta(0, &to_ipc(&root.join("discarded.rs")))]);
host.flush_events();
release.send(()).unwrap();
assert!(crash.wait().is_err());
host.flush_events();
host.post_events(Some("main"), vec![delta(0, &to_ipc(&root.join("after-restart.rs")))]);
host.flush_events();
assert!(host.wait_events(Duration::from_secs(1)), "dropped actor work must not keep its mailbox alive");
let data = host.run_command("files-events", "events", None, Default::default()).unwrap().unwrap();
let batches: Vec<FilesChanged> = serde_json::from_str(&data).unwrap();
assert_eq!(
batches,
[FilesChanged { created: vec![to_ipc(&root.join("after-restart.rs"))], ..Default::default() }]
);
}
#[test]
fn files_changed_actor_mailbox_is_bounded_and_yields_to_commands() {
use std::time::Duration;
let tmp = tempfile::tempdir().unwrap();
let root = dunce::canonicalize(tmp.path()).unwrap();
let host = files_changed_host(&root);
let plugin = host.get("files-events").unwrap();
let (actor, _) = host.ensure_active(&plugin, Some("main")).unwrap();
let (entered, waiting) = std::sync::mpsc::channel();
let (release, released) = std::sync::mpsc::channel();
actor.post(
move |_| {
entered.send(()).unwrap();
released.recv().unwrap();
Ok(())
},
|_| {},
);
waiting.recv_timeout(Duration::from_secs(10)).unwrap();
for i in 0..600 {
host.post_events(Some("main"), vec![delta(i % 3, &to_ipc(&root.join(format!("path-{i}"))))]);
host.flush_events();
}
let pending = plugin.batches.lock().pending;
// A command gets a scheduling turn after one coalesced delivery.
let command = actor.submit(None, move |rt| rt.run_command("events", None, Default::default())).unwrap();
release.send(()).unwrap();
let data = command.wait().unwrap().unwrap();
assert_eq!(pending, 1, "flushes must not grow the actor job queue");
let batches: Vec<FilesChanged> = serde_json::from_str(&data).unwrap();
assert_eq!(batches.len(), 1);
assert_eq!(batches[0].created.len(), 200);
assert_eq!(batches[0].modified.len(), 200);
assert_eq!(batches[0].removed.len(), 200);
assert!(host.wait_events(Duration::from_secs(10)));
}
#[test]
fn files_changed_rechecks_filters_after_a_busy_actor_command() {
use std::time::Duration;
for globs in ["", "other/**", "config/*.yaml"] {
let tmp = tempfile::tempdir().unwrap();
let root = dunce::canonicalize(tmp.path()).unwrap();
let host = files_changed_host(&root);
let plugin = host.get("files-events").unwrap();
let (actor, _) = host.ensure_active(&plugin, Some("main")).unwrap();
let (entered, waiting) = std::sync::mpsc::channel();
let (release, released) = std::sync::mpsc::channel();
actor.post(
move |_| {
entered.send(()).unwrap();
released.recv().unwrap();
Ok(())
},
|_| {},
);
waiting.recv_timeout(Duration::from_secs(10)).unwrap();
let filter =
actor.submit(None, move |rt| rt.run_command("filter", Some(globs), Default::default())).unwrap();
host.post_events(
Some("main"),
vec![WorkbenchEvent::FilesChanged(FilesChanged {
created: vec![to_ipc(&root.join("config/a.yaml")), to_ipc(&root.join("src/a.rs"))],
modified: vec![to_ipc(&root.join("config/b.yaml"))],
removed: vec![to_ipc(&root.join("config/c.yaml"))],
})],
);
host.flush_events();
release.send(()).unwrap();
filter.wait().unwrap();
assert!(host.wait_events(Duration::from_secs(10)));
let data = host.run_command("files-events", "events", None, Default::default()).unwrap();
if globs == "config/*.yaml" {
let batches: Vec<FilesChanged> = serde_json::from_str(&data.unwrap()).unwrap();
assert_eq!(
batches,
[FilesChanged {
created: vec![to_ipc(&root.join("config/a.yaml"))],
modified: vec![to_ipc(&root.join("config/b.yaml"))],
removed: vec![to_ipc(&root.join("config/c.yaml"))],
}]
);
} else {
assert_eq!(data, None, "excluded batches must not invoke the guest handler");
}
}
}
#[test]
fn files_changed_delivery_honors_filters_registered_during_activation() {
use super::super::Consent;
use std::time::{Duration, Instant};
let tmp = tempfile::tempdir().unwrap();
let root = dunce::canonicalize(tmp.path()).unwrap();
let host =
PluginHost::new(root.join("plugins"), Arc::new(|_: Option<&str>, _| {}), Default::default()).unwrap();
host.set_workspace(Some(Arc::new(Workspace(root.clone()))));
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../ide-plugin/tests/fixtures/path-events");
let copy = root.join("activation-fixture");
std::fs::create_dir_all(copy.join("dist")).unwrap();
std::fs::write(
copy.join("plugin.toml"),
std::fs::read_to_string(fixture.join("plugin.toml"))
.unwrap()
.replace("[\"on-command:*\"]", "[\"on-command:*\", \"on-event:file-opened\"]"),
)
.unwrap();
std::fs::copy(fixture.join("dist/plugin.wasm"), copy.join("dist/plugin.wasm")).unwrap();
host.install_from_dir(&copy, &Consent::all()).unwrap();
host.post_events(Some("main"), vec![WorkbenchEvent::FileOpened(to_ipc(&root.join("src/excluded.rs")))]);
host.flush_events_at(Instant::now() + Duration::from_secs(3));
assert!(host.wait_events(Duration::from_secs(10)));
assert_eq!(host.run_command("test.path-events", "count", None, Default::default()).unwrap(), None);
}
#[test]
fn files_changed_rechecks_runtime_unsubscribe_on_busy_actor() {
use super::super::Consent;
use std::time::Duration;
let tmp = tempfile::tempdir().unwrap();
let root = dunce::canonicalize(tmp.path()).unwrap();
let host =
PluginHost::new(root.join("plugins"), Arc::new(|_: Option<&str>, _| {}), Default::default()).unwrap();
host.set_workspace(Some(Arc::new(Workspace(root.clone()))));
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../ide-plugin/tests/fixtures/files-events");
let copy = root.join("runtime-fixture");
std::fs::create_dir_all(copy.join("dist")).unwrap();
std::fs::write(
copy.join("plugin.toml"),
std::fs::read_to_string(fixture.join("plugin.toml")).unwrap().replace(", \"on-event:files-changed\"", ""),
)
.unwrap();
std::fs::copy(fixture.join("dist/plugin.wasm"), copy.join("dist/plugin.wasm")).unwrap();
host.install_from_dir(&copy, &Consent::all()).unwrap();
host.run_command("files-events", "filter", Some("**"), Default::default()).unwrap();
let plugin = host.get("files-events").unwrap();
let (actor, _) = host.ensure_active(&plugin, Some("main")).unwrap();
let (entered, waiting) = std::sync::mpsc::channel();
let (release, released) = std::sync::mpsc::channel();
actor.post(
move |_| {
entered.send(()).unwrap();
released.recv().unwrap();
Ok(())
},
|_| {},
);
waiting.recv_timeout(Duration::from_secs(10)).unwrap();
let off = actor.submit(None, |rt| rt.run_command("unsubscribe", None, Default::default())).unwrap();
host.post_events(Some("main"), vec![delta(0, &to_ipc(&root.join("excluded.rs")))]);
host.flush_events();
release.send(()).unwrap();
off.wait().unwrap();
assert!(host.wait_events(Duration::from_secs(10)));
assert_eq!(host.run_command("files-events", "events", None, Default::default()).unwrap(), None);
}
#[test]
fn files_changed_dropped_actor_job_releases_mailbox_before_restart() {
use std::time::Duration;
let tmp = tempfile::tempdir().unwrap();
let root = dunce::canonicalize(tmp.path()).unwrap();
let host = files_changed_host(&root);
let plugin = host.get("files-events").unwrap();
let (actor, _) = host.ensure_active(&plugin, Some("main")).unwrap();
let (entered, waiting) = std::sync::mpsc::channel();
let (release, released) = std::sync::mpsc::channel();
actor.post(
move |_| {
entered.send(()).unwrap();
released.recv().unwrap();
Err::<(), _>(ide_plugin::CallError::Trap("test actor exit".into()))
},
|_| {},
);
waiting.recv_timeout(Duration::from_secs(10)).unwrap();
host.post_events(Some("main"), vec![delta(0, &to_ipc(&root.join("dropped.rs")))]);
host.flush_events();
assert_eq!(plugin.batches.lock().pending, 1);
release.send(()).unwrap();
actor.stop(true);
assert_eq!(plugin.batches.lock().pending, 0, "dropped closures must release their batch ticket");
assert!(plugin.batches.lock().files_pending.is_empty());
host.set_enabled("files-events", false).unwrap();
host.set_enabled("files-events", true).unwrap();
host.post_events(Some("main"), vec![delta(0, &to_ipc(&root.join("fresh.rs")))]);
host.flush_events();
assert!(host.wait_events(Duration::from_secs(10)));
let data = host.run_command("files-events", "events", None, Default::default()).unwrap().unwrap();
let batches: Vec<FilesChanged> = serde_json::from_str(&data).unwrap();
assert_eq!(batches, [FilesChanged { created: vec![to_ipc(&root.join("fresh.rs"))], ..Default::default() }]);
}
#[test]
fn files_changed_old_ticket_cannot_clear_successor_mailbox_or_running_state() {
use super::super::{BatchTicket, Batches, PendingFiles};
let batches = Arc::new(parking_lot::Mutex::new(Batches::default()));
let key = (Some("main".into()), Some(PathBuf::from("/project")));
let old = Arc::new(());
let new = Arc::new(());
let ticket = BatchTicket { batches: batches.clone(), files_key: Some(key.clone()), token: old };
{
let mut state = batches.lock();
state.pending = 2;
state.running = Some([ide_plugin::EventKind::FilesChanged].into());
state.running_token = Some(new.clone());
state.files_pending.insert(
key.clone(),
PendingFiles {
queue: super::super::EventQueue {
since: std::time::Instant::now(),
events: vec![delta(0, "/project/new")],
files_source: None,
},
actor: std::sync::Weak::new(),
token: new.clone(),
},
);
}
drop(ticket);
let state = batches.lock();
assert_eq!(state.pending, 1);
assert!(Arc::ptr_eq(&state.files_pending[&key].token, &new));
assert!(Arc::ptr_eq(state.running_token.as_ref().unwrap(), &new));
assert!(state.running.is_some());
}
#[test]
fn every_runtime_event_has_a_matching_manifest_activation() {
for kind in ide_plugin::EventKind::ALL {
if kind != ide_plugin::EventKind::Scheduled {
assert!(ide_extension::EVENT_KINDS.contains(&kind.name()));
}
}
for kind in ide_extension::EVENT_KINDS {
assert!(ide_plugin::EventKind::parse(kind).is_some());
}
}
#[test]
fn batches_coalesce_without_losing_categories_or_paths() {
let mut events = vec![];
for i in 0..600 {
append(
&mut events,
vec![WorkbenchEvent::FilesChanged(FilesChanged {
created: vec![format!("/project/{i}")],
modified: vec!["/project/shared".into()],
removed: vec!["/project/gone".into()],
})],
);
}
let [WorkbenchEvent::FilesChanged(batch)] = events.as_slice() else { panic!("one coalesced batch") };
assert_eq!(batch.created.len(), 600);
assert_eq!(batch.modified, ["/project/shared"]);
assert_eq!(batch.removed, ["/project/gone"]);
}
#[test]
fn closed_incarnations_drop_only_filesystem_events() {
let active = Arc::new(AtomicBool::new(true));
let source = Source { active: Arc::downgrade(&active) };
let mut events = vec![WorkbenchEvent::FilesChanged(FilesChanged::default()), WorkbenchEvent::Scheduled];
retain_live(&mut events, Some(&source));
assert_eq!(events.len(), 2);
active.store(false, Ordering::Release);
retain_live(&mut events, Some(&source));
assert_eq!(events, [WorkbenchEvent::Scheduled]);
assert!(!source.same(&Source { active: Arc::downgrade(&Arc::new(AtomicBool::new(true))) }));
}
struct Workspace(PathBuf);
impl super::super::PluginWorkspace for Workspace {
fn projects(&self) -> Vec<PathBuf> {
vec![self.0.clone(), self.0.join("other")]
}
fn buffer_text(&self, _: &Path) -> Option<String> {
None
}
fn document(&self, _: &Path) -> Option<(u64, String)> {
None
}
fn apply_edits(&self, _: &Path, _: u64, _: &[ide_core::TextEdit]) -> Result<u64, ide_plugin::HostError> {
unreachable!()
}
fn trusted(&self, _: &Path) -> bool {
true
}
fn project_of(&self, window: &str) -> Option<PathBuf> {
match window {
"main" => Some(self.0.clone()),
"other" => Some(self.0.join("other")),
_ => None,
}
}
fn publish_diagnostics(
&self,
_: &str,
_: &Path,
_: Vec<ide_core::Diagnostic>,
) -> Result<(), ide_plugin::HostError> {
Ok(())
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn host_shutdown_discards_queued_filesystem_batches() {
use super::super::Consent;
let tmp = tempfile::tempdir().unwrap();
let root = dunce::canonicalize(tmp.path()).unwrap().join("project");
std::fs::create_dir(&root).unwrap();
let backend = crate::Host::new(crate::HostConfig {
config_dir: tmp.path().join("config"),
data_dir: tmp.path().join("data"),
local_data_dir: tmp.path().join("data"),
languages: Arc::new(ide_language::LanguageRegistry::load(None).unwrap()),
syntax: Arc::default(),
sink_for: Box::new(|_| Arc::new(ide_core::NullSink)),
owner: |window| window,
runtime: tokio::runtime::Handle::current(),
project_environment: false,
});
backend.open_project("main", &root).unwrap();
let active = backend.project("main").unwrap().files_events_active.clone();
let plugins =
PluginHost::new(tmp.path().join("plugins"), Arc::new(|_: Option<&str>, _| {}), Default::default()).unwrap();
plugins.set_workspace(Some(Arc::new(Workspace(root.clone()))));
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../ide-plugin/tests/fixtures/files-events");
plugins.install_from_dir(&fixture, &Consent::all()).unwrap();
let batch = ide_vfs::FsBatch { created: vec![root.join("new.rs")], ..Default::default() };
plugins.post_files_changed("main", &root, &active, &batch);
assert!(!plugins.queues.lock().is_empty());
backend.shutdown().await;
plugins.flush_events();
assert_eq!(plugins.instances(), 0, "host shutdown must discard queued filesystem batches");
plugins.post_files_changed("main", &root, &active, &batch);
assert!(plugins.queues.lock().is_empty(), "a retained watcher cannot post after host shutdown");
}
#[test]
fn producer_respects_project_incarnation_window_and_dev_root() {
use super::super::{Consent, PluginSource};
use std::time::Duration;
let tmp = tempfile::tempdir().unwrap();
let root = dunce::canonicalize(tmp.path()).unwrap();
std::fs::create_dir(root.join("other")).unwrap();
let host =
PluginHost::new(root.join("plugins"), Arc::new(|_: Option<&str>, _| {}), Default::default()).unwrap();
host.set_workspace(Some(Arc::new(Workspace(root.clone()))));
host.set_project_policy(Arc::new(|_| true));
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../ide-plugin/tests/fixtures/files-events");
host.load_dir(&fixture, PluginSource::Dev, Some(root.clone()));
host.grant("files-events", &Consent::all()).unwrap();
let active = Arc::new(AtomicBool::new(true));
let batch = ide_vfs::FsBatch {
created: vec![root.join("new.rs"), root.parent().unwrap().join("outside.rs")],
..Default::default()
};
host.post_files_changed("closed", &root, &active, &batch);
host.post_files_changed("main", &root.join("wrong"), &active, &batch);
host.post_files_changed(
"other",
&root.join("other"),
&active,
&ide_vfs::FsBatch { modified: vec![root.join("other/a.rs")], ..Default::default() },
);
host.post_files_changed(
"main",
&root,
&active,
&ide_vfs::FsBatch {
vcs: vec![root.join(".git/HEAD")],
ignored: vec![root.join(".env")],
..Default::default()
},
);
host.flush_events();
assert_eq!(host.instances(), 0, "unrelated windows, roots, and metadata do not wake a dev plugin");
host.post_files_changed("main", &root, &active, &batch);
active.store(false, Ordering::Release);
host.flush_events();
assert_eq!(host.instances(), 0, "closing before delivery discards the batch");
host.post_files_changed("main", &root, &active, &batch);
assert!(host.queues.lock().is_empty(), "a closing watcher cannot post more events");
let reopened = Arc::new(AtomicBool::new(true));
host.post_files_changed("main", &root, &reopened, &batch);
host.flush_events();
assert!(host.wait_events(Duration::from_secs(10)));
let inv = ide_plugin::Invocation {
client: Some("main".into()),
workspace_root: Some(root.clone()),
..Default::default()
};
let data = host.run_command("files-events", "events", None, inv).unwrap().unwrap();
let received: Vec<FilesChanged> = serde_json::from_str(&data).unwrap();
assert_eq!(received, [FilesChanged { created: vec![to_ipc(&root.join("new.rs"))], ..Default::default() }]);
}
#[test]
fn queued_filter_replacement_and_pause_do_not_activate() {
use super::super::{Consent, event_paths::PathFilter};
let tmp = tempfile::tempdir().unwrap();
let root = dunce::canonicalize(tmp.path()).unwrap();
let host =
PluginHost::new(root.join("plugins"), Arc::new(|_: Option<&str>, _| {}), Default::default()).unwrap();
host.set_workspace(Some(Arc::new(Workspace(root.clone()))));
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../ide-plugin/tests/fixtures/files-events");
host.install_from_dir(&fixture, &Consent::all()).unwrap();
let active = Arc::new(AtomicBool::new(true));
let filter = |globs: Vec<String>| {
host.shared.subscriptions.lock().set_paths(
"files-events",
"files-changed",
PathFilter::new("files-changed", globs).unwrap(),
);
};
let batch = ide_vfs::FsBatch {
created: vec![root.join("config/a.yaml"), root.join("src/unrelated.rs")],
modified: vec![root.join("config/b.yaml")],
removed: vec![root.join("config/c.yaml")],
..Default::default()
};
filter(vec!["config/*.yaml".into()]);
host.post_files_changed("main", &root, &active, &batch);
assert!(!host.queues.lock().is_empty());
filter(vec!["src/**".into()]);
host.flush_events();
assert_eq!(host.instances(), 0, "replacement excludes paths already queued");
filter(vec!["config/*.yaml".into()]);
host.post_files_changed("main", &root, &active, &batch);
filter(vec![]);
host.flush_events();
assert_eq!(host.instances(), 0, "pausing suppresses a queued batch before activation");
host.post_files_changed("main", &root, &active, &batch);
assert!(host.queues.lock().is_empty(), "paused paths never queue work");
}
}
+205 -34
View File
@@ -20,6 +20,7 @@ pub mod commit;
pub(crate) mod diagnostics;
pub mod endpoints;
mod event_paths;
mod files_changed;
mod hover;
mod icon_extensions;
mod icon_patterns;
@@ -187,6 +188,7 @@ const LATENCY_WINDOW: usize = 128;
struct EventQueue {
since: Instant,
events: Vec<WorkbenchEvent>,
files_source: Option<files_changed::Source>,
}
/// Most events queued per plugin and window; older ones are dropped.
@@ -217,6 +219,39 @@ struct Batches {
pending: u32,
/// Kinds of the batch being handled right now.
running: Option<BTreeSet<EventKind>>,
files_pending: HashMap<FilesQueueKey, PendingFiles>,
running_token: Option<Arc<()>>,
}
type FilesQueueKey = (Option<String>, Option<PathBuf>);
struct PendingFiles {
queue: EventQueue,
actor: std::sync::Weak<Actor>,
token: Arc<()>,
}
/// Dropping a posted job must release its mailbox even when the actor never runs it.
struct BatchTicket {
batches: Arc<Mutex<Batches>>,
files_key: Option<FilesQueueKey>,
token: Arc<()>,
}
impl Drop for BatchTicket {
fn drop(&mut self) {
let mut batches = self.batches.lock();
if let Some(key) = &self.files_key
&& batches.files_pending.get(key).is_some_and(|p| Arc::ptr_eq(&p.token, &self.token))
{
batches.files_pending.remove(key);
}
batches.pending = batches.pending.saturating_sub(1);
if batches.running_token.as_ref().is_some_and(|token| Arc::ptr_eq(token, &self.token)) {
batches.running = None;
batches.running_token = None;
}
}
}
impl LoadedPlugin {
@@ -1461,7 +1496,7 @@ impl PluginHost {
let mut out = BTreeSet::new();
for p in self.plugins.read().values() {
for kind in EventKind::ALL {
if self.wants(p, kind, subs.kinds.get(p.id())) {
if Self::wants(p, kind, subs.kinds.get(p.id())) {
out.insert(kind);
}
}
@@ -1471,7 +1506,7 @@ impl PluginHost {
/// Whether `plugin` gets `kind` events: subscribed at run time or with
/// `on-event:<kind>`, the capability granted, and able to run.
fn wants(&self, plugin: &LoadedPlugin, kind: EventKind, runtime: Option<&BTreeSet<EventKind>>) -> bool {
fn wants(plugin: &LoadedPlugin, kind: EventKind, runtime: Option<&BTreeSet<EventKind>>) -> bool {
let declared =
plugin.manifest().activation.iter().any(|a| matches!(a, ActivationEvent::Event(k) if k == kind.name()));
if !declared && !runtime.is_some_and(|s| s.contains(&kind)) {
@@ -1485,6 +1520,15 @@ impl PluginHost {
/// Queues workbench events from window `client` for the plugins that want
/// them; [`flush_events`](Self::flush_events) delivers them in batches.
pub fn post_events(&self, client: Option<&str>, events: Vec<WorkbenchEvent>) {
self.post_events_from(client, events, None);
}
fn post_events_from(
&self,
client: Option<&str>,
events: Vec<WorkbenchEvent>,
source: Option<files_changed::Source>,
) {
if events.is_empty() {
return;
}
@@ -1492,32 +1536,60 @@ impl PluginHost {
let plugins: Vec<_> = self.plugins.read().values().cloned().collect();
let now = Instant::now();
let mut queues = self.queues.lock();
let mut events = events;
files_changed::retain_live(&mut events, source.as_ref());
for p in plugins {
let wanted: Vec<WorkbenchEvent> =
events.iter().filter(|e| self.wants(&p, e.kind(), subs.kinds.get(p.id()))).cloned().collect();
let wanted = self.filter_path_events(&p, client, &wanted, &subs);
let wanted: Vec<WorkbenchEvent> = events
.iter()
.filter(|e| Self::wants(&p, e.kind(), subs.kinds.get(p.id())))
.filter(|e| {
!matches!(e, WorkbenchEvent::FilesChanged(_))
|| p.dev_root
.as_ref()
.is_none_or(|root| client.and_then(|c| self.shared.project_of(c)).as_ref() == Some(root))
})
.cloned()
.collect();
let wanted = Self::filter_path_events(&self.shared, &p, client, &wanted, &subs);
if wanted.is_empty() {
continue;
}
let queue = queues
.entry((p.id().to_owned(), client.map(str::to_owned)))
.or_insert_with(|| EventQueue { since: now, events: Vec::new() });
queue.events.extend(wanted);
let queue = queues.entry((p.id().to_owned(), client.map(str::to_owned))).or_insert_with(|| EventQueue {
since: now,
events: Vec::new(),
files_source: None,
});
if let Some(source) = &source {
if !queue.files_source.as_ref().is_some_and(|old| old.same(source)) {
queue.events.retain(|e| !matches!(e, WorkbenchEvent::FilesChanged(_)));
}
queue.files_source = Some(source.clone());
}
files_changed::append(&mut queue.events, wanted);
let excess = queue.events.len().saturating_sub(MAX_QUEUED_EVENTS);
queue.events.drain(..excess);
let mut remaining = excess;
queue.events.retain(|event| {
if remaining > 0 && !matches!(event, WorkbenchEvent::FilesChanged(_)) {
remaining -= 1;
false
} else {
true
}
});
}
}
/// Delivers queued events: to running plugins right away, to idle ones
/// (which must be activated) after [`IDLE_BATCH`], so frequent events
/// don't keep waking them. Call it a few times a second.
/// don't keep waking them. Filesystem events are already debounced by
/// the watcher and bypass that delay. Call it a few times a second.
pub fn flush_events(&self) {
self.flush_events_at(Instant::now());
}
#[doc(hidden)]
pub fn flush_events_at(&self, now: Instant) {
let due: Vec<(QueueKey, Vec<WorkbenchEvent>)> = {
let due: Vec<(QueueKey, EventQueue)> = {
let mut queues = self.queues.lock();
let plugins = self.plugins.read();
let keys: Vec<_> = queues
@@ -1528,15 +1600,17 @@ impl PluginHost {
running
|| now.saturating_duration_since(q.since) >= IDLE_BATCH
|| q.events.len() >= MAX_QUEUED_EVENTS / 2
|| q.events.contains(&WorkbenchEvent::Scheduled)
|| q.events
.iter()
.any(|e| matches!(e, WorkbenchEvent::FilesChanged(_) | WorkbenchEvent::Scheduled))
})
.map(|(k, _)| k.clone())
.collect();
keys.into_iter().filter_map(|k| queues.remove(&k).map(|q| (k, q.events))).collect()
keys.into_iter().filter_map(|k| queues.remove(&k).map(|q| (k, q))).collect()
};
let mut changed = false;
for ((id, client), events) in due {
changed |= self.deliver(&id, client, events);
for ((id, client), queue) in due {
changed |= self.deliver(&id, client, queue);
}
// Batches that crashed since.
let plugins: Vec<_> = self.plugins.read().values().cloned().collect();
@@ -1569,22 +1643,50 @@ impl PluginHost {
}
}
fn filter_delivery_events(
shared: &api::Shared,
plugin: &LoadedPlugin,
client: Option<&str>,
events: &[WorkbenchEvent],
subs: &event_paths::Subscriptions,
) -> Vec<WorkbenchEvent> {
Self::filter_path_events(shared, plugin, client, events, subs)
.into_iter()
.filter(|event| {
!matches!(
event,
WorkbenchEvent::FileOpened(_) | WorkbenchEvent::FileSaved(_) | WorkbenchEvent::FilesChanged(_)
) || Self::wants(plugin, event.kind(), subs.kinds.get(plugin.id()))
})
.collect()
}
/// Hands one batch to the plugin's `event-handler.handle` without waiting
/// for it. A newer batch of the same kinds cooperatively cancels the one
/// being handled (a save while the previous save is still being checked).
/// Filesystem deltas must all run: a newer delta cannot replace an older one.
/// Returns whether the plugin was activated.
fn deliver(&self, id: &str, client: Option<String>, events: Vec<WorkbenchEvent>) -> bool {
fn deliver(&self, id: &str, client: Option<String>, mut queue: EventQueue) -> bool {
files_changed::retain_live(&mut queue.events, queue.files_source.as_ref());
if queue.events.is_empty() {
return false;
}
if queue.events.iter().any(|e| matches!(e, WorkbenchEvent::FilesChanged(_)))
&& queue.events.iter().any(|e| !matches!(e, WorkbenchEvent::FilesChanged(_)))
{
let (files, other) = queue.events.into_iter().partition(|e| matches!(e, WorkbenchEvent::FilesChanged(_)));
let changed =
self.deliver(id, client.clone(), EventQueue { since: queue.since, events: other, files_source: None });
return self.deliver(
id,
client,
EventQueue { since: queue.since, events: files, files_source: queue.files_source },
) | changed;
}
let Ok(plugin) = self.get(id) else { return false };
let subs = self.shared.subscriptions.lock().clone();
let events: Vec<_> = self
.filter_path_events(&plugin, client.as_deref(), &events, &subs)
.into_iter()
.filter(|event| {
!matches!(event, WorkbenchEvent::FileOpened(_) | WorkbenchEvent::FileSaved(_))
|| self.wants(&plugin, event.kind(), subs.kinds.get(id))
})
.collect();
if events.is_empty() {
let eligible = Self::filter_delivery_events(&self.shared, &plugin, client.as_deref(), &queue.events, &subs);
if eligible.is_empty() {
return false;
}
if !plugin.meta.lock().pending.is_empty() {
@@ -1597,20 +1699,92 @@ impl PluginHost {
editor: None,
selected_paths: Vec::new(),
};
let kinds: BTreeSet<EventKind> = events.iter().map(WorkbenchEvent::kind).collect();
let kinds: BTreeSet<EventKind> = queue.events.iter().map(WorkbenchEvent::kind).collect();
let filesystem = kinds.contains(&EventKind::FilesChanged);
let mailbox_key = (client.clone(), invocation.workspace_root.clone());
let mut ordinary = None;
let token = Arc::new(());
{
let mut batches = plugin.batches.lock();
if batches.running.as_ref() == Some(&kinds) && !kinds.contains(&EventKind::Scheduled) {
if batches.running.as_ref() == Some(&kinds)
&& !kinds.contains(&EventKind::Scheduled)
&& !kinds.contains(&EventKind::FilesChanged)
{
actor.cancel_soft();
}
if filesystem {
files_changed::retain_live(&mut queue.events, queue.files_source.as_ref());
if queue.events.is_empty() {
return activated;
}
if let Some(pending) = batches.files_pending.get_mut(&mailbox_key)
&& pending.actor.upgrade().is_some_and(|owner| Arc::ptr_eq(&owner, &actor))
{
let pending = &mut pending.queue;
if queue
.files_source
.as_ref()
.is_some_and(|source| !pending.files_source.as_ref().is_some_and(|old| old.same(source)))
{
pending.events.clear();
pending.files_source = queue.files_source.clone();
}
files_changed::append(&mut pending.events, queue.events);
return activated;
}
batches.files_pending.insert(
mailbox_key.clone(),
PendingFiles { queue, actor: Arc::downgrade(&actor), token: token.clone() },
);
} else {
ordinary = Some(queue);
}
batches.pending += 1;
}
let started = Instant::now();
let (start, finish) = (plugin.batches.clone(), plugin.batches.clone());
let start = plugin.batches.clone();
let ticket = BatchTicket {
batches: plugin.batches.clone(),
files_key: filesystem.then(|| mailbox_key.clone()),
token: token.clone(),
};
let done_plugin = plugin.clone();
let delivery_plugin = plugin.clone();
let shared = self.shared.clone();
actor.post(
move |rt| {
start.lock().running = Some(kinds);
let _ticket = ticket;
let mut queue = {
let mut batches = start.lock();
let queue = if filesystem {
if !batches.files_pending.get(&mailbox_key).is_some_and(|p| Arc::ptr_eq(&p.token, &token)) {
return Ok(());
}
batches.files_pending.remove(&mailbox_key).expect("owned filesystem batch").queue
} else {
ordinary.expect("ordinary event batch")
};
batches.running = Some(kinds);
batches.running_token = Some(token);
queue
};
files_changed::retain_live(&mut queue.events, queue.files_source.as_ref());
if filesystem
&& invocation.workspace_root != invocation.client.as_deref().and_then(|c| shared.project_of(c))
{
return Ok(());
}
let subs = shared.subscriptions.lock().clone();
let events = Self::filter_delivery_events(
&shared,
&delivery_plugin,
invocation.client.as_deref(),
&queue.events,
&subs,
);
if events.is_empty() {
return Ok(());
}
rt.handle_events(events, invocation)
},
move |result| {
@@ -1627,9 +1801,6 @@ impl PluginHost {
}
_ => {}
}
let mut batches = finish.lock();
batches.pending = batches.pending.saturating_sub(1);
batches.running = None;
},
);
activated
@@ -1827,7 +1998,7 @@ impl PluginHost {
self.queues
.lock()
.entry((p.id().to_owned(), None))
.or_insert_with(|| EventQueue { since: now, events: Vec::new() })
.or_insert_with(|| EventQueue { since: now, events: Vec::new(), files_source: None })
.events
.push(WorkbenchEvent::Scheduled);
}
+7
View File
@@ -20,6 +20,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
/// Everything bound to one open project folder.
pub struct Project {
pub(crate) trusted: Arc<AtomicBool>,
pub(crate) files_events_active: Arc<AtomicBool>,
pub root: PathBuf,
/// Window-scoped event sink, for commands that emit project events.
pub sink: SharedSink,
@@ -319,7 +320,10 @@ impl Host {
// One-shot latch for the post-start watch-limit warning (#1915): the
// synchronous startup walk and a later batch can both observe it.
let notify_watch_exhausted = Arc::new(AtomicBool::new(false));
let files_events_active = Arc::new(AtomicBool::new(true));
let watcher = {
let files_events_active = files_events_active.clone();
let window = window.to_owned();
let kotlin_bridge = kotlin_bridge.clone();
let phpstan = phpstan.clone();
let repos = repos.clone();
@@ -357,6 +361,7 @@ impl Host {
documents.on_disk_changed(&[&changed[..], &ignored].concat());
if let Some(plugins) = crate::plugins::editor_plugin_host() {
plugins.invalidate_endpoints(&root, &[changed.clone(), batch.removed.clone()].concat());
plugins.post_files_changed(&window, &root, &files_events_active, batch);
}
// A framework's root marker (`angular.json`) created or deleted: its
// templates gain or lose their mode. Markers in `node_modules` only
@@ -554,6 +559,7 @@ impl Host {
content_watchers: Mutex::new(content_watchers),
lsp_watchers: Mutex::new(Vec::new()),
_scratch_watcher: scratch_watcher,
files_events_active,
light: false,
});
{
@@ -703,6 +709,7 @@ impl Host {
content_watchers: Mutex::new(Vec::new()),
lsp_watchers: Mutex::new(Vec::new()),
_scratch_watcher: None,
files_events_active: Arc::new(AtomicBool::new(false)),
light: true,
root: dir,
});
+188
View File
@@ -15,6 +15,7 @@ use std::sync::Arc;
use std::time::{Duration, Instant};
type Events = Arc<Mutex<Vec<(Option<String>, PluginEvent)>>>;
type EventReadGate = (std::sync::mpsc::Sender<()>, std::sync::mpsc::Receiver<()>);
/// What plugins see of the test project: real `Documents`, as in the IDE.
struct TestWorkspace {
@@ -23,6 +24,7 @@ struct TestWorkspace {
documents: Arc<ide_workspace::Documents>,
trusted: bool,
project_queries_left: Mutex<Option<usize>>,
event_read_gate: Mutex<Option<EventReadGate>>,
/// (producer, path) → published diagnostics.
diagnostics: Mutex<std::collections::BTreeMap<(String, PathBuf), Vec<ide_core::Diagnostic>>>,
/// plugin → its settings as the user set them.
@@ -41,6 +43,7 @@ impl TestWorkspace {
documents: Arc::new(documents),
trusted: true,
project_queries_left: Mutex::new(None),
event_read_gate: Mutex::new(None),
diagnostics: Mutex::default(),
settings: Mutex::default(),
})
@@ -74,6 +77,13 @@ impl PluginWorkspace for TestWorkspace {
std::iter::once(self.root.clone()).chain(self.other_roots.lock().iter().cloned()).collect()
}
fn buffer_text(&self, path: &Path) -> Option<String> {
if path.ends_with("event-barrier.txt") {
let gate = self.event_read_gate.lock().take();
if let Some((entered, release)) = gate {
entered.send(()).unwrap();
release.recv_timeout(Duration::from_secs(10)).expect("release blocked event read");
}
}
self.documents.text_of(path)
}
fn buffer_dirty(&self, path: &Path) -> bool {
@@ -2142,6 +2152,7 @@ fn git_toolbox_does_not_fetch_untrusted_projects() {
other_roots: Mutex::default(),
documents: f.workspace.documents.clone(),
trusted: false,
event_read_gate: Mutex::default(),
diagnostics: Mutex::default(),
settings: Mutex::default(),
});
@@ -4940,3 +4951,180 @@ fn marker_providers_preserve_names_and_package_paths() {
["run", "database", "icons/very-long-resource-marker-icon.svg", "overriding"]
);
}
/// The released WIT variant stays unchanged: SDK decodes other(files-changed).
#[test]
fn files_changed_wakes_idle_once_and_keeps_every_delta() {
let f = fixture(Limits::default());
f.install(&sample("files-events"));
assert!(f.host.event_kinds().contains(&EventKind::FilesChanged));
assert_eq!(f.host.instances(), 0);
for i in 0..600 {
f.host.post_events(
Some("main"),
vec![WorkbenchEvent::FilesChanged(ide_plugin::FilesChanged {
created: vec![to_ipc(&f.project.join(format!("新建😀-{i}.rs")))],
modified: vec![to_ipc(&f.project.join("shared.rs"))],
removed: vec![to_ipc(&f.project.join("gone.rs"))],
})],
);
}
// Already debounced by the watcher; no two-second idle delay.
f.host.flush_events();
assert!(f.host.wait_events(Duration::from_secs(10)));
let batches: Vec<ide_plugin::FilesChanged> =
serde_json::from_str(&f.run("files-events", "events", None).unwrap().unwrap()).unwrap();
assert_eq!(batches.len(), 1);
assert_eq!(batches[0].created.len(), 600);
assert_eq!(batches[0].modified, [to_ipc(&f.project.join("shared.rs"))]);
assert_eq!(batches[0].removed, [to_ipc(&f.project.join("gone.rs"))]);
for i in 0..3 {
f.host.post_events(
Some("main"),
vec![WorkbenchEvent::FilesChanged(ide_plugin::FilesChanged {
modified: vec![to_ipc(&f.project.join(format!("delta-{i}.rs")))],
..Default::default()
})],
);
f.host.flush_events();
}
assert!(f.host.wait_events(Duration::from_secs(10)));
let batches: Vec<ide_plugin::FilesChanged> =
serde_json::from_str(&f.run("files-events", "events", None).unwrap().unwrap()).unwrap();
let mut later: Vec<_> = batches[1..].iter().flat_map(|b| b.modified.iter().cloned()).collect();
later.sort();
assert_eq!(
later,
(0..3).map(|i| to_ipc(&f.project.join(format!("delta-{i}.rs")))).collect::<Vec<_>>(),
"later deltas must not cancel earlier batches"
);
}
#[test]
fn files_changed_requires_events_capability() {
let f = fixture(Limits::default());
let dir = f._tmp.path().join("without-events");
copy_dir(&sample("files-events"), &dir);
let manifest = dir.join("plugin.toml");
std::fs::write(
&manifest,
std::fs::read_to_string(&manifest).unwrap().replace("[\"events\", \"storage\"]", "[\"storage\"]"),
)
.unwrap();
f.install(&dir);
assert!(!f.host.event_kinds().contains(&EventKind::FilesChanged));
f.host.post_events(Some("main"), vec![WorkbenchEvent::FilesChanged(ide_plugin::FilesChanged::default())]);
f.host.flush_events();
assert_eq!(f.host.instances(), 0, "an ungranted plugin must not wake");
}
#[test]
fn files_changed_does_not_cancel_a_batch_blocked_in_a_host_call() {
let f = fixture(Limits::default());
f.install(&sample("files-events"));
std::fs::write(f.project.join("event-barrier.txt"), "ready").unwrap();
let (entered, waiting) = std::sync::mpsc::channel();
let (release, released) = std::sync::mpsc::channel();
*f.workspace.event_read_gate.lock() = Some((entered, released));
f.host.post_events(
Some("main"),
vec![WorkbenchEvent::FilesChanged(ide_plugin::FilesChanged {
created: vec![to_ipc(&f.project.join("__block__.txt"))],
..Default::default()
})],
);
f.host.flush_events();
waiting.recv_timeout(Duration::from_secs(10)).expect("first event reaches its host call");
f.host.post_events(
Some("main"),
vec![WorkbenchEvent::FilesChanged(ide_plugin::FilesChanged {
removed: vec![to_ipc(&f.project.join("gone.txt"))],
..Default::default()
})],
);
f.host.flush_events();
release.send(()).unwrap();
assert!(f.host.wait_events(Duration::from_secs(10)));
let batches: Vec<ide_plugin::FilesChanged> =
serde_json::from_str(&f.run("files-events", "events", None).unwrap().unwrap()).unwrap();
assert_eq!(batches.len(), 2, "a later filesystem delta cannot discard an earlier one");
assert_eq!(batches[0].created, [to_ipc(&f.project.join("__block__.txt"))]);
assert_eq!(batches[1].removed, [to_ipc(&f.project.join("gone.txt"))]);
}
#[test]
fn files_changed_rechecks_filters_when_an_actor_batch_starts() {
for filter in ["", "other/**"] {
let f = fixture(Limits::default());
f.install(&sample("files-events"));
f.run("files-events", "filter-after-event", Some(filter)).unwrap();
std::fs::write(f.project.join("event-barrier.txt"), "ready").unwrap();
let (entered, waiting) = std::sync::mpsc::channel();
let (release, released) = std::sync::mpsc::channel();
*f.workspace.event_read_gate.lock() = Some((entered, released));
f.host.post_events(
Some("main"),
vec![WorkbenchEvent::FilesChanged(ide_plugin::FilesChanged {
created: vec![to_ipc(&f.project.join("__block__.txt"))],
..Default::default()
})],
);
f.host.flush_events();
waiting.recv_timeout(Duration::from_secs(10)).expect("first batch blocks before replacing its filter");
f.host.post_events(
Some("main"),
vec![WorkbenchEvent::FilesChanged(ide_plugin::FilesChanged {
modified: vec![to_ipc(&f.project.join("queued.txt"))],
..Default::default()
})],
);
f.host.flush_events();
release.send(()).unwrap();
assert!(f.host.wait_events(Duration::from_secs(10)));
let batches: Vec<ide_plugin::FilesChanged> =
serde_json::from_str(&f.run("files-events", "events", None).unwrap().unwrap()).unwrap();
assert_eq!(batches.len(), 1, "filter {filter:?} must suppress the batch waiting in the actor");
}
}
#[test]
fn files_changed_filters_each_category_before_waking_idle_plugins() {
let f = fixture(Limits { idle_timeout: Some(Duration::ZERO), ..Limits::default() });
f.install(&sample("files-events"));
f.run("files-events", "filter", Some("config/*.yaml")).unwrap();
assert!(eventually(|| {
f.host.tick();
f.host.instances() == 0
}));
let path = |relative: &str| to_ipc(&f.project.join(relative));
f.host.post_events(
Some("main"),
vec![WorkbenchEvent::FilesChanged(ide_plugin::FilesChanged {
created: vec![path("src/unrelated.rs")],
modified: vec![path("notes.txt")],
removed: vec![path("elsewhere/a.yaml")],
})],
);
f.host.flush_events();
assert_eq!(f.host.instances(), 0, "unmatched batches do not wake an idle plugin");
f.host.post_events(
Some("main"),
vec![WorkbenchEvent::FilesChanged(ide_plugin::FilesChanged {
created: vec![path("config/new.yaml"), path("src/unrelated.rs")],
modified: vec![path("config/changed.yaml"), path("notes.txt")],
removed: vec![path("config/gone.yaml"), path("elsewhere/a.yaml")],
})],
);
f.host.flush_events();
assert!(f.host.wait_events(Duration::from_secs(10)));
let received: Vec<ide_plugin::FilesChanged> =
serde_json::from_str(&f.run("files-events", "events", None).unwrap().unwrap()).unwrap();
assert_eq!(
received,
[ide_plugin::FilesChanged {
created: vec![path("config/new.yaml")],
modified: vec![path("config/changed.yaml")],
removed: vec![path("config/gone.yaml")],
}]
);
}
+5 -1
View File
@@ -193,7 +193,7 @@ pub trait HostApi: Send + Sync {
) -> Result<ProcessOutput, HostError>;
/// Delivers every event of `kind`, dropping its path filter.
fn subscribe(&self, kind: EventKind);
/// Delivers `kind` (`file-opened`, `file-saved`) only for paths matching `globs`.
/// Delivers `kind` (`file-opened`, `file-saved`, `files-changed`) only for matching paths.
fn subscribe_paths(&self, kind: &str, globs: Vec<String>) -> Result<(), HostError>;
fn unsubscribe(&self, kind: &str);
/// Starts `cmd` and returns the handle to it without waiting.
@@ -652,6 +652,10 @@ fn event_to_wit(e: WorkbenchEvent) -> wit_events::Event {
match e {
WorkbenchEvent::FileOpened(p) => wit_events::Event::FileOpened(p),
WorkbenchEvent::FileSaved(p) => wit_events::Event::FileSaved(p),
WorkbenchEvent::FilesChanged(batch) => wit_events::Event::Other(wit_types::Extension {
kind: "files-changed".into(),
data: serde_json::to_string(&batch).expect("file paths serialize"),
}),
WorkbenchEvent::EditorFocused(p) => wit_events::Event::EditorFocused(p),
WorkbenchEvent::VcsChanged(p) => wit_events::Event::VcsChanged(p),
WorkbenchEvent::Scheduled => wit_events::Event::Scheduled,
+14 -1
View File
@@ -98,6 +98,7 @@ pub enum Prompt {
pub enum EventKind {
FileOpened,
FileSaved,
FilesChanged,
EditorFocused,
ActionPerformed,
VcsChanged,
@@ -105,9 +106,10 @@ pub enum EventKind {
}
impl EventKind {
pub const ALL: [Self; 6] = [
pub const ALL: [Self; 7] = [
Self::FileOpened,
Self::FileSaved,
Self::FilesChanged,
Self::EditorFocused,
Self::ActionPerformed,
Self::VcsChanged,
@@ -118,6 +120,7 @@ impl EventKind {
match self {
Self::FileOpened => "file-opened",
Self::FileSaved => "file-saved",
Self::FilesChanged => "files-changed",
Self::EditorFocused => "editor-focused",
Self::ActionPerformed => "action-performed",
Self::VcsChanged => "vcs-changed",
@@ -175,12 +178,21 @@ pub struct ActionPerformed {
pub shortcut: Option<String>,
}
/// Debounced worktree changes. Paths use the same absolute form as `file-saved`.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)]
pub struct FilesChanged {
pub created: Vec<String>,
pub modified: Vec<String>,
pub removed: Vec<String>,
}
/// A workbench event delivered to plugins.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase", tag = "kind", content = "value")]
pub enum WorkbenchEvent {
FileOpened(String),
FileSaved(String),
FilesChanged(FilesChanged),
EditorFocused(Option<String>),
ActionPerformed(ActionPerformed),
VcsChanged(String),
@@ -192,6 +204,7 @@ impl WorkbenchEvent {
match self {
Self::FileOpened(_) => EventKind::FileOpened,
Self::FileSaved(_) => EventKind::FileSaved,
Self::FilesChanged(_) => EventKind::FilesChanged,
Self::EditorFocused(_) => EventKind::EditorFocused,
Self::ActionPerformed(_) => EventKind::ActionPerformed,
Self::VcsChanged(_) => EventKind::VcsChanged,
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
@@ -1 +1 @@
b243e04f0c4e95d66cfa588c805b504b9dc0cfb80c699f4304b888e298812fb9
d9cf3708e558ce4931376b5f8c5b3d9d0771d65100ba2ac2c9d1a5a5cf13e62f
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
@@ -1 +1 @@
1079919eeaf79cff09ecba0e5be922dbb1c2989b5f44216c349cedf634c83939
606d02b53a0f51e41b5accb209750d2c768adf56190fe667caa2f90173e84d91
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
@@ -1 +1 @@
3be9380d804cf5446d80ff43d00442c64de80ff7d66560d0c78ff3eee7602531
2bf7c821d304b184c4b02540d170b7ec06e6a40b62436f61bfb97836e39dabab
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
@@ -1 +1 @@
9b1e49d2375cccada5952bc881b78fb56740f3f391950ce4a7fa661965e49dce
296e84611d0b89399944c98972f8c98378fce49368a16944646f1ed8c1d172a1
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
@@ -1 +1 @@
e357dc8ec3348b94ab8761b682290e8351c29dc44a96a88032c62e6b7cc1f84a
5da8fd5058502bf5a4f7a96352e3337cb7fef0d8478ebab335adc7959c027d8b
+337
View File
@@ -0,0 +1,337 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anyhow"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "bitflags"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "equivalent"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
dependencies = [
"foldhash",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
"serde",
"serde_core",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "log"
version = "0.4.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c"
[[package]]
name = "macro-string"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
[[package]]
name = "openidea-plugin-files-events"
version = "0.1.0"
dependencies = [
"openidea-plugin",
"serde_json",
]
[[package]]
name = "prettyplease"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "syn"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
[[package]]
name = "wasm-encoder"
version = "0.262.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4495a6f85dd94c411c749303efcbc5defade0b0cbdbc0541495241ab917a9b5a"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.262.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c86ff6668190d9fb70372b306b4f9fe8930f0e2f57ce72e4922bf2f117900949"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.262.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "974d643b125b272607e2fff8012ea14a8328acd7621e4bafa7afb32500052b1b"
dependencies = [
"bitflags",
"hashbrown",
"indexmap",
"semver",
]
[[package]]
name = "wit-bindgen"
version = "0.63.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b85a987ab61d365382d25c7d47e26edd48941b3bf6e3fbf76eff918bc71dab06"
dependencies = [
"bitflags",
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen-core"
version = "0.63.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "775e122e390603abfacd548027016cd0113a6e2bf2f8ce6bdd3658b013456ea8"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.63.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84e26e13b9d972026778d2ffb7985736de116026fdc8d9464f966e12df2eb240"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.63.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16a008a0d6b4840677326beb4edbeb210a2f1b7d0183fb7cc33f466b90fd7fb6"
dependencies = [
"anyhow",
"macro-string",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.262.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "078def49fdb49cab405c3006e05793b3fdaf7069126ba6b9c97d01ec8c98fe4c"
dependencies = [
"anyhow",
"bitflags",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.262.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00f0dc051466ffe46787721fca3d088536f158ae400be044f1b1c386ba9397ad"
dependencies = [
"anyhow",
"hashbrown",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-ident",
"wasmparser",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
@@ -0,0 +1,21 @@
[package]
name = "openidea-plugin-files-events"
version = "0.1.0"
edition = "2024"
publish = false
[workspace]
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
openidea-plugin = { path = "../../../../../sdk/rust/openidea-plugin" }
serde_json = "1"
[profile.release]
opt-level = "s"
lto = true
strip = true
codegen-units = 1
panic = "abort"
Binary file not shown.
@@ -0,0 +1 @@
dc42bf8cb4af2720ce83f4a5dd1bffdc6315ae7f041b84a2500ee9f735c73677
@@ -0,0 +1,26 @@
id = "files-events"
name = "Filesystem events test"
version = "0.1.0"
engines.openidea = "^1.3"
wasm = "dist/plugin.wasm"
activation = ["on-command:*", "on-event:files-changed"]
[capabilities]
required = ["events", "storage"]
optional = ["project.read"]
[[contributes.commands]]
id = "events"
title = "Read filesystem events"
[[contributes.commands]]
id = "filter"
title = "Filter filesystem events"
[[contributes.commands]]
id = "filter-after-event"
title = "Change the filter after the next event"
[[contributes.commands]]
id = "unsubscribe"
title = "Unsubscribe from filesystem events"
@@ -0,0 +1,49 @@
use openidea_plugin::{Error, Event, FilesChanged, Invocation, Plugin, host, register_plugin};
#[derive(Default)]
struct FilesEvents;
impl Plugin for FilesEvents {
fn run_command(&mut self, id: &str, _ctx: &Invocation, arg: Option<&str>) -> Result<Option<String>, Error> {
match id {
"events" => host::storage::get("events"),
"filter" => {
host::subscribe_paths("files-changed", &arg.unwrap_or_default().lines().collect::<Vec<_>>())?;
Ok(None)
}
"filter-after-event" => {
host::storage::set("next-filter", arg.unwrap_or_default())?;
Ok(None)
}
"unsubscribe" => {
host::unsubscribe(openidea_plugin::EventKind::FilesChanged);
Ok(None)
}
_ => Err(Error::NotFound(id.into())),
}
}
fn handle_events(&mut self, events: &[Event], ctx: &Invocation) {
let mut received: Vec<FilesChanged> =
host::storage::get("events").unwrap().map(|s| serde_json::from_str(&s).unwrap()).unwrap_or_default();
for event in events {
if let Event::FilesChanged(batch) = event {
if batch.created.iter().any(|p| p.ends_with("/__block__.txt")) {
let task = host::begin_task("Filesystem delta", false);
ctx.project().unwrap().read_file("event-barrier.txt").unwrap();
if task.is_cancelled() {
return;
}
}
received.push(batch.clone());
if let Some(filter) = host::storage::get("next-filter").unwrap() {
host::storage::delete("next-filter").unwrap();
host::subscribe_paths("files-changed", &filter.lines().collect::<Vec<_>>()).unwrap();
}
}
}
host::storage::set("events", &serde_json::to_string(&received).unwrap()).unwrap();
}
}
register_plugin!(FilesEvents);
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
@@ -1 +1 @@
2d217bbc64084a567bb9ab3b3211707bb3910775a0ff83e999224450b3147f16
f1cb247d29f80c7313432cd660d0c26d50691a64a3d0cae0bc6b3ba09915dbe7
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
@@ -1 +1 @@
d17719e25a37452fff08809ba1d8d347db9b753a592f54874cc94f1f60b647ed
ef09a1ce4ef3a5a28aee01861c7de00bbac568175e880b7c87222a1a0aee3da4
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
@@ -1 +1 @@
e347601e9dcf2024eed831fdb1a97a4b804b1422cfb05cda73f8057cf76125e0
35647c498dc439bcf5a135d5c21223150977f89511c7bbef652997c95ebc0f59
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
@@ -1 +1 @@
6e6411816e1a398718d443318a688c874f724a874d7ccf151c12d9680b6354d2
190f1d506b854c3dc0ade563b8cda2488767522d4fd653427513ea9d1d83c989
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
@@ -1 +1 @@
2b5b1ba2fdd256a01dadecd70f402512d069a5650b39c015835424617ee9dad3
a1361137766e4912126cfc5f6eed3942f455bc9b31e4fbce658df613130ccba3
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
@@ -1 +1 @@
b191c05468f235a6fae36cef79a1cd81ab2c5cc75972fc79dc2ec96be6b1430a
791a13024ba10672d4e93d759d232443abc5e2cf9f97cacfe3d1ce93b0d4eeed
Binary file not shown.
@@ -1 +1 @@
9ca3febe3a08e7f747112a3639ee594c85008258b5474cc906d93af2d682aa91
7976a4b3f67e536a24bbdbc9fd6cf23a102669a8aa039c02f9d956329300ff36
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
@@ -1 +1 @@
2e86bfc80245ba988e0377b3e9f3e94ce68d01d34a188ffdace5a9ec91bebaa4
9fce102b05a2d11a09b632a9e20185b994a8ab71ca86969754aedef10a4d4985
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
@@ -1 +1 @@
1fe522ba55691c65026bca5a2a25ba298ee55f4800b4911a0ef38e3e9cb957c3
3c9cd0d3c519dccf414268cafe5342374009341b38c0ab061e72a9b27976e8c7
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
@@ -1 +1 @@
175611b829f280adf44c7a4f2d544dc78c9111bd9cb3cb4842a89e456f503249
1c09ba923ca3b59df4144095c6aba6c03981fc2dc5d208bda3ba0cfa4c4cde51
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
@@ -1 +1 @@
3f52ba4bcb2615ad4fe61fae48ac88e6e06c7f0713cf8b41283d12efb614f8fd
bddfc4fff3e25c611525ebfdee3fb7ab3e4890c394c3970da78f80a12a7a645d
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
@@ -1 +1 @@
40c382c554daa12d2fb83c3be98064ed7fc9122e435bd331ea6bf382f4d399dd
5e1afb744ed0a230648d3dd9c2a9cee8cf7fc53db2f944a1ac580799b7e9ef96
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
@@ -1 +1 @@
8039afdedc540c2d3278813b526eaf3d1e6a8a4b8f3bab498323837e59989cbc
6daecc5ace3661fac8b158d3bde0ecb13b366d8918e28aa846f10858c5c57442
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
@@ -1 +1 @@
6d63e1eb1bb8b2c0adb41f33f5164b57ddd8860b0a8a712c046963500d48b25f
cb791f264a947897336ada01f56f81e7bade9a046c701908dfff9852e0cdf15d
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
@@ -1 +1 @@
52879d53687affe3aa1ac2ef1f18bb673ca2f02615840a223c8250bb7b0a636d
33adfc97d25a9777d3215488dd96dc41d772ed0c71621fb5c78e3c766031d6ae
+19 -2
View File
@@ -502,6 +502,7 @@ from another feature, or nothing when run from Search Everywhere / a shortcut. `
| Kind | Capability | Delivered |
| ------------------ | -------------- | ------------------------------------------------------------------------- |
| `file-opened` | `events` | a file opens in an editor |
| `files-changed` | `events` | debounced created, modified and removed paths in open projects (API 1.3) |
| `file-saved` | `events` | after the file is written |
| `editor-focused` | `events` | the focused editor changes (path, or none) |
| `vcs-changed` | `events` | a repository's state changed (commit, checkout, fetch…; its root) |
@@ -510,7 +511,21 @@ from another feature, or nothing when run from Search Everywhere / a shortcut. `
Events are batched: a running plugin gets them within a fraction of a second,
an idle one collects them for 2 s before it is woken, so frequent events
(Key Promoter's actions) don't keep it alive.
(Key Promoter's actions) don't keep it alive. `files-changed` is already debounced
by the project's watcher (100 ms quiet period, at most 1 s during a continuous
burst), so it bypasses the idle delay. Queued filesystem batches coalesce paths;
a later batch does not cancel an earlier one. Closed project sessions discard
pending filesystem events. Excluded and git-ignored paths follow the normal
project watcher rules; scratch files and language-server dependency watches do
not produce this event.
The released WIT event remains unchanged: `event::other` carries kind
`files-changed` and JSON `{ "created": [...], "modified": [...], "removed": [...] }`.
The Rust SDK exposes `Event::FilesChanged(FilesChanged)` and
`EventKind::FilesChanged`. Paths have the same absolute form as `file-saved`.
Subscribe with `host::subscribe(EventKind::FilesChanged)` or
`activation = ["on-event:files-changed"]`. Plugins using this event must declare
`engines.openidea = "^1.3"`.
Kinds are names (`subscribe` takes a string), so later IDEs add kinds without
a new API: an IDE refuses a kind it doesn't know with `invalid`, and a kind
@@ -519,7 +534,9 @@ The SDK's event enums are `#[non_exhaustive]`; match them with a `_` arm.
API 1.3 adds `host::subscribe_paths("file-opened", &["config/*.yaml"])`
(`events.subscribe-paths`). The host matches project-relative globs before
queueing an event or waking an idle plugin. `file-saved` works the same way.
queueing an event or waking an idle plugin. `file-saved` and `files-changed` work
the same way. For `files-changed`, each category is filtered and an empty batch
is omitted; replacing or pausing a filter also affects batches already queued.
Patterns use `/`, `*` stays within a directory, and `**` crosses
directories. Up to 64 patterns of 1024 bytes each are accepted; absolute paths,
parent traversal and malformed patterns fail without replacing the old filter.
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -150,6 +152,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
710e95defa1995356f259b1fd69c7df8cfe1d0c1c55b04df0f0d7173b7cb61b9
978b1af84eae4b6ac381e1991241a11d6a416f3f45856bc0ad985ed59435b67d
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
145b164b99c18b6e9d0d0c2704fff0fa878fc56761d5cb76669a2a0cc9ddb3aa
5125e2e47c9adf8043d82fb60b9810fd3edcef272ff4a6c41b2450e19617d8cc
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
897d2b653cb5b8a91ef806307efa6fefd773c0aaacc746bf87fd7d2bea4bf147
4fc2c8ca94ee5e45999c68285a124f535cde6f612aac319f04526acceb639339
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
9724a33ae85205245ec0bb94369d55e2df08aa328e872af2c03ad45c230d7865
8cd633605b641e53cb27b61ce7648ab161671a334e6c5fcaba4a7c27c332612b
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -150,6 +152,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
d5245ffd43097795062f7bb887162c13d99d48b68f7883e4ff6adb41a663d157
48525c9df30fce0a443dadbfb85dd6f69e56dadbe9c529cfe4274ef24ac19a3b
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
2fbf40065eae7c9ef0e927dfae4fde1f698858b2ae3047db9c242a12e443d823
de4355361ad2f4b0950669ca41b17752bc7b67fc24a5eb40a2ffc7e75fe62441
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
7b5480ba83b888a43bb641f95ad679d60b132548516b92d91d8d7e118220f2c3
cba6d696b5984ba62619982042e496c39afcc2e97d8093872a9c7e0522d30b8c
+3
View File
@@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
@@ -149,6 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
6762b2c436f8dd0a586759cbf0b2cc316560f581619fc32efb717f96505f720b
a735b01070abd15165d221f994d712e199a4468f50c637d5c704d13639f2643a
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff. Show more