дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(plugins): filter file events by path and resolve a document's project (#2003)
Adds API 1.3 `events.subscribe-paths(kind, globs)` for `file-opened` and `file-saved`: project-relative globs checked before an event is queued or wakes an idle plugin, and again before a queued event is delivered. A new filter replaces the old one atomically; `subscribe` restores unfiltered delivery; filters survive idle reclamation. Also `workspace.project-of(path)`, the most specific open project containing a document (reading it still needs `project.read`). Closes #1958. Co-authored-by: arnaud-ritti <[email protected]>
Цей коміт міститься в:
1 джерело
8509ca671f
коміт
1891119f9d
43 files changed
+949
-42
No files matched your search
@@ -188,7 +188,7 @@ pub(super) struct Shared {
|
||||
contexts: Mutex<BTreeMap<ContextKey, String>>,
|
||||
closed_clients: Mutex<BTreeSet<String>>,
|
||||
/// plugin → kinds subscribed at run time (kept across idle reclaim).
|
||||
pub subscriptions: Mutex<HashMap<String, BTreeSet<EventKind>>>,
|
||||
pub subscriptions: Mutex<super::event_paths::Subscriptions>,
|
||||
/// plugin → files it shows problems for, withdrawn when it stops.
|
||||
diagnostics: Mutex<HashMap<String, BTreeSet<PathBuf>>>,
|
||||
pub secrets: RwLock<Arc<dyn SecretStore>>,
|
||||
@@ -331,7 +331,7 @@ impl Shared {
|
||||
let _ = workspace.publish_diagnostics(plugin, &path, Vec::new());
|
||||
}
|
||||
}
|
||||
if subscriptions && self.subscriptions.lock().remove(plugin).is_some() {
|
||||
if subscriptions && self.subscriptions.lock().remove(plugin) {
|
||||
self.sink.emit(None, PluginEvent::SubscriptionsChanged);
|
||||
}
|
||||
}
|
||||
@@ -1166,13 +1166,22 @@ impl HostApi for PluginApi {
|
||||
})
|
||||
}
|
||||
|
||||
fn subscribe(&self, kind: EventKind, on: bool) {
|
||||
let changed = {
|
||||
let mut subs = self.shared.subscriptions.lock();
|
||||
let set = subs.entry(self.id().to_owned()).or_default();
|
||||
if on { set.insert(kind) } else { set.remove(&kind) }
|
||||
};
|
||||
if changed {
|
||||
fn subscribe(&self, kind: EventKind) {
|
||||
if self.shared.subscriptions.lock().set(self.id(), kind) {
|
||||
self.shared.sink.emit(None, PluginEvent::SubscriptionsChanged);
|
||||
}
|
||||
}
|
||||
|
||||
fn subscribe_paths(&self, kind: &str, globs: Vec<String>) -> Result<(), HostError> {
|
||||
let filter = super::event_paths::PathFilter::new(kind, globs)?;
|
||||
if self.shared.subscriptions.lock().set_paths(self.id(), kind, filter) {
|
||||
self.shared.sink.emit(None, PluginEvent::SubscriptionsChanged);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unsubscribe(&self, kind: &str) {
|
||||
if self.shared.subscriptions.lock().unsubscribe(self.id(), kind) {
|
||||
self.shared.sink.emit(None, PluginEvent::SubscriptionsChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
//! Project-relative event filters, kept with runtime subscriptions across idle reclaim.
|
||||
use super::{LoadedPlugin, PluginHost};
|
||||
use globset::{GlobBuilder, GlobMatcher};
|
||||
use ide_plugin::{EventKind, HostError, WorkbenchEvent};
|
||||
use std::{
|
||||
collections::{BTreeSet, HashMap},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(super) struct Subscriptions {
|
||||
pub kinds: HashMap<String, BTreeSet<EventKind>>,
|
||||
pub paths: HashMap<String, HashMap<String, PathFilter>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct PathFilter {
|
||||
pub globs: Vec<String>,
|
||||
matchers: Vec<GlobMatcher>,
|
||||
}
|
||||
|
||||
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 globs.len() > 64 {
|
||||
return Err(HostError::Invalid("at most 64 event path globs".into()));
|
||||
}
|
||||
let mut matchers = Vec::new();
|
||||
for input in &globs {
|
||||
let glob = input.trim_start_matches("./");
|
||||
if glob.is_empty()
|
||||
|| input.len() > 1024
|
||||
|| glob.starts_with('/')
|
||||
|| glob.contains('\\')
|
||||
|| glob.contains(':')
|
||||
|| glob.chars().any(char::is_control)
|
||||
|| glob.split('/').any(|c| c == "..")
|
||||
{
|
||||
return Err(HostError::Invalid(
|
||||
"event globs must be project-relative paths, at most 1024 bytes each".into(),
|
||||
));
|
||||
}
|
||||
matchers.push(
|
||||
GlobBuilder::new(glob)
|
||||
.literal_separator(true)
|
||||
.build()
|
||||
.map_err(|e| HostError::Invalid(format!("invalid event glob: {e}")))?
|
||||
.compile_matcher(),
|
||||
);
|
||||
}
|
||||
Ok(Self { globs, matchers })
|
||||
}
|
||||
|
||||
pub fn matches(&self, root: &Path, path: &Path) -> bool {
|
||||
let path = ide_plugin::resolve_real(path);
|
||||
let root = ide_plugin::resolve_real(root);
|
||||
path.strip_prefix(&root).is_ok_and(|relative| self.matchers.iter().any(|m| m.is_match(relative)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Subscriptions {
|
||||
pub fn remove(&mut self, plugin: &str) -> bool {
|
||||
self.kinds.remove(plugin).is_some() | self.paths.remove(plugin).is_some()
|
||||
}
|
||||
/// Subscribes to every event of `kind`, dropping its path filter.
|
||||
pub fn set(&mut self, plugin: &str, kind: EventKind) -> bool {
|
||||
let removed = self.paths.get_mut(plugin).is_some_and(|filters| filters.remove(kind.name()).is_some());
|
||||
self.kinds.entry(plugin.to_owned()).or_default().insert(kind) | removed
|
||||
}
|
||||
pub fn set_paths(&mut self, plugin: &str, kind: &str, filter: PathFilter) -> bool {
|
||||
let mut changed = false;
|
||||
if let Some(kind) = EventKind::parse(kind) {
|
||||
changed = self.kinds.entry(plugin.to_owned()).or_default().insert(kind);
|
||||
}
|
||||
let previous = self.paths.entry(plugin.to_owned()).or_default().insert(kind.to_owned(), filter.clone());
|
||||
previous.is_none_or(|old| old.globs != filter.globs) | changed
|
||||
}
|
||||
pub fn unsubscribe(&mut self, plugin: &str, kind: &str) -> bool {
|
||||
let removed = self.paths.get_mut(plugin).is_some_and(|filters| filters.remove(kind).is_some());
|
||||
let off = EventKind::parse(kind)
|
||||
.is_some_and(|kind| self.kinds.get_mut(plugin).is_some_and(|kinds| kinds.remove(&kind)));
|
||||
removed | off
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginHost {
|
||||
pub(super) fn filter_path_events(
|
||||
&self,
|
||||
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();
|
||||
events
|
||||
.iter()
|
||||
.filter(|event| {
|
||||
let path = match event {
|
||||
WorkbenchEvent::FileOpened(path) | WorkbenchEvent::FileSaved(path) => Path::new(path),
|
||||
_ => return true,
|
||||
};
|
||||
let Some(filter) = filters.get(event.kind().name()) else { return true };
|
||||
if !path.is_absolute() {
|
||||
return false;
|
||||
}
|
||||
let Some(workspace) = &workspace else { return false };
|
||||
let root = match client {
|
||||
Some(client) => workspace.project_of(client),
|
||||
None => {
|
||||
let real = ide_plugin::resolve_real(path);
|
||||
workspace
|
||||
.projects()
|
||||
.into_iter()
|
||||
.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;
|
||||
}
|
||||
filter.matches(&root, path)
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn glob_validation_is_bounded_and_project_relative() {
|
||||
for invalid in ["", "/etc/*", "../*", "a/../b", "C:/a", "a\\b", "a\nb", "["] {
|
||||
assert!(PathFilter::new("file-opened", vec![invalid.into()]).is_err(), "{invalid:?}");
|
||||
}
|
||||
assert!(PathFilter::new("editor-focused", vec!["*".into()]).is_err());
|
||||
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());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matchers_use_or_semantics_and_component_boundaries() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let root = dunce::canonicalize(root.path()).unwrap();
|
||||
let filter = PathFilter::new("file-opened", vec!["./config/*.yaml".into(), "src/**/*.rs".into()]).unwrap();
|
||||
for path in ["config/a.yaml", "src/main.rs", "src/深い/a.rs"] {
|
||||
assert!(filter.matches(&root, &root.join(path)), "{path}");
|
||||
}
|
||||
for path in ["config/nested/a.yaml", "other/a.yaml", "src/a.ts"] {
|
||||
assert!(!filter.matches(&root, &root.join(path)), "{path}");
|
||||
}
|
||||
assert!(!filter.matches(&root, &root.with_extension("other").join("config/a.yaml")));
|
||||
assert!(!PathFilter::new("file-opened", vec![]).unwrap().matches(&root, &root.join("a")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subscriptions_replace_atomically_and_legacy_subscribe_clears_filters() {
|
||||
let mut subs = Subscriptions::default();
|
||||
subs.set_paths("plugin", "file-opened", PathFilter::new("file-opened", vec!["*.rs".into()]).unwrap());
|
||||
assert!(subs.kinds["plugin"].contains(&EventKind::FileOpened));
|
||||
subs.set_paths("plugin", "file-opened", PathFilter::new("file-opened", vec![]).unwrap());
|
||||
assert!(subs.paths["plugin"]["file-opened"].globs.is_empty());
|
||||
subs.set("plugin", EventKind::FileOpened);
|
||||
assert!(subs.paths["plugin"].is_empty());
|
||||
subs.set_paths("plugin", "file-saved", PathFilter::new("file-saved", vec!["*".into()]).unwrap());
|
||||
assert!(subs.unsubscribe("plugin", "file-saved"));
|
||||
assert!(subs.remove("plugin"));
|
||||
assert!(subs.paths.is_empty() && subs.kinds.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
mod api;
|
||||
pub mod commit;
|
||||
pub mod endpoints;
|
||||
mod event_paths;
|
||||
mod hover;
|
||||
mod icon_extensions;
|
||||
mod icon_patterns;
|
||||
@@ -1450,7 +1451,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.get(p.id())) {
|
||||
if self.wants(p, kind, subs.kinds.get(p.id())) {
|
||||
out.insert(kind);
|
||||
}
|
||||
}
|
||||
@@ -1483,7 +1484,8 @@ impl PluginHost {
|
||||
let mut queues = self.queues.lock();
|
||||
for p in plugins {
|
||||
let wanted: Vec<WorkbenchEvent> =
|
||||
events.iter().filter(|e| self.wants(&p, e.kind(), subs.get(p.id()))).cloned().collect();
|
||||
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);
|
||||
if wanted.is_empty() {
|
||||
continue;
|
||||
}
|
||||
@@ -1563,6 +1565,18 @@ impl PluginHost {
|
||||
/// Returns whether the plugin was activated.
|
||||
fn deliver(&self, id: &str, client: Option<String>, events: Vec<WorkbenchEvent>) -> bool {
|
||||
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() {
|
||||
return false;
|
||||
}
|
||||
if !plugin.meta.lock().pending.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ type Events = Arc<Mutex<Vec<(Option<String>, PluginEvent)>>>;
|
||||
/// What plugins see of the test project: real `Documents`, as in the IDE.
|
||||
struct TestWorkspace {
|
||||
root: PathBuf,
|
||||
other_roots: Mutex<Vec<PathBuf>>,
|
||||
documents: Arc<ide_workspace::Documents>,
|
||||
trusted: bool,
|
||||
/// (producer, path) → published diagnostics.
|
||||
@@ -35,6 +36,7 @@ impl TestWorkspace {
|
||||
);
|
||||
Arc::new(Self {
|
||||
root: root.to_path_buf(),
|
||||
other_roots: Mutex::default(),
|
||||
documents: Arc::new(documents),
|
||||
trusted: true,
|
||||
diagnostics: Mutex::default(),
|
||||
@@ -67,7 +69,7 @@ impl TestWorkspace {
|
||||
|
||||
impl PluginWorkspace for TestWorkspace {
|
||||
fn projects(&self) -> Vec<PathBuf> {
|
||||
vec![self.root.clone()]
|
||||
std::iter::once(self.root.clone()).chain(self.other_roots.lock().iter().cloned()).collect()
|
||||
}
|
||||
fn buffer_text(&self, path: &Path) -> Option<String> {
|
||||
self.documents.text_of(path)
|
||||
@@ -1653,6 +1655,7 @@ fn git_toolbox_does_not_fetch_untrusted_projects() {
|
||||
f.install(&official("git-toolbox"));
|
||||
let untrusted = Arc::new(TestWorkspace {
|
||||
root: f.project.clone(),
|
||||
other_roots: Mutex::default(),
|
||||
documents: f.workspace.documents.clone(),
|
||||
trusted: false,
|
||||
diagnostics: Mutex::default(),
|
||||
@@ -3565,3 +3568,97 @@ fn directory_prompts_and_project_requests_use_the_calling_window_without_a_capab
|
||||
assert!(!f.take_events().iter().any(|(_, event)| matches!(event, PluginEvent::OpenProject { .. })));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_filtered_events_do_not_wake_idle_plugins_and_recheck_queued_filters() {
|
||||
let f = fixture(Limits { idle_timeout: Some(Duration::from_millis(100)), ..Limits::default() });
|
||||
f.install(&sample("path-events"));
|
||||
f.run("test.path-events", "count", None).unwrap();
|
||||
let matched = to_ipc(&f.project.join("config/app.yaml"));
|
||||
let other = to_ipc(&f.project.join("src/main.rs"));
|
||||
let deliver = |events| {
|
||||
f.host.post_events(Some("main"), events);
|
||||
f.host.flush_events_at(Instant::now() + Duration::from_secs(3));
|
||||
assert!(f.host.wait_events(Duration::from_secs(10)));
|
||||
};
|
||||
std::thread::sleep(Duration::from_millis(150));
|
||||
f.host.tick();
|
||||
assert_eq!(f.status("test.path-events"), PluginStatus::Inactive);
|
||||
let activations = f.host.info_of("test.path-events").unwrap().stats.activations;
|
||||
deliver(vec![WorkbenchEvent::FileOpened(other.clone()), WorkbenchEvent::FileSaved(other.clone())]);
|
||||
assert_eq!(f.host.info_of("test.path-events").unwrap().stats.activations, activations);
|
||||
deliver(vec![WorkbenchEvent::FileOpened(matched.clone()), WorkbenchEvent::FileSaved(matched.clone())]);
|
||||
assert_eq!(f.run("test.path-events", "count", None).unwrap().as_deref(), Some("2"));
|
||||
assert!(f.run("test.path-events", "filter", Some("../*")).is_err());
|
||||
deliver(vec![WorkbenchEvent::FileOpened(matched.clone())]);
|
||||
assert_eq!(
|
||||
f.run("test.path-events", "count", None).unwrap().as_deref(),
|
||||
Some("3"),
|
||||
"invalid filter preserves previous one"
|
||||
);
|
||||
|
||||
f.host.post_events(Some("main"), vec![WorkbenchEvent::FileOpened(matched.clone())]);
|
||||
f.run("test.path-events", "filter", None).unwrap();
|
||||
f.host.flush_events_at(Instant::now() + Duration::from_secs(3));
|
||||
assert!(f.host.wait_events(Duration::from_secs(10)));
|
||||
assert_eq!(
|
||||
f.run("test.path-events", "count", None).unwrap().as_deref(),
|
||||
Some("3"),
|
||||
"queued event uses current filter"
|
||||
);
|
||||
deliver(vec![WorkbenchEvent::FileOpened(matched.clone())]);
|
||||
assert_eq!(f.run("test.path-events", "count", None).unwrap().as_deref(), Some("3"));
|
||||
f.run("test.path-events", "all", None).unwrap();
|
||||
deliver(vec![WorkbenchEvent::FileOpened(other.clone())]);
|
||||
assert_eq!(f.run("test.path-events", "count", None).unwrap().as_deref(), Some("4"));
|
||||
f.host.post_events(Some("main"), vec![WorkbenchEvent::FileOpened(other.clone())]);
|
||||
f.run("test.path-events", "off", None).unwrap();
|
||||
f.host.flush_events_at(Instant::now() + Duration::from_secs(3));
|
||||
assert!(f.host.wait_events(Duration::from_secs(10)));
|
||||
deliver(vec![WorkbenchEvent::FileOpened(other)]);
|
||||
assert_eq!(f.run("test.path-events", "count", None).unwrap().as_deref(), Some("4"));
|
||||
|
||||
f.host.set_enabled("test.path-events", false).unwrap();
|
||||
assert!(!f.host.event_kinds().contains(&WorkbenchEvent::FileOpened(String::new()).kind()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_lookup_works_in_commands_and_provider_calls_without_an_invocation_project() {
|
||||
let f = fixture(Limits::default());
|
||||
f.install(&sample("path-events"));
|
||||
let path = f.project.join("src/notes.txt");
|
||||
assert_eq!(f.run("test.path-events", "project", Some(&to_ipc(&path))).unwrap(), Some(to_ipc(&f.project)));
|
||||
assert_eq!(f.run("test.path-events", "project", Some("src/notes.txt")).unwrap(), None);
|
||||
assert_eq!(
|
||||
f.run("test.path-events", "project", Some(&to_ipc(&f.project.with_extension("other").join("a")))).unwrap(),
|
||||
None
|
||||
);
|
||||
let doc = ide_plugin::ProviderDocument { path, language: "text".into(), version: 1, text: "one".into() };
|
||||
let hovers = f.host.provide_hover(&doc, ide_core::Pos::new(0, 0));
|
||||
assert_eq!(hovers.len(), 1);
|
||||
assert_eq!(hovers[0].markdown, to_ipc(&f.project));
|
||||
let nested = f.project.join("nested");
|
||||
std::fs::create_dir(&nested).unwrap();
|
||||
f.workspace.other_roots.lock().push(nested.clone());
|
||||
assert_eq!(
|
||||
f.run("test.path-events", "project", Some(&to_ipc(&nested.join("new.txt")))).unwrap(),
|
||||
Some(to_ipc(&nested))
|
||||
);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
std::os::unix::fs::symlink(&nested, f.project.join("alias")).unwrap();
|
||||
assert_eq!(
|
||||
f.run("test.path-events", "project", Some(&to_ipc(&f.project.join("alias/new.txt")))).unwrap(),
|
||||
Some(to_ipc(&nested))
|
||||
);
|
||||
std::os::unix::fs::symlink(f._tmp.path(), f.project.join("outside")).unwrap();
|
||||
assert_eq!(
|
||||
f.run("test.path-events", "project", Some(&to_ipc(&f.project.join("outside/new.txt")))).unwrap(),
|
||||
None
|
||||
);
|
||||
}
|
||||
f.host.set_capability("test.path-events", "project.read", false).unwrap();
|
||||
assert!(f.run("test.path-events", "project", Some(&to_ipc(&doc.path))).unwrap_err().contains("project.read"));
|
||||
f.host.set_capability("test.path-events", "events", false).unwrap();
|
||||
assert!(f.run("test.path-events", "filter", Some("*")).unwrap_err().contains("events"));
|
||||
}
|
||||
@@ -20,6 +20,6 @@ pub use runtime::{
|
||||
API_VERSION, CANCELLED, CallCancel, CallError, GuestError, HostApi, PluginEngine, Runtime, RuntimeSetup, check_api,
|
||||
component_api, host_api,
|
||||
};
|
||||
pub use sandbox::{MAX_READ_BYTES, inside_root, read_text, resolve_in_root, resolve_new_in_root};
|
||||
pub use sandbox::{MAX_READ_BYTES, inside_root, read_text, resolve_in_root, resolve_new_in_root, resolve_real};
|
||||
pub use types::*;
|
||||
pub use wasmtime::component::Component;
|
||||
@@ -110,6 +110,17 @@ pub trait HostApi: Send + Sync {
|
||||
}
|
||||
/// Roots of the open projects.
|
||||
fn projects(&self) -> Vec<std::path::PathBuf>;
|
||||
/// The most specific open project containing a document (including a new file).
|
||||
fn project_of(&self, path: &Path) -> Option<std::path::PathBuf> {
|
||||
if !path.is_absolute() {
|
||||
return None;
|
||||
}
|
||||
let real = crate::resolve_real(path);
|
||||
self.projects()
|
||||
.into_iter()
|
||||
.filter(|root| real.starts_with(crate::resolve_real(root)))
|
||||
.max_by_key(|root| root.components().count())
|
||||
}
|
||||
/// Unsaved text of an open editor's file.
|
||||
fn buffer_text(&self, path: &Path) -> Option<String>;
|
||||
/// Version and text of the open document at `path`.
|
||||
@@ -166,7 +177,11 @@ pub trait HostApi: Send + Sync {
|
||||
cancel: &CancellationToken,
|
||||
timeout: Duration,
|
||||
) -> Result<ProcessOutput, HostError>;
|
||||
fn subscribe(&self, kind: EventKind, on: bool);
|
||||
/// 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`.
|
||||
fn subscribe_paths(&self, kind: &str, globs: Vec<String>) -> Result<(), HostError>;
|
||||
fn unsubscribe(&self, kind: &str);
|
||||
/// Starts a progress indicator; cancelling it from the UI cancels `cancel`.
|
||||
fn task_begin(&self, client: Option<&str>, title: String, cancellable: bool, cancel: CancellationToken) -> u64;
|
||||
fn task_report(&self, id: u64, done: u32, total: Option<u32>, message: Option<String>);
|
||||
|
||||
@@ -21,7 +21,7 @@ fn joined_path(root: &Path, path: &str) -> Result<(PathBuf, PathBuf), String> {
|
||||
/// the canonical path when it exists, else the canonicalized deepest existing
|
||||
/// ancestor with the still-missing tail resolved textually — so the location
|
||||
/// of a missing path is still judged where it would really land.
|
||||
fn resolve_real(joined: &Path) -> PathBuf {
|
||||
pub fn resolve_real(joined: &Path) -> PathBuf {
|
||||
let mut tail = Vec::new();
|
||||
let mut candidate = joined.to_path_buf();
|
||||
let mut real = loop {
|
||||
|
||||
@@ -460,6 +460,10 @@ impl workspace::Host for StoreData {
|
||||
Ok(self.host.open_project(client.as_deref(), path, new_window).map_err(to_wit))
|
||||
}
|
||||
|
||||
fn project_of(&mut self, path: String) -> wasmtime::Result<Option<Resource<ProjectRes>>> {
|
||||
self.host.project_of(Path::new(&path)).map(|root| self.push(ProjectRes { root })).transpose()
|
||||
}
|
||||
|
||||
fn projects(&mut self) -> Fallible<Vec<Resource<ProjectRes>>> {
|
||||
if let Err(e) = self.require(Capability::ProjectRead) {
|
||||
return Ok(Err(e));
|
||||
@@ -758,14 +762,19 @@ impl events::Host for StoreData {
|
||||
{
|
||||
return Ok(Err(e));
|
||||
}
|
||||
self.host.subscribe(kind, true);
|
||||
self.host.subscribe(kind);
|
||||
Ok(Ok(()))
|
||||
}
|
||||
|
||||
fn unsubscribe(&mut self, kind: String) -> wasmtime::Result<()> {
|
||||
if let Some(kind) = EventKind::parse(&kind) {
|
||||
self.host.subscribe(kind, false);
|
||||
fn subscribe_paths(&mut self, kind: String, globs: Vec<String>) -> Fallible<()> {
|
||||
if let Err(e) = self.require(Capability::Events) {
|
||||
return Ok(Err(e));
|
||||
}
|
||||
Ok(self.host.subscribe_paths(&kind, globs).map_err(to_wit))
|
||||
}
|
||||
|
||||
fn unsubscribe(&mut self, kind: String) -> wasmtime::Result<()> {
|
||||
self.host.unsubscribe(&kind);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
f81a6b838d4bee81a058f79436a012f75f55a0c80ad1f10f0af7bd2aba9c32c6
|
||||
627be971ecc31efd268258eff214ed08c671c2a158677b773de9d5be462b17c4
|
||||
@@ -1 +1 @@
|
||||
344660862897f17d7b8f7cdb1397f715201abeab3fc8d7ba1b62c82ca0d9e60b
|
||||
56683bb1e102025801610a2d61ce393f03bde9a3e5b5e52dde2ff272b3f10662
|
||||
@@ -1 +1 @@
|
||||
47c52d929c61fc852f232c0ba5eb798bbdbc9497797cd7f454b32969779d669b
|
||||
d6a10bd1a97e7da44338fc9a28f5a64054c9269f553003298f816bb114f42a88
|
||||
@@ -1 +1 @@
|
||||
c8b3c8b074c765a81f8d1096ffe36cd8b93999096b9b77959a4d469e5f6e25f6
|
||||
21d7a4cf9348568b7cb2b565c6e26a727165319bd685147fcedd3a8465565990
|
||||
@@ -1 +1 @@
|
||||
58090f3c530ce4e736b7d182e73d1eaaee7f8db65f76eed0481a00f50828896a
|
||||
636846f46f27ef901fde5c8767991fdc1060c86d7f90a12b0cb08933a2e60b37
|
||||
+1
-1
@@ -1 +1 @@
|
||||
56b93b6a70821331e1bc835b64b4273d7a2989c66b6623e6a8d097464375e9b3
|
||||
a35dfaecf8d76d3ca95c80e810be26781671f58a48b50374e2656cdd6959cdf2
|
||||
@@ -1 +1 @@
|
||||
ec5a77e097fea4962ffa8227c0bfb247e61d360c5da983c7dabfc84159bf457a
|
||||
a8b92a0ee9b46d8d414457ff287d6092e85d4536e913d146aa49662814ff1836
|
||||
@@ -1 +1 @@
|
||||
89291c6203a966a5163358ffd64630874d014fe239d7e9747890ebbd8f686540
|
||||
2fa3d6146a99eee79d4aec12bf8777369ae5cc76c3f7d6a8c254d5a27d58eeb9
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
# 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 = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openidea-test-path-events"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"openidea-plugin",
|
||||
]
|
||||
|
||||
[[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",
|
||||
]
|
||||
|
||||
[[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,20 @@
|
||||
[package]
|
||||
name = "openidea-test-path-events"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[workspace]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
openidea-plugin = { path = "../../../../../sdk/rust/openidea-plugin" }
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
Бінарний файл не відображається.
@@ -0,0 +1 @@
|
||||
c14d91010021e3b35f6ae29a2a327bb3ad66087f07cfd744814443862c0050cc
|
||||
@@ -0,0 +1,34 @@
|
||||
id = "test.path-events"
|
||||
name = "Path event fixture"
|
||||
version = "0.1.0"
|
||||
engines.openidea = "^1.3"
|
||||
wasm = "dist/plugin.wasm"
|
||||
activation = ["on-command:*"]
|
||||
|
||||
[capabilities]
|
||||
required = ["storage"]
|
||||
optional = ["events", "project.read"]
|
||||
|
||||
[[contributes.providers]]
|
||||
kinds = ["hover"]
|
||||
languages = ["text", "plaintext"]
|
||||
|
||||
[[contributes.commands]]
|
||||
id = "project"
|
||||
title = "project"
|
||||
|
||||
[[contributes.commands]]
|
||||
id = "filter"
|
||||
title = "filter"
|
||||
|
||||
[[contributes.commands]]
|
||||
id = "all"
|
||||
title = "all"
|
||||
|
||||
[[contributes.commands]]
|
||||
id = "off"
|
||||
title = "off"
|
||||
|
||||
[[contributes.commands]]
|
||||
id = "count"
|
||||
title = "count"
|
||||
@@ -0,0 +1,47 @@
|
||||
use openidea_plugin::hover::{Document, Hover, HoverProvider};
|
||||
use openidea_plugin::{Context, Error, Event, EventKind, Invocation, Plugin, Position, host, register_plugin};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Fixture;
|
||||
|
||||
impl Plugin for Fixture {
|
||||
fn activate(&mut self, _: &Context) -> Result<(), Error> {
|
||||
// Persist the chosen filter so reactivation does not overwrite it.
|
||||
let glob = host::storage::get("filter")?.unwrap_or_else(|| "config/*.yaml".into());
|
||||
let globs: Vec<_> = glob.lines().collect();
|
||||
let _ = host::subscribe_paths("file-opened", &globs);
|
||||
let _ = host::subscribe_paths("file-saved", &["config/*.yaml"]);
|
||||
Ok(())
|
||||
}
|
||||
fn run_command(&mut self, id: &str, _: &Invocation, arg: Option<&str>) -> Result<Option<String>, Error> {
|
||||
match id {
|
||||
"project" => host::project_of(arg.unwrap_or_default()).map(|p| p.root()).transpose(),
|
||||
"filter" => {
|
||||
let glob = arg.unwrap_or_default();
|
||||
host::subscribe_paths("file-opened", &glob.lines().collect::<Vec<_>>())?;
|
||||
host::storage::set("filter", glob)?;
|
||||
Ok(None)
|
||||
}
|
||||
"all" => {
|
||||
host::subscribe(EventKind::FileOpened)?;
|
||||
Ok(None)
|
||||
}
|
||||
"off" => {
|
||||
host::unsubscribe(EventKind::FileOpened);
|
||||
Ok(None)
|
||||
}
|
||||
"count" => Ok(host::storage::get("count")?),
|
||||
_ => Err(Error::NotFound(id.into())),
|
||||
}
|
||||
}
|
||||
fn handle_events(&mut self, events: &[Event], _: &Invocation) {
|
||||
let count = host::storage::get("count").ok().flatten().and_then(|n| n.parse::<usize>().ok()).unwrap_or(0);
|
||||
host::storage::set("count", &(count + events.len()).to_string()).unwrap();
|
||||
}
|
||||
}
|
||||
impl HoverProvider for Fixture {
|
||||
fn hover(&mut self, doc: &Document, _: Position) -> Option<Hover> {
|
||||
Some(Hover::new(host::project_of(&doc.path)?.root().ok()?))
|
||||
}
|
||||
}
|
||||
register_plugin!(Fixture, hover);
|
||||
+1
-1
@@ -1 +1 @@
|
||||
284efc25ade647bd1944024a04a944d63cbc69d8647cc9eff95eb4add0d8509c
|
||||
e9b6c10973ce62057eb3c92a747bcb27fdef1cfb6dd1d8681aa0035d3a3b5fa2
|
||||
@@ -1 +1 @@
|
||||
243ac921ab396b2fc2735bb8552394d90246080264143f1205c59f184db4b504
|
||||
a873a709240a3d24c45481b220ac283103f10645166952cb5dcc809f7995e085
|
||||
@@ -1 +1 @@
|
||||
4d753f6d72e25afee529684219a0c69fcadd33c53a377d503343fb6532ac8979
|
||||
8801a468856e0e4783848f89d51d6d6f45426e6cadf2b2532518500ff23aa6f7
|
||||
@@ -419,6 +419,27 @@ a new API: an IDE refuses a kind it doesn't know with `invalid`, and a kind
|
||||
newer than the plugin's SDK arrives as `Event::Other { kind, data }` (JSON).
|
||||
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.
|
||||
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.
|
||||
|
||||
Register filters in `activate`, using a startup or command activation to install
|
||||
them before observing events. Patterns are ORed; another call replaces the same
|
||||
kind's filter, an empty list pauses it, `subscribe(kind)` restores all paths,
|
||||
and `unsubscribe(kind)` removes it. Filters survive idle reclaim and disappear
|
||||
on disable, uninstall or reload. Each window's events use its current project;
|
||||
files outside that project do not match.
|
||||
|
||||
Provider calls can use `host::project_of(&doc.path)` (`workspace.project-of`)
|
||||
without an invocation project or enumerating every open project. It returns the
|
||||
deepest open project containing the absolute path, or `None` outside open roots.
|
||||
Symlinks and missing file tails are resolved before checking containment.
|
||||
Obtaining a handle grants no read access: `Project::root` and file operations
|
||||
still require `project.read`.
|
||||
|
||||
### Commits
|
||||
|
||||
A plugin can work on the commit the user is preparing, in the Commit tool
|
||||
|
||||
@@ -1 +1 @@
|
||||
10030cb0dc2733d70cd740e717da9f9feebab18e79ac712ebb5bd03c6d40b316
|
||||
d46e07036f0f59c18ca54affbca6446c320476998f3e92199cd93ed43dd725d9
|
||||
@@ -1 +1 @@
|
||||
9c1afe80f5c8062fa4abd05d3d1cdf73d4ac07b8aa46b329dcbaf6b29d5ec5cf
|
||||
c28b0da5a21fc0fc5dd543d01eaeeed4f7bbd89a5d28b95ce911a1c092209adc
|
||||
@@ -1 +1 @@
|
||||
2e0e8d50faede7a1b2ed077de83699adcf519fcbf78d371d01a79ce570c1b85a
|
||||
f4258172c7a8278d00f5da9d76420380200d8b0d8bc387ceafab30703cbdac4a
|
||||
@@ -1 +1 @@
|
||||
c145616a7c143fee66e9808f998577114bfb31effb089cc9e6c843c0d8078696
|
||||
648b030335ecfdf82651561fc5e71edc9f9d4319ae4bfb56d1e6d7e1f029d781
|
||||
@@ -1 +1 @@
|
||||
65517af4c466e7b470b3d06f6ff67fb27d94bb008184ef1709ba68b8183ee892
|
||||
f7bbe6097f18377a79f39ba2369140e24eff65501e78be94e3420c6667362743
|
||||
@@ -1 +1 @@
|
||||
d5a3ff93ed282f7c183967113c38d323b332897b1a88d6cd69d1c2324a679852
|
||||
936d6e2c74f87cc1d95c52ea674716d934df917c12fb8431989f8dc64989b174
|
||||
@@ -1 +1 @@
|
||||
d2d1cbe6bebead67613348c467a75dddfa7c4c0a22ce2f97caae9590d7f84809
|
||||
182af35d880a9adfa2566a42b62db6cf69f7d2a5157e0428228b649864e9417c
|
||||
@@ -1 +1 @@
|
||||
a75d13d6d02ff377046aa95a8be642cd8ba3da6d07d03d5730c97bfb5582164a
|
||||
f750b060f9a4217da81395aaa9875811d61d087e8d7fef1a0d7b46e3d08cd4a8
|
||||
@@ -1 +1 @@
|
||||
6a6bc97f4d23e20f8605acc801d2722162867a5ff61d2295f7447702e26c4268
|
||||
4edc8aad4feb31205d567e303a18f9d0d4969bc65dc8f3699b46cd05b93d8868
|
||||
@@ -1 +1 @@
|
||||
15145ecb6e490604e7184581427c8a6b44af7d6030738b0f9d5eb5ce44dbd949
|
||||
e1e58cd1ee112923dfc1debe86f8feb18c1a4e9f58490b5e371ee0272cad6ced
|
||||
@@ -1 +1 @@
|
||||
11b90fde669f51cdc9aac4722d4d0e4bc0c9dbddf2497dff5e67f02a9fa2ccbb
|
||||
318e264055acfd1d10491dfea784fdb2a034d3b4da8f7583e81720e21ea1947c
|
||||
@@ -182,6 +182,10 @@ mod imp {
|
||||
wit::workspace::projects().map_err(error)
|
||||
}
|
||||
|
||||
pub fn project_of(path: &str) -> Option<ProjectRes> {
|
||||
wit::workspace::project_of(path)
|
||||
}
|
||||
|
||||
pub fn project_root(p: &ProjectRes) -> Result<String, Error> {
|
||||
p.root().map_err(error)
|
||||
}
|
||||
@@ -278,6 +282,10 @@ mod imp {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe_paths(kind: &str, globs: &[&str]) -> Result<(), Error> {
|
||||
wit::events::subscribe_paths(kind, &globs.iter().map(|g| (*g).to_owned()).collect::<Vec<_>>()).map_err(error)
|
||||
}
|
||||
|
||||
pub fn subscribe(k: EventKind) -> Result<(), Error> {
|
||||
wit::events::subscribe(k.name()).map_err(error)
|
||||
}
|
||||
@@ -621,6 +629,12 @@ pub fn projects() -> Result<Vec<Project>, Error> {
|
||||
Ok(imp::projects()?.into_iter().map(|inner| Project { inner }).collect())
|
||||
}
|
||||
|
||||
/// The deepest open project containing an absolute document path, including in providers.
|
||||
/// Obtaining the handle needs no capability; reading it still needs `project.read`.
|
||||
pub fn project_of(path: &str) -> Option<Project> {
|
||||
imp::project_of(path).map(|inner| Project { inner })
|
||||
}
|
||||
|
||||
/// A file open in an editor.
|
||||
pub struct TextEditor {
|
||||
inner: imp::EditorRes,
|
||||
@@ -790,6 +804,13 @@ pub fn locale() -> String {
|
||||
imp::locale()
|
||||
}
|
||||
|
||||
/// Subscribe to project-relative path globs for file-opened or file-saved.
|
||||
/// Replaces the previous filter. Empty globs pause delivery; `subscribe` restores all paths.
|
||||
/// Requires `events`. Register during activation before the events you want to observe.
|
||||
pub fn subscribe_paths(kind: &str, globs: &[&str]) -> Result<(), Error> {
|
||||
imp::subscribe_paths(kind, globs)
|
||||
}
|
||||
|
||||
/// Delivers `kind` events to [`crate::Plugin::handle_events`] for the rest of
|
||||
/// the session (also after the instance is reclaimed: an event activates the
|
||||
/// plugin again). Needs `events` (`events.input` for `ActionPerformed`).
|
||||
|
||||
@@ -72,6 +72,7 @@ struct State {
|
||||
context: BTreeMap<String, ContextValue>,
|
||||
storage: BTreeMap<String, String>,
|
||||
subscriptions: BTreeSet<EventKind>,
|
||||
path_subscriptions: BTreeMap<String, Vec<String>>,
|
||||
tasks: Vec<MockTask>,
|
||||
vcs: Option<RepositoryStatus>,
|
||||
remotes: Vec<(String, String)>,
|
||||
@@ -230,6 +231,10 @@ impl MockHost {
|
||||
self.state.borrow().storage.clone()
|
||||
}
|
||||
|
||||
pub fn path_subscriptions(&self) -> BTreeMap<String, Vec<String>> {
|
||||
self.state.borrow().path_subscriptions.clone()
|
||||
}
|
||||
|
||||
pub fn subscriptions(&self) -> BTreeSet<EventKind> {
|
||||
self.state.borrow().subscriptions.clone()
|
||||
}
|
||||
@@ -613,6 +618,27 @@ pub mod current {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn project_of(path: &str) -> Option<ProjectRes> {
|
||||
let path = path.replace('\\', "/");
|
||||
let absolute =
|
||||
path.starts_with('/') || (path.as_bytes().get(1) == Some(&b':') && path.as_bytes().get(2) == Some(&b'/'));
|
||||
if !absolute || path.split('/').any(|part| part == "..") {
|
||||
return None;
|
||||
}
|
||||
with(|s| {
|
||||
s.root
|
||||
.iter()
|
||||
.chain(&s.other_projects)
|
||||
.filter(|root| {
|
||||
let root = root.replace('\\', "/");
|
||||
path == root
|
||||
|| path.strip_prefix(root.trim_end_matches('/')).is_some_and(|rest| rest.starts_with('/'))
|
||||
})
|
||||
.max_by_key(|root| root.replace('\\', "/").split('/').count())
|
||||
.map(|root| MockProject { root: root.clone() })
|
||||
})
|
||||
}
|
||||
|
||||
pub fn project_root(p: &ProjectRes) -> Result<String, Error> {
|
||||
with(|s| {
|
||||
require(s, "project.read")?;
|
||||
@@ -797,6 +823,38 @@ pub mod current {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn subscribe_paths(kind: &str, globs: &[&str]) -> Result<(), Error> {
|
||||
with(|s| {
|
||||
require(s, "events")?;
|
||||
if !matches!(kind, "file-opened" | "file-saved")
|
||||
|| globs.len() > 64
|
||||
|| globs.iter().any(|g| {
|
||||
let relative = g.trim_start_matches("./");
|
||||
relative.is_empty()
|
||||
|| g.len() > 1024
|
||||
|| relative.starts_with('/')
|
||||
|| g.contains('\\')
|
||||
|| g.contains(':')
|
||||
|| g.chars().any(char::is_control)
|
||||
|| g.split('/').any(|p| p == "..")
|
||||
})
|
||||
{
|
||||
return Err(Error::Invalid("invalid event path filter".into()));
|
||||
}
|
||||
match kind {
|
||||
"file-opened" => {
|
||||
s.subscriptions.insert(EventKind::FileOpened);
|
||||
}
|
||||
"file-saved" => {
|
||||
s.subscriptions.insert(EventKind::FileSaved);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
s.path_subscriptions.insert(kind.to_owned(), globs.iter().map(|g| (*g).to_owned()).collect());
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn subscribe(kind: EventKind) -> Result<(), Error> {
|
||||
with(|s| {
|
||||
let cap = match kind {
|
||||
@@ -807,13 +865,17 @@ pub mod current {
|
||||
if let Some(cap) = cap {
|
||||
require(s, cap)?;
|
||||
}
|
||||
s.path_subscriptions.remove(kind.name());
|
||||
s.subscriptions.insert(kind);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn unsubscribe(kind: EventKind) {
|
||||
with(|s| s.subscriptions.remove(&kind));
|
||||
with(|s| {
|
||||
s.path_subscriptions.remove(kind.name());
|
||||
s.subscriptions.remove(&kind);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn project_find(p: &ProjectRes, glob: &str, limit: u32) -> Result<Vec<String>, Error> {
|
||||
@@ -965,6 +1027,38 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::host;
|
||||
|
||||
#[test]
|
||||
fn path_subscriptions_replace_pause_and_restore() {
|
||||
let mock = MockHost::install();
|
||||
host::subscribe_paths("file-opened", &["config/*.yaml"]).unwrap();
|
||||
assert_eq!(mock.path_subscriptions()["file-opened"], ["config/*.yaml"]);
|
||||
host::subscribe_paths("file-opened", &[]).unwrap();
|
||||
assert!(mock.path_subscriptions()["file-opened"].is_empty());
|
||||
assert!(host::subscribe_paths("file-opened", &["../secret"]).is_err());
|
||||
host::subscribe(EventKind::FileOpened).unwrap();
|
||||
assert!(mock.path_subscriptions().is_empty());
|
||||
host::subscribe_paths("file-saved", &["**/*.rs"]).unwrap();
|
||||
host::unsubscribe(EventKind::FileSaved);
|
||||
assert!(mock.path_subscriptions().is_empty());
|
||||
let _denied = MockHost::install().with_capabilities(&[]);
|
||||
assert!(matches!(host::subscribe_paths("file-opened", &[]), Err(Error::Denied(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_lookup_uses_deepest_root_without_granting_read_access() {
|
||||
let mock = MockHost::install();
|
||||
mock.add_project("/project/nested");
|
||||
assert_eq!(host::project_of("/project/nested/new.txt").unwrap().root().unwrap(), "/project/nested");
|
||||
assert!(host::project_of("/project-other/file").is_none());
|
||||
assert!(host::project_of("relative/file").is_none());
|
||||
mock.add_project("C:/workspace");
|
||||
assert_eq!(host::project_of(r"C:\workspace\file.txt").unwrap().root().unwrap(), "C:/workspace");
|
||||
mock.add_project("//server/share");
|
||||
assert_eq!(host::project_of(r"\\server\share\file.txt").unwrap().root().unwrap(), "//server/share");
|
||||
let _denied = MockHost::install().with_capabilities(&[]);
|
||||
assert!(matches!(host::project_of("/project/file").unwrap().root(), Err(Error::Denied(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invocations_carry_selected_paths() {
|
||||
let host = MockHost::install();
|
||||
|
||||
@@ -36,6 +36,10 @@ interface events {
|
||||
/// and an IDE that doesn't know one fails with `invalid`.
|
||||
/// Requires `events` (`events.input` for `action-performed`).
|
||||
subscribe: func(kind: string) -> result<_, error>;
|
||||
/// Replaces this kind's subscription with project-relative globs (OR).
|
||||
/// Empty globs pause delivery; `subscribe` restores unfiltered delivery.
|
||||
/// Kinds: file-opened, file-saved. Requires `events`.
|
||||
subscribe-paths: func(kind: string, globs: list<string>) -> result<_, error>;
|
||||
unsubscribe: func(kind: string);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@ interface workspace {
|
||||
/// `path` is an absolute directory path on the UI machine.
|
||||
open-project: func(path: string, new-window: bool) -> result<_, error>;
|
||||
|
||||
/// The open project containing an absolute document path, or `none`.
|
||||
/// Reading the returned project still requires `project.read`.
|
||||
project-of: func(path: string) -> option<project>;
|
||||
|
||||
/// What a path inside a project is.
|
||||
enum file-kind {
|
||||
file,
|
||||
|
||||
@@ -36,6 +36,10 @@ interface events {
|
||||
/// and an IDE that doesn't know one fails with `invalid`.
|
||||
/// Requires `events` (`events.input` for `action-performed`).
|
||||
subscribe: func(kind: string) -> result<_, error>;
|
||||
/// Replaces this kind's subscription with project-relative globs (OR).
|
||||
/// Empty globs pause delivery; `subscribe` restores unfiltered delivery.
|
||||
/// Kinds: file-opened, file-saved. Requires `events`.
|
||||
subscribe-paths: func(kind: string, globs: list<string>) -> result<_, error>;
|
||||
unsubscribe: func(kind: string);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@ interface workspace {
|
||||
/// `path` is an absolute directory path on the UI machine.
|
||||
open-project: func(path: string, new-window: bool) -> result<_, error>;
|
||||
|
||||
/// The open project containing an absolute document path, or `none`.
|
||||
/// Reading the returned project still requires `project.read`.
|
||||
project-of: func(path: string) -> option<project>;
|
||||
|
||||
/// What a path inside a project is.
|
||||
enum file-kind {
|
||||
file,
|
||||
|
||||
Reference in new issue
Заблокувати користувача