diff --git a/crates/ide-extension/src/capability.rs b/crates/ide-extension/src/capability.rs index e47a60455..9b4848611 100644 --- a/crates/ide-extension/src/capability.rs +++ b/crates/ide-extension/src/capability.rs @@ -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() diff --git a/crates/ide-extension/src/manifest.rs b/crates/ide-extension/src/manifest.rs index c6403d704..4f07c6e7c 100644 --- a/crates/ide-extension/src/manifest.rs +++ b/crates/ide-extension/src/manifest.rs @@ -144,7 +144,8 @@ impl ActivationEvent { /// Workbench event kinds (`on-event:`, 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"] { diff --git a/crates/ide-host/src/lib.rs b/crates/ide-host/src/lib.rs index 6d3f939b7..3911b2d18 100644 --- a/crates/ide-host/src/lib.rs +++ b/crates/ide-host/src/lib.rs @@ -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 { diff --git a/crates/ide-host/src/plugins/event_paths.rs b/crates/ide-host/src/plugins/event_paths.rs index b8ff5eb54..cf2d38fbe 100644 --- a/crates/ide-host/src/plugins/event_paths.rs +++ b/crates/ide-host/src/plugins/event_paths.rs @@ -21,8 +21,10 @@ pub(super) struct PathFilter { impl PathFilter { pub fn new(kind: &str, globs: Vec) -> Result { - 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 { 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::>() + }; + 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] diff --git a/crates/ide-host/src/plugins/files_changed.rs b/crates/ide-host/src/plugins/files_changed.rs new file mode 100644 index 000000000..15d8e41d3 --- /dev/null +++ b/crates/ide-host/src/plugins/files_changed.rs @@ -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, +} + +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, 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, incoming: Vec) { + 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, + 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::>(); + 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 = 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 = 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 = 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(©, &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(©, &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 = 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 { + vec![self.0.clone(), self.0.join("other")] + } + fn buffer_text(&self, _: &Path) -> Option { + None + } + fn document(&self, _: &Path) -> Option<(u64, String)> { + None + } + fn apply_edits(&self, _: &Path, _: u64, _: &[ide_core::TextEdit]) -> Result { + unreachable!() + } + fn trusted(&self, _: &Path) -> bool { + true + } + fn project_of(&self, window: &str) -> Option { + match window { + "main" => Some(self.0.clone()), + "other" => Some(self.0.join("other")), + _ => None, + } + } + fn publish_diagnostics( + &self, + _: &str, + _: &Path, + _: Vec, + ) -> 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 = 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| { + 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"); + } +} diff --git a/crates/ide-host/src/plugins/mod.rs b/crates/ide-host/src/plugins/mod.rs index 1326eb716..bd9177e26 100644 --- a/crates/ide-host/src/plugins/mod.rs +++ b/crates/ide-host/src/plugins/mod.rs @@ -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, + files_source: Option, } /// 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>, + files_pending: HashMap, + running_token: Option>, +} + +type FilesQueueKey = (Option, Option); + +struct PendingFiles { + queue: EventQueue, + actor: std::sync::Weak, + token: Arc<()>, +} + +/// Dropping a posted job must release its mailbox even when the actor never runs it. +struct BatchTicket { + batches: Arc>, + files_key: Option, + 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:`, the capability granted, and able to run. - fn wants(&self, plugin: &LoadedPlugin, kind: EventKind, runtime: Option<&BTreeSet>) -> bool { + fn wants(plugin: &LoadedPlugin, kind: EventKind, runtime: Option<&BTreeSet>) -> 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) { + self.post_events_from(client, events, None); + } + + fn post_events_from( + &self, + client: Option<&str>, + events: Vec, + source: Option, + ) { 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 = - 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 = 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)> = { + 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 { + 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, events: Vec) -> bool { + fn deliver(&self, id: &str, client: Option, 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 = events.iter().map(WorkbenchEvent::kind).collect(); + let kinds: BTreeSet = 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); } diff --git a/crates/ide-host/src/project.rs b/crates/ide-host/src/project.rs index f99b4ff5e..ebcfcb2ac 100644 --- a/crates/ide-host/src/project.rs +++ b/crates/ide-host/src/project.rs @@ -20,6 +20,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; /// Everything bound to one open project folder. pub struct Project { pub(crate) trusted: Arc, + pub(crate) files_events_active: Arc, 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, }); diff --git a/crates/ide-host/tests/plugins.rs b/crates/ide-host/tests/plugins.rs index fab352c4f..011d611cb 100644 --- a/crates/ide-host/tests/plugins.rs +++ b/crates/ide-host/tests/plugins.rs @@ -15,6 +15,7 @@ use std::sync::Arc; use std::time::{Duration, Instant}; type Events = Arc, 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, trusted: bool, project_queries_left: Mutex>, + event_read_gate: Mutex>, /// (producer, path) → published diagnostics. diagnostics: Mutex>>, /// 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 { + 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 = + 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 = + 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::>(), + "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 = + 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 = + 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 = + 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")], + }] + ); +} diff --git a/crates/ide-plugin/src/runtime.rs b/crates/ide-plugin/src/runtime.rs index 42491954b..fab0b8ec5 100644 --- a/crates/ide-plugin/src/runtime.rs +++ b/crates/ide-plugin/src/runtime.rs @@ -193,7 +193,7 @@ pub trait HostApi: Send + Sync { ) -> Result; /// 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) -> 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, diff --git a/crates/ide-plugin/src/types.rs b/crates/ide-plugin/src/types.rs index e54b56b0e..23e010aeb 100644 --- a/crates/ide-plugin/src/types.rs +++ b/crates/ide-plugin/src/types.rs @@ -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, } +/// 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, + pub modified: Vec, + pub removed: Vec, +} + /// 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), 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, diff --git a/crates/ide-plugin/tests/fixtures/commit/Cargo.lock b/crates/ide-plugin/tests/fixtures/commit/Cargo.lock index a8964abce..94e6337ba 100644 --- a/crates/ide-plugin/tests/fixtures/commit/Cargo.lock +++ b/crates/ide-plugin/tests/fixtures/commit/Cargo.lock @@ -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]] diff --git a/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm index 55b9cf546..a7c8ddc06 100644 Binary files a/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources index 97c654186..9f3e84ead 100644 --- a/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources @@ -1 +1 @@ -b243e04f0c4e95d66cfa588c805b504b9dc0cfb80c699f4304b888e298812fb9 +d9cf3708e558ce4931376b5f8c5b3d9d0771d65100ba2ac2c9d1a5a5cf13e62f diff --git a/crates/ide-plugin/tests/fixtures/detail-views/Cargo.lock b/crates/ide-plugin/tests/fixtures/detail-views/Cargo.lock index a4edc6e9e..970a14e7d 100644 --- a/crates/ide-plugin/tests/fixtures/detail-views/Cargo.lock +++ b/crates/ide-plugin/tests/fixtures/detail-views/Cargo.lock @@ -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]] diff --git a/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm index 152ec98f0..f8268720e 100644 Binary files a/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm.sources index adb199892..d4da40c7e 100644 --- a/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm.sources @@ -1 +1 @@ -1079919eeaf79cff09ecba0e5be922dbb1c2989b5f44216c349cedf634c83939 +606d02b53a0f51e41b5accb209750d2c768adf56190fe667caa2f90173e84d91 diff --git a/crates/ide-plugin/tests/fixtures/diagnostics/Cargo.lock b/crates/ide-plugin/tests/fixtures/diagnostics/Cargo.lock index 0e18d38a4..388a0348d 100644 --- a/crates/ide-plugin/tests/fixtures/diagnostics/Cargo.lock +++ b/crates/ide-plugin/tests/fixtures/diagnostics/Cargo.lock @@ -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]] diff --git a/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm index f4a32700b..03c32bcdc 100644 Binary files a/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm.sources index 0b2110f8d..b6318df69 100644 --- a/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm.sources @@ -1 +1 @@ -3be9380d804cf5446d80ff43d00442c64de80ff7d66560d0c78ff3eee7602531 +2bf7c821d304b184c4b02540d170b7ec06e6a40b62436f61bfb97836e39dabab diff --git a/crates/ide-plugin/tests/fixtures/endpoints/Cargo.lock b/crates/ide-plugin/tests/fixtures/endpoints/Cargo.lock index fb5e979aa..16e32c97a 100644 --- a/crates/ide-plugin/tests/fixtures/endpoints/Cargo.lock +++ b/crates/ide-plugin/tests/fixtures/endpoints/Cargo.lock @@ -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]] diff --git a/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm index 074636187..be65ec9f2 100644 Binary files a/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources index edc4bceb5..7bd1857ce 100644 --- a/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources @@ -1 +1 @@ -9b1e49d2375cccada5952bc881b78fb56740f3f391950ce4a7fa661965e49dce +296e84611d0b89399944c98972f8c98378fce49368a16944646f1ed8c1d172a1 diff --git a/crates/ide-plugin/tests/fixtures/file-templates/Cargo.lock b/crates/ide-plugin/tests/fixtures/file-templates/Cargo.lock index abf437ba4..71eddfb27 100644 --- a/crates/ide-plugin/tests/fixtures/file-templates/Cargo.lock +++ b/crates/ide-plugin/tests/fixtures/file-templates/Cargo.lock @@ -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]] diff --git a/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm index b99fb74c0..829cae47f 100644 Binary files a/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources index f4e5acd68..36a2035e4 100644 --- a/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources @@ -1 +1 @@ -e357dc8ec3348b94ab8761b682290e8351c29dc44a96a88032c62e6b7cc1f84a +5da8fd5058502bf5a4f7a96352e3337cb7fef0d8478ebab335adc7959c027d8b diff --git a/crates/ide-plugin/tests/fixtures/files-events/Cargo.lock b/crates/ide-plugin/tests/fixtures/files-events/Cargo.lock new file mode 100644 index 000000000..11a90fc33 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/files-events/Cargo.lock @@ -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" diff --git a/crates/ide-plugin/tests/fixtures/files-events/Cargo.toml b/crates/ide-plugin/tests/fixtures/files-events/Cargo.toml new file mode 100644 index 000000000..20af97981 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/files-events/Cargo.toml @@ -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" diff --git a/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm new file mode 100644 index 000000000..17f45f8ac Binary files /dev/null and b/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm.sources new file mode 100644 index 000000000..95d07ccf8 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm.sources @@ -0,0 +1 @@ +dc42bf8cb4af2720ce83f4a5dd1bffdc6315ae7f041b84a2500ee9f735c73677 diff --git a/crates/ide-plugin/tests/fixtures/files-events/plugin.toml b/crates/ide-plugin/tests/fixtures/files-events/plugin.toml new file mode 100644 index 000000000..c3a578c4e --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/files-events/plugin.toml @@ -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" diff --git a/crates/ide-plugin/tests/fixtures/files-events/src/lib.rs b/crates/ide-plugin/tests/fixtures/files-events/src/lib.rs new file mode 100644 index 000000000..25bfad3ac --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/files-events/src/lib.rs @@ -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, Error> { + match id { + "events" => host::storage::get("events"), + "filter" => { + host::subscribe_paths("files-changed", &arg.unwrap_or_default().lines().collect::>())?; + 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 = + 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::>()).unwrap(); + } + } + } + host::storage::set("events", &serde_json::to_string(&received).unwrap()).unwrap(); + } +} + +register_plugin!(FilesEvents); diff --git a/crates/ide-plugin/tests/fixtures/grouped-views/Cargo.lock b/crates/ide-plugin/tests/fixtures/grouped-views/Cargo.lock index bff656dfd..f29b5d75e 100644 --- a/crates/ide-plugin/tests/fixtures/grouped-views/Cargo.lock +++ b/crates/ide-plugin/tests/fixtures/grouped-views/Cargo.lock @@ -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]] diff --git a/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm index 4641c77cb..7649318f5 100644 Binary files a/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources index 5014ca22a..b36f72784 100644 --- a/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources @@ -1 +1 @@ -2d217bbc64084a567bb9ab3b3211707bb3910775a0ff83e999224450b3147f16 +f1cb247d29f80c7313432cd660d0c26d50691a64a3d0cae0bc6b3ba09915dbe7 diff --git a/crates/ide-plugin/tests/fixtures/hello/Cargo.lock b/crates/ide-plugin/tests/fixtures/hello/Cargo.lock index 2ba582e73..aa382e471 100644 --- a/crates/ide-plugin/tests/fixtures/hello/Cargo.lock +++ b/crates/ide-plugin/tests/fixtures/hello/Cargo.lock @@ -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]] diff --git a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm index 2d95bd98a..636e5de40 100644 Binary files a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources index 547e1387a..1caacbafa 100644 --- a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources @@ -1 +1 @@ -d17719e25a37452fff08809ba1d8d347db9b753a592f54874cc94f1f60b647ed +ef09a1ce4ef3a5a28aee01861c7de00bbac568175e880b7c87222a1a0aee3da4 diff --git a/crates/ide-plugin/tests/fixtures/hover/Cargo.lock b/crates/ide-plugin/tests/fixtures/hover/Cargo.lock index 244bd084f..15b9b34a2 100644 --- a/crates/ide-plugin/tests/fixtures/hover/Cargo.lock +++ b/crates/ide-plugin/tests/fixtures/hover/Cargo.lock @@ -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]] diff --git a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm index ebb48a2b5..0bdc1e61b 100644 Binary files a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources index 258ce390d..b9885bce2 100644 --- a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources @@ -1 +1 @@ -e347601e9dcf2024eed831fdb1a97a4b804b1422cfb05cda73f8057cf76125e0 +35647c498dc439bcf5a135d5c21223150977f89511c7bbef652997c95ebc0f59 diff --git a/crates/ide-plugin/tests/fixtures/indexer/Cargo.lock b/crates/ide-plugin/tests/fixtures/indexer/Cargo.lock index ce35c853c..94ffa3178 100644 --- a/crates/ide-plugin/tests/fixtures/indexer/Cargo.lock +++ b/crates/ide-plugin/tests/fixtures/indexer/Cargo.lock @@ -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]] diff --git a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm index be22447b6..432b5deb4 100644 Binary files a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources index 5ed332e97..760639cde 100644 --- a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources @@ -1 +1 @@ -6e6411816e1a398718d443318a688c874f724a874d7ccf151c12d9680b6354d2 +190f1d506b854c3dc0ade563b8cda2488767522d4fd653427513ea9d1d83c989 diff --git a/crates/ide-plugin/tests/fixtures/inlay-hints/Cargo.lock b/crates/ide-plugin/tests/fixtures/inlay-hints/Cargo.lock index 7e9f07917..5287633e0 100644 --- a/crates/ide-plugin/tests/fixtures/inlay-hints/Cargo.lock +++ b/crates/ide-plugin/tests/fixtures/inlay-hints/Cargo.lock @@ -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]] diff --git a/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm index 0a5ff06f7..d54dc1fe3 100644 Binary files a/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources index 25c98a7bb..ef7f4f503 100644 --- a/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources @@ -1 +1 @@ -2b5b1ba2fdd256a01dadecd70f402512d069a5650b39c015835424617ee9dad3 +a1361137766e4912126cfc5f6eed3942f455bc9b31e4fbce658df613130ccba3 diff --git a/crates/ide-plugin/tests/fixtures/jobs/Cargo.lock b/crates/ide-plugin/tests/fixtures/jobs/Cargo.lock index 2c35dac73..5d07150cb 100644 --- a/crates/ide-plugin/tests/fixtures/jobs/Cargo.lock +++ b/crates/ide-plugin/tests/fixtures/jobs/Cargo.lock @@ -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]] diff --git a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm index ac2956a08..55a43d18a 100644 Binary files a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources index 08ef8eeab..cfa73e88e 100644 --- a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources @@ -1 +1 @@ -b191c05468f235a6fae36cef79a1cd81ab2c5cc75972fc79dc2ec96be6b1430a +791a13024ba10672d4e93d759d232443abc5e2cf9f97cacfe3d1ce93b0d4eeed diff --git a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm index d6f0415e4..d8bcadbb8 100644 Binary files a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources index 3fcc9418c..61734164a 100644 --- a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources @@ -1 +1 @@ -9ca3febe3a08e7f747112a3639ee594c85008258b5474cc906d93af2d682aa91 +7976a4b3f67e536a24bbdbc9fd6cf23a102669a8aa039c02f9d956329300ff36 diff --git a/crates/ide-plugin/tests/fixtures/path-events/Cargo.lock b/crates/ide-plugin/tests/fixtures/path-events/Cargo.lock index 9295140e5..de9955855 100644 --- a/crates/ide-plugin/tests/fixtures/path-events/Cargo.lock +++ b/crates/ide-plugin/tests/fixtures/path-events/Cargo.lock @@ -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]] diff --git a/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm index cc5032eda..ffcb1929c 100644 Binary files a/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm.sources index 29fcbddaa..b4d3323e4 100644 --- a/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm.sources @@ -1 +1 @@ -2e86bfc80245ba988e0377b3e9f3e94ce68d01d34a188ffdace5a9ec91bebaa4 +9fce102b05a2d11a09b632a9e20185b994a8ab71ca86969754aedef10a4d4985 diff --git a/crates/ide-plugin/tests/fixtures/private-dir/Cargo.lock b/crates/ide-plugin/tests/fixtures/private-dir/Cargo.lock index 2a17ed442..86e2d1e64 100644 --- a/crates/ide-plugin/tests/fixtures/private-dir/Cargo.lock +++ b/crates/ide-plugin/tests/fixtures/private-dir/Cargo.lock @@ -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]] diff --git a/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm index 9facf5798..1d9877a7c 100644 Binary files a/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources index c06542e72..848cc4294 100644 --- a/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources @@ -1 +1 @@ -1fe522ba55691c65026bca5a2a25ba298ee55f4800b4911a0ef38e3e9cb957c3 +3c9cd0d3c519dccf414268cafe5342374009341b38c0ab061e72a9b27976e8c7 diff --git a/crates/ide-plugin/tests/fixtures/semantic-tokens/Cargo.lock b/crates/ide-plugin/tests/fixtures/semantic-tokens/Cargo.lock index 3a47de575..be2767d88 100644 --- a/crates/ide-plugin/tests/fixtures/semantic-tokens/Cargo.lock +++ b/crates/ide-plugin/tests/fixtures/semantic-tokens/Cargo.lock @@ -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]] diff --git a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm index 6896d2afc..14de2b4b1 100644 Binary files a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources index 86ce748d0..c661d12d7 100644 --- a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources @@ -1 +1 @@ -175611b829f280adf44c7a4f2d544dc78c9111bd9cb3cb4842a89e456f503249 +1c09ba923ca3b59df4144095c6aba6c03981fc2dc5d208bda3ba0cfa4c4cde51 diff --git a/crates/ide-plugin/tests/fixtures/storage/Cargo.lock b/crates/ide-plugin/tests/fixtures/storage/Cargo.lock index 2906de079..1f127b51e 100644 --- a/crates/ide-plugin/tests/fixtures/storage/Cargo.lock +++ b/crates/ide-plugin/tests/fixtures/storage/Cargo.lock @@ -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]] diff --git a/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm index 848444dba..372582f9e 100644 Binary files a/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources index d129dd999..273800050 100644 --- a/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources @@ -1 +1 @@ -3f52ba4bcb2615ad4fe61fae48ac88e6e06c7f0713cf8b41283d12efb614f8fd +bddfc4fff3e25c611525ebfdee3fb7ab3e4890c394c3970da78f80a12a7a645d diff --git a/crates/ide-plugin/tests/fixtures/symbols/Cargo.lock b/crates/ide-plugin/tests/fixtures/symbols/Cargo.lock index 2ea6e3f00..cb3bc05ea 100644 --- a/crates/ide-plugin/tests/fixtures/symbols/Cargo.lock +++ b/crates/ide-plugin/tests/fixtures/symbols/Cargo.lock @@ -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]] diff --git a/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm index cced0fea5..72890eb64 100644 Binary files a/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm.sources index 72ef034bb..02a4e550c 100644 --- a/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm.sources @@ -1 +1 @@ -40c382c554daa12d2fb83c3be98064ed7fc9122e435bd331ea6bf382f4d399dd +5e1afb744ed0a230648d3dd9c2a9cee8cf7fc53db2f944a1ac580799b7e9ef96 diff --git a/crates/ide-plugin/tests/fixtures/tables/Cargo.lock b/crates/ide-plugin/tests/fixtures/tables/Cargo.lock index d7cbc7276..633a82b08 100644 --- a/crates/ide-plugin/tests/fixtures/tables/Cargo.lock +++ b/crates/ide-plugin/tests/fixtures/tables/Cargo.lock @@ -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]] diff --git a/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm index ca2af3295..26dbc4c7b 100644 Binary files a/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources index 58b168532..0477cd9f2 100644 --- a/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources @@ -1 +1 @@ -8039afdedc540c2d3278813b526eaf3d1e6a8a4b8f3bab498323837e59989cbc +6daecc5ace3661fac8b158d3bde0ecb13b366d8918e28aa846f10858c5c57442 diff --git a/crates/ide-plugin/tests/fixtures/web-log/Cargo.lock b/crates/ide-plugin/tests/fixtures/web-log/Cargo.lock index ecf4a074d..e8970c7bd 100644 --- a/crates/ide-plugin/tests/fixtures/web-log/Cargo.lock +++ b/crates/ide-plugin/tests/fixtures/web-log/Cargo.lock @@ -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]] diff --git a/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm index e8b0a13d8..c40a0772e 100644 Binary files a/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm.sources index 58d9c536a..92ae32a13 100644 --- a/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm.sources @@ -1 +1 @@ -6d63e1eb1bb8b2c0adb41f33f5164b57ddd8860b0a8a712c046963500d48b25f +cb791f264a947897336ada01f56f81e7bade9a046c701908dfff9852e0cdf15d diff --git a/crates/ide-plugin/tests/fixtures/web-pty/Cargo.lock b/crates/ide-plugin/tests/fixtures/web-pty/Cargo.lock index e3a599c86..54673d248 100644 --- a/crates/ide-plugin/tests/fixtures/web-pty/Cargo.lock +++ b/crates/ide-plugin/tests/fixtures/web-pty/Cargo.lock @@ -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]] diff --git a/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm index 77534ce35..80255cd69 100644 Binary files a/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources index 01af947e0..0926e9d23 100644 --- a/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources @@ -1 +1 @@ -52879d53687affe3aa1ac2ef1f18bb673ca2f02615840a223c8250bb7b0a636d +33adfc97d25a9777d3215488dd96dc41d772ed0c71621fb5c78e3c766031d6ae diff --git a/plugins/README.md b/plugins/README.md index 5791fa3d4..de045e921 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -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. diff --git a/plugins/official/ai-completion/Cargo.lock b/plugins/official/ai-completion/Cargo.lock index e7bc7ab92..f482b25d2 100644 --- a/plugins/official/ai-completion/Cargo.lock +++ b/plugins/official/ai-completion/Cargo.lock @@ -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]] diff --git a/plugins/official/ai-completion/dist/plugin.wasm b/plugins/official/ai-completion/dist/plugin.wasm index 9640db216..1e0905041 100644 Binary files a/plugins/official/ai-completion/dist/plugin.wasm and b/plugins/official/ai-completion/dist/plugin.wasm differ diff --git a/plugins/official/ai-completion/dist/plugin.wasm.sources b/plugins/official/ai-completion/dist/plugin.wasm.sources index c8cdd1293..2f7855233 100644 --- a/plugins/official/ai-completion/dist/plugin.wasm.sources +++ b/plugins/official/ai-completion/dist/plugin.wasm.sources @@ -1 +1 @@ -710e95defa1995356f259b1fd69c7df8cfe1d0c1c55b04df0f0d7173b7cb61b9 +978b1af84eae4b6ac381e1991241a11d6a416f3f45856bc0ad985ed59435b67d diff --git a/plugins/official/checkstyle/Cargo.lock b/plugins/official/checkstyle/Cargo.lock index bc79d7916..c46a67dad 100644 --- a/plugins/official/checkstyle/Cargo.lock +++ b/plugins/official/checkstyle/Cargo.lock @@ -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]] diff --git a/plugins/official/checkstyle/dist/plugin.wasm b/plugins/official/checkstyle/dist/plugin.wasm index 0d535e53a..e3756b549 100644 Binary files a/plugins/official/checkstyle/dist/plugin.wasm and b/plugins/official/checkstyle/dist/plugin.wasm differ diff --git a/plugins/official/checkstyle/dist/plugin.wasm.sources b/plugins/official/checkstyle/dist/plugin.wasm.sources index 94f53796b..cf3b5cad7 100644 --- a/plugins/official/checkstyle/dist/plugin.wasm.sources +++ b/plugins/official/checkstyle/dist/plugin.wasm.sources @@ -1 +1 @@ -145b164b99c18b6e9d0d0c2704fff0fa878fc56761d5cb76669a2a0cc9ddb3aa +5125e2e47c9adf8043d82fb60b9810fd3edcef272ff4a6c41b2450e19617d8cc diff --git a/plugins/official/csv-editor/Cargo.lock b/plugins/official/csv-editor/Cargo.lock index e0f64ffba..80eb1de9e 100644 --- a/plugins/official/csv-editor/Cargo.lock +++ b/plugins/official/csv-editor/Cargo.lock @@ -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]] diff --git a/plugins/official/csv-editor/dist/plugin.wasm b/plugins/official/csv-editor/dist/plugin.wasm index f9348b8e1..c64ba8e51 100644 Binary files a/plugins/official/csv-editor/dist/plugin.wasm and b/plugins/official/csv-editor/dist/plugin.wasm differ diff --git a/plugins/official/csv-editor/dist/plugin.wasm.sources b/plugins/official/csv-editor/dist/plugin.wasm.sources index f63423056..4d24352f6 100644 --- a/plugins/official/csv-editor/dist/plugin.wasm.sources +++ b/plugins/official/csv-editor/dist/plugin.wasm.sources @@ -1 +1 @@ -897d2b653cb5b8a91ef806307efa6fefd773c0aaacc746bf87fd7d2bea4bf147 +4fc2c8ca94ee5e45999c68285a124f535cde6f612aac319f04526acceb639339 diff --git a/plugins/official/git-toolbox/Cargo.lock b/plugins/official/git-toolbox/Cargo.lock index 9ad60b812..470d69f57 100644 --- a/plugins/official/git-toolbox/Cargo.lock +++ b/plugins/official/git-toolbox/Cargo.lock @@ -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]] diff --git a/plugins/official/git-toolbox/dist/plugin.wasm b/plugins/official/git-toolbox/dist/plugin.wasm index 251966e71..db5212204 100644 Binary files a/plugins/official/git-toolbox/dist/plugin.wasm and b/plugins/official/git-toolbox/dist/plugin.wasm differ diff --git a/plugins/official/git-toolbox/dist/plugin.wasm.sources b/plugins/official/git-toolbox/dist/plugin.wasm.sources index 4224ff9a6..e926cb43c 100644 --- a/plugins/official/git-toolbox/dist/plugin.wasm.sources +++ b/plugins/official/git-toolbox/dist/plugin.wasm.sources @@ -1 +1 @@ -9724a33ae85205245ec0bb94369d55e2df08aa328e872af2c03ad45c230d7865 +8cd633605b641e53cb27b61ce7648ab161671a334e6c5fcaba4a7c27c332612b diff --git a/plugins/official/graphviz/Cargo.lock b/plugins/official/graphviz/Cargo.lock index f7341f4a9..c17b29d49 100644 --- a/plugins/official/graphviz/Cargo.lock +++ b/plugins/official/graphviz/Cargo.lock @@ -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]] diff --git a/plugins/official/graphviz/dist/plugin.wasm b/plugins/official/graphviz/dist/plugin.wasm index c9dca6b8e..a98f5d86e 100644 Binary files a/plugins/official/graphviz/dist/plugin.wasm and b/plugins/official/graphviz/dist/plugin.wasm differ diff --git a/plugins/official/graphviz/dist/plugin.wasm.sources b/plugins/official/graphviz/dist/plugin.wasm.sources index 04659899a..2436a9f37 100644 --- a/plugins/official/graphviz/dist/plugin.wasm.sources +++ b/plugins/official/graphviz/dist/plugin.wasm.sources @@ -1 +1 @@ -d5245ffd43097795062f7bb887162c13d99d48b68f7883e4ff6adb41a663d157 +48525c9df30fce0a443dadbfb85dd6f69e56dadbe9c529cfe4274ef24ac19a3b diff --git a/plugins/official/key-promoter/Cargo.lock b/plugins/official/key-promoter/Cargo.lock index 089dc6102..d1fd4e8c8 100644 --- a/plugins/official/key-promoter/Cargo.lock +++ b/plugins/official/key-promoter/Cargo.lock @@ -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]] diff --git a/plugins/official/key-promoter/dist/plugin.wasm b/plugins/official/key-promoter/dist/plugin.wasm index 70aeeee3d..b75747bcc 100644 Binary files a/plugins/official/key-promoter/dist/plugin.wasm and b/plugins/official/key-promoter/dist/plugin.wasm differ diff --git a/plugins/official/key-promoter/dist/plugin.wasm.sources b/plugins/official/key-promoter/dist/plugin.wasm.sources index c89faa388..3499a85f5 100644 --- a/plugins/official/key-promoter/dist/plugin.wasm.sources +++ b/plugins/official/key-promoter/dist/plugin.wasm.sources @@ -1 +1 @@ -2fbf40065eae7c9ef0e927dfae4fde1f698858b2ae3047db9c242a12e443d823 +de4355361ad2f4b0950669ca41b17752bc7b67fc24a5eb40a2ffc7e75fe62441 diff --git a/plugins/official/license-header/Cargo.lock b/plugins/official/license-header/Cargo.lock index 736eea2b5..0322d2029 100644 --- a/plugins/official/license-header/Cargo.lock +++ b/plugins/official/license-header/Cargo.lock @@ -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]] diff --git a/plugins/official/license-header/dist/plugin.wasm b/plugins/official/license-header/dist/plugin.wasm index ca272ee18..d3e9e1623 100644 Binary files a/plugins/official/license-header/dist/plugin.wasm and b/plugins/official/license-header/dist/plugin.wasm differ diff --git a/plugins/official/license-header/dist/plugin.wasm.sources b/plugins/official/license-header/dist/plugin.wasm.sources index 85b014072..fb9e59b89 100644 --- a/plugins/official/license-header/dist/plugin.wasm.sources +++ b/plugins/official/license-header/dist/plugin.wasm.sources @@ -1 +1 @@ -7b5480ba83b888a43bb641f95ad679d60b132548516b92d91d8d7e118220f2c3 +cba6d696b5984ba62619982042e496c39afcc2e97d8093872a9c7e0522d30b8c diff --git a/plugins/official/mybatis/Cargo.lock b/plugins/official/mybatis/Cargo.lock index f4c6da463..d7c52bf34 100644 --- a/plugins/official/mybatis/Cargo.lock +++ b/plugins/official/mybatis/Cargo.lock @@ -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]] diff --git a/plugins/official/mybatis/dist/plugin.wasm b/plugins/official/mybatis/dist/plugin.wasm index 42d6f614d..76bb5bb26 100644 Binary files a/plugins/official/mybatis/dist/plugin.wasm and b/plugins/official/mybatis/dist/plugin.wasm differ diff --git a/plugins/official/mybatis/dist/plugin.wasm.sources b/plugins/official/mybatis/dist/plugin.wasm.sources index 2283eb53b..d9b565b62 100644 --- a/plugins/official/mybatis/dist/plugin.wasm.sources +++ b/plugins/official/mybatis/dist/plugin.wasm.sources @@ -1 +1 @@ -6762b2c436f8dd0a586759cbf0b2cc316560f581619fc32efb717f96505f720b +a735b01070abd15165d221f994d712e199a4468f50c637d5c704d13639f2643a diff --git a/plugins/official/regex-tester/Cargo.lock b/plugins/official/regex-tester/Cargo.lock index 5ccd74ebb..c3c26b7f6 100644 --- a/plugins/official/regex-tester/Cargo.lock +++ b/plugins/official/regex-tester/Cargo.lock @@ -107,6 +107,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" name = "openidea-plugin" version = "0.1.0" dependencies = [ + "serde", + "serde_json", "wit-bindgen", ] @@ -189,6 +191,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", + "serde_derive", ] [[package]] diff --git a/plugins/official/regex-tester/dist/plugin.wasm b/plugins/official/regex-tester/dist/plugin.wasm index 0828885b2..9b3a80b1f 100644 Binary files a/plugins/official/regex-tester/dist/plugin.wasm and b/plugins/official/regex-tester/dist/plugin.wasm differ diff --git a/plugins/official/regex-tester/dist/plugin.wasm.sources b/plugins/official/regex-tester/dist/plugin.wasm.sources index 12cb7e196..9ee5d010b 100644 --- a/plugins/official/regex-tester/dist/plugin.wasm.sources +++ b/plugins/official/regex-tester/dist/plugin.wasm.sources @@ -1 +1 @@ -f8e3d365d62382437b4af9d7889c135725cf437bb1ec3a1144466af72ef8f859 +01d43147d97f29798681450bfc75a1ef9cd578ede283012aecc09665234e9bce diff --git a/plugins/official/solidity/Cargo.lock b/plugins/official/solidity/Cargo.lock index d2be61fe3..cfd1e9c75 100644 --- a/plugins/official/solidity/Cargo.lock +++ b/plugins/official/solidity/Cargo.lock @@ -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]] diff --git a/plugins/official/solidity/dist/plugin.wasm b/plugins/official/solidity/dist/plugin.wasm index 8dccd1dd7..8c1cc4950 100644 Binary files a/plugins/official/solidity/dist/plugin.wasm and b/plugins/official/solidity/dist/plugin.wasm differ diff --git a/plugins/official/solidity/dist/plugin.wasm.sources b/plugins/official/solidity/dist/plugin.wasm.sources index 623c361f2..d1e23c563 100644 --- a/plugins/official/solidity/dist/plugin.wasm.sources +++ b/plugins/official/solidity/dist/plugin.wasm.sources @@ -1 +1 @@ -9e8e7a1fc6aa73e11655114e4cf8c642ea6ea2318b0711fb4e1a82fb0f353bae +924d3e7316d6d6e4dd8c371a1485df3215d9b2e71c27e7c683edb613fd1bee1a diff --git a/plugins/official/string-tools/Cargo.lock b/plugins/official/string-tools/Cargo.lock index 8f0807fe9..090af1841 100644 --- a/plugins/official/string-tools/Cargo.lock +++ b/plugins/official/string-tools/Cargo.lock @@ -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]] diff --git a/plugins/official/string-tools/dist/plugin.wasm b/plugins/official/string-tools/dist/plugin.wasm index 4f7b8ac75..f8f5691ba 100644 Binary files a/plugins/official/string-tools/dist/plugin.wasm and b/plugins/official/string-tools/dist/plugin.wasm differ diff --git a/plugins/official/string-tools/dist/plugin.wasm.sources b/plugins/official/string-tools/dist/plugin.wasm.sources index d7bedcb6c..293f6626e 100644 --- a/plugins/official/string-tools/dist/plugin.wasm.sources +++ b/plugins/official/string-tools/dist/plugin.wasm.sources @@ -1 +1 @@ -428dd3b2ebc9bc248775e385fc431f0621e564533ed54f70d12bf5db950627cb +3ff5852da7219a878efa8dc4d47b69b314d7ccfb1403c2ff2522aaeaba5ea9e2 diff --git a/sdk/rust/openidea-plugin/Cargo.lock b/sdk/rust/openidea-plugin/Cargo.lock index 9629fb20c..a9bbd3fcf 100644 --- a/sdk/rust/openidea-plugin/Cargo.lock +++ b/sdk/rust/openidea-plugin/Cargo.lock @@ -98,6 +98,8 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" name = "openidea-plugin" version = "0.1.0" dependencies = [ + "serde", + "serde_json", "wit-bindgen", "wit-parser", ] @@ -143,6 +145,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", + "serde_derive", ] [[package]] diff --git a/sdk/rust/openidea-plugin/Cargo.toml b/sdk/rust/openidea-plugin/Cargo.toml index 3f36703f7..1a4e42ad2 100644 --- a/sdk/rust/openidea-plugin/Cargo.toml +++ b/sdk/rust/openidea-plugin/Cargo.toml @@ -15,6 +15,10 @@ path = "src/lib.rs" [target.'cfg(target_arch = "wasm32")'.dependencies] wit-bindgen = "0.63" +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" + [dev-dependencies] # Reads the WIT in tests/wit_coverage.rs (the version wit-bindgen already uses). wit-parser = "0.262" diff --git a/sdk/rust/openidea-plugin/README.md b/sdk/rust/openidea-plugin/README.md index 67811d0f0..7d24498f7 100644 --- a/sdk/rust/openidea-plugin/README.md +++ b/sdk/rust/openidea-plugin/README.md @@ -3,6 +3,19 @@ The Rust API is documented by `cargo doc`. Web pages shipped alongside a Rust component use `window.openidea` rather than Rust imports. +API 1.3 adds `Event::FilesChanged(FilesChanged)` for debounced external file +creation, modification and removal in open projects. Declare +`engines.openidea = "^1.3"`, the `events` capability and +`activation = ["on-event:files-changed"]`, then handle the typed event in +`Plugin::handle_events`. Use `host::subscribe_paths("files-changed", &["config/**"])` +to receive only matching project-relative paths in each category. An empty +filter pauses delivery. Paths are absolute, like `Event::FileSaved`; pending +batches are discarded when their project closes. The released WIT variant +is unchanged: the SDK decodes `event::other` with kind `files-changed`. + +See [the event contract](../../../plugins/README.md#events) +and the [real Wasm fixture](../../../crates/ide-plugin/tests/fixtures/files-events). + API 1.3 adds capability-gated interactive PTYs to that web bridge. Declare `engines.openidea = "^1.3"` and `process:` in `plugin.toml`, then send `pty-spawn`, `pty-input`, `pty-resize` and `pty-kill` JSON strings using diff --git a/sdk/rust/openidea-plugin/src/host.rs b/sdk/rust/openidea-plugin/src/host.rs index 66ffea31a..a7b3bd2ba 100644 --- a/sdk/rust/openidea-plugin/src/host.rs +++ b/sdk/rust/openidea-plugin/src/host.rs @@ -534,7 +534,7 @@ mod imp { source: ActionSource::parse(&a.source), shortcut: a.shortcut, }), - E::Other(x) => Event::Other { kind: x.kind, data: x.data }, + E::Other(x) => Event::from_other(x.kind, x.data), } } } @@ -1017,7 +1017,7 @@ pub fn locale() -> String { imp::locale() } -/// Subscribe to project-relative path globs for file-opened or file-saved. +/// Subscribe to project-relative path globs for file-opened, file-saved or files-changed. /// Replaces the previous filter. Empty globs pause delivery; `subscribe` restores all paths. /// Requires `events`. Register during activation before the events you want to observe. pub fn subscribe_paths(kind: &str, globs: &[&str]) -> Result<(), Error> { diff --git a/sdk/rust/openidea-plugin/src/mock.rs b/sdk/rust/openidea-plugin/src/mock.rs index 056b6fcac..43e159c33 100644 --- a/sdk/rust/openidea-plugin/src/mock.rs +++ b/sdk/rust/openidea-plugin/src/mock.rs @@ -1126,7 +1126,7 @@ pub mod current { pub fn subscribe_paths(kind: &str, globs: &[&str]) -> Result<(), Error> { with(|s| { require(s, "events")?; - if !matches!(kind, "file-opened" | "file-saved") + if !matches!(kind, "file-opened" | "file-saved" | "files-changed") || globs.len() > 64 || globs.iter().any(|g| { let relative = g.trim_start_matches("./"); @@ -1148,6 +1148,9 @@ pub mod current { "file-saved" => { s.subscriptions.insert(EventKind::FileSaved); } + "files-changed" => { + s.subscriptions.insert(EventKind::FilesChanged); + } _ => {} } s.path_subscriptions.insert(kind.to_owned(), globs.iter().map(|g| (*g).to_owned()).collect()); diff --git a/sdk/rust/openidea-plugin/src/types.rs b/sdk/rust/openidea-plugin/src/types.rs index f69089617..1168f7519 100644 --- a/sdk/rust/openidea-plugin/src/types.rs +++ b/sdk/rust/openidea-plugin/src/types.rs @@ -213,6 +213,7 @@ impl From for ContextValue { pub enum EventKind { FileOpened, FileSaved, + FilesChanged, EditorFocused, /// Needs `events.input`. ActionPerformed, @@ -226,6 +227,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", @@ -270,12 +272,21 @@ pub struct ActionPerformed { pub shortcut: Option, } +/// Debounced changes in an open project, delivered with the `events` capability. +#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct FilesChanged { + pub created: Vec, + pub modified: Vec, + pub removed: Vec, +} + /// A workbench event (see [`crate::host::subscribe`]). #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum Event { FileOpened(String), FileSaved(String), + FilesChanged(FilesChanged), EditorFocused(Option), ActionPerformed(ActionPerformed), /// A repository (its root) changed. @@ -494,3 +505,15 @@ impl NotificationAction { self } } + +impl Event { + /// Decodes additive events while preserving unknown or malformed payloads. + pub fn from_other(kind: String, data: String) -> Self { + if kind == "files-changed" + && let Ok(batch) = serde_json::from_str(&data) + { + return Self::FilesChanged(batch); + } + Self::Other { kind, data } + } +} diff --git a/sdk/rust/openidea-plugin/tests/files_changed.rs b/sdk/rust/openidea-plugin/tests/files_changed.rs new file mode 100644 index 000000000..99d09afea --- /dev/null +++ b/sdk/rust/openidea-plugin/tests/files_changed.rs @@ -0,0 +1,41 @@ +use openidea_plugin::{Event, EventKind, FilesChanged}; + +#[test] +fn additive_files_changed_payload_is_typed_and_lossless() { + let batch = FilesChanged { + created: vec!["/project/新建😀.rs".into()], + modified: vec![r"C:\project\a.rs".into()], + removed: vec!["/project/gone.rs".into()], + }; + assert_eq!(EventKind::FilesChanged.name(), "files-changed"); + let data = serde_json::to_string(&batch).unwrap(); + assert_eq!(Event::from_other("files-changed".into(), data), Event::FilesChanged(batch)); +} + +#[test] +fn unknown_and_malformed_events_keep_the_original_payload() { + for (kind, data) in + [("future", "{}"), ("files-changed", "bad"), ("files-changed", r#"{"created":[],"modified":[],"removed":[1]}"#)] + { + assert_eq!(Event::from_other(kind.into(), data.into()), Event::Other { kind: kind.into(), data: data.into() }); + } +} + +#[test] +fn mock_filters_files_changed_with_the_events_capability() { + use openidea_plugin::{Error, host, mock::MockHost}; + let mock = MockHost::install(); + host::subscribe_paths("files-changed", &["config/*.yaml"]).unwrap(); + assert_eq!(mock.path_subscriptions()["files-changed"], ["config/*.yaml"]); + assert!(mock.subscriptions().contains(&EventKind::FilesChanged)); + host::subscribe_paths("files-changed", &[]).unwrap(); + assert!(mock.path_subscriptions()["files-changed"].is_empty()); + host::subscribe(EventKind::FilesChanged).unwrap(); + assert!(mock.path_subscriptions().is_empty()); + host::subscribe_paths("files-changed", &["**/*.yaml"]).unwrap(); + host::unsubscribe(EventKind::FilesChanged); + assert!(mock.path_subscriptions().is_empty()); + assert!(!mock.subscriptions().contains(&EventKind::FilesChanged)); + let _denied = MockHost::install().with_capabilities(&[]); + assert!(matches!(host::subscribe_paths("files-changed", &[]), Err(Error::Denied(_)))); +} diff --git a/sdk/rust/openidea-plugin/wit/events.wit b/sdk/rust/openidea-plugin/wit/events.wit index 165b89e2e..8851d18a7 100644 --- a/sdk/rust/openidea-plugin/wit/events.wit +++ b/sdk/rust/openidea-plugin/wit/events.wit @@ -29,7 +29,8 @@ interface events { /// Delivers `kind` events from now on, for the rest of the session (also /// across idle reclaim: an event activates the plugin again). Declare /// `activation = ["on-event:"]` to get them before first use. - /// Kinds: `file-opened`, `file-saved`, `editor-focused` (the focused editor + /// Kinds: `file-opened`, `file-saved`, `files-changed` (batched disk changes + /// as `other`, with created/modified/removed path lists in JSON), `editor-focused` (the focused editor /// changed), `vcs-changed` (a repository's state changed: commit, /// checkout, fetch…), `action-performed` (an IDE action ran) and /// `scheduled` (the `on-schedule` period elapsed); later IDEs add more, @@ -38,7 +39,7 @@ interface events { subscribe: func(kind: string) -> result<_, error>; /// Replaces this kind's subscription with project-relative globs (OR). /// Empty globs pause delivery; `subscribe` restores unfiltered delivery. - /// Kinds: file-opened, file-saved. Requires `events`. + /// Kinds: file-opened, file-saved, files-changed. Requires `events`. subscribe-paths: func(kind: string, globs: list) -> result<_, error>; unsubscribe: func(kind: string); } diff --git a/src/bindings.ts b/src/bindings.ts index 8e7c7d973..8f21ba196 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -3715,7 +3715,7 @@ export type ErrorKind = "notFound" | "alreadyExists" | "permissionDenied" | "inv "unavailable" | "io" | "protocol" | "internal"; /** Kinds of workbench events a plugin can subscribe to (`on-event:`). */ -export type EventKind = "file-opened" | "file-saved" | "editor-focused" | "action-performed" | "vcs-changed" | "scheduled"; +export type EventKind = "file-opened" | "file-saved" | "files-changed" | "editor-focused" | "action-performed" | "vcs-changed" | "scheduled"; /** An exception breakpoint filter offered by an adapter (`exceptionBreakpointFilters`). */ export type ExceptionFilter = { @@ -3991,6 +3991,13 @@ export type FileTypeAssociation = { hashbangs?: string[], }; +/** Debounced worktree changes. Paths use the same absolute form as `file-saved`. */ +export type FilesChanged = { + created: string[], + modified: string[], + removed: string[], +}; + export type FoldKind = "region" | "comment" | "imports"; export type FoldRange = { @@ -8320,7 +8327,7 @@ export type WindowTab = { }; /** A workbench event delivered to plugins. */ -export type WorkbenchEvent = { kind: "fileOpened"; value: string } | { kind: "fileSaved"; value: string } | { kind: "editorFocused"; value: string | null } | { kind: "actionPerformed"; value: ActionPerformed } | { kind: "vcsChanged"; value: string } | { kind: "scheduled" }; +export type WorkbenchEvent = { kind: "fileOpened"; value: string } | { kind: "fileSaved"; value: string } | { kind: "filesChanged"; value: FilesChanged } | { kind: "editorFocused"; value: string | null } | { kind: "actionPerformed"; value: ActionPerformed } | { kind: "vcsChanged"; value: string } | { kind: "scheduled" }; export type WorkingTreeChange = { path: string, diff --git a/src/features/plugins/PluginsPage.tsx b/src/features/plugins/PluginsPage.tsx index 26b6eb2ca..a6a8cf584 100644 --- a/src/features/plugins/PluginsPage.tsx +++ b/src/features/plugins/PluginsPage.tsx @@ -296,7 +296,7 @@ function CapabilityRow({ cap, children }: { cap: CapabilityInfo; children?: Reac return (
  • {children} - {cap.description} + {tx(cap.description)} {cap.id} {RISK_LABEL[cap.risk].label}
  • diff --git a/src/features/plugins/consent.tsx b/src/features/plugins/consent.tsx index 24caddfa0..b2f1268a6 100644 --- a/src/features/plugins/consent.tsx +++ b/src/features/plugins/consent.tsx @@ -103,7 +103,7 @@ function ConsentDialog({ pending, onDone }: { pending: Pending; onDone: () => vo • )} - {c.description} + {tx(c.description)} {c.id} {RISK_LABEL[c.risk].label} diff --git a/src/platform/locales/text/en.json b/src/platform/locales/text/en.json index dcfd19fa8..ec3356e02 100644 --- a/src/platform/locales/text/en.json +++ b/src/platform/locales/text/en.json @@ -19,6 +19,7 @@ "connect_timeout must be greater than zero": "connect_timeout must be greater than zero", "database connection is closed": "database connection is closed", "Database": "Database", + "Observe editor, filesystem and version control events": "Observe editor, filesystem and version control events", "Run `{command}` in a terminal.": "Run `{command}` in a terminal.", "Run `{command}` in a terminal and sign in.": "Run `{command}` in a terminal and sign in.", "{name} needs you to sign in. {hint}": "{name} needs you to sign in. {hint}", diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index cc4601719..132733ed6 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -18,6 +18,7 @@ "commit or roll back before clearing the selected database": "清除所选数据库前,请先提交或回滚事务", "connect_timeout must be greater than zero": "connect_timeout 必须大于零", "database connection is closed": "数据库连接已关闭", + "Observe editor, filesystem and version control events": "监听编辑器、文件系统和版本控制事件", "Run `{command}` in a terminal.": "在终端中运行 `{command}`。", "Run `{command}` in a terminal and sign in.": "在终端中运行 `{command}` 并登录。", "{name} needs you to sign in. {hint}": "{name} 需要你登录。{hint}", diff --git a/wit/events.wit b/wit/events.wit index 165b89e2e..8851d18a7 100644 --- a/wit/events.wit +++ b/wit/events.wit @@ -29,7 +29,8 @@ interface events { /// Delivers `kind` events from now on, for the rest of the session (also /// across idle reclaim: an event activates the plugin again). Declare /// `activation = ["on-event:"]` to get them before first use. - /// Kinds: `file-opened`, `file-saved`, `editor-focused` (the focused editor + /// Kinds: `file-opened`, `file-saved`, `files-changed` (batched disk changes + /// as `other`, with created/modified/removed path lists in JSON), `editor-focused` (the focused editor /// changed), `vcs-changed` (a repository's state changed: commit, /// checkout, fetch…), `action-performed` (an IDE action ran) and /// `scheduled` (the `on-schedule` period elapsed); later IDEs add more, @@ -38,7 +39,7 @@ interface events { subscribe: func(kind: string) -> result<_, error>; /// Replaces this kind's subscription with project-relative globs (OR). /// Empty globs pause delivery; `subscribe` restores unfiltered delivery. - /// Kinds: file-opened, file-saved. Requires `events`. + /// Kinds: file-opened, file-saved, files-changed. Requires `events`. subscribe-paths: func(kind: string, globs: list) -> result<_, error>; unsubscribe: func(kind: string); }