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