diff --git a/crates/ide-host/src/plugins/api.rs b/crates/ide-host/src/plugins/api.rs index 31b19d560..0b2824730 100644 --- a/crates/ide-host/src/plugins/api.rs +++ b/crates/ide-host/src/plugins/api.rs @@ -437,8 +437,18 @@ pub(super) struct Shared { pub ui: Arc, /// The UI language the windows report (BCP 47). pub locale: RwLock, + /// (plugin, window) → the log tabs its code opened there, oldest first: + /// what a tab may run when it starts. + log_tabs: Mutex>, } +/// A window's log tabs of one plugin: key → what it runs, oldest first. +type LogTabs = Vec<(String, ide_plugin::ServiceLog)>; + +/// Log tabs a plugin's code may have open in one window; opening more forgets +/// the oldest (that tab can no longer start its program). +const MAX_LOG_TABS: usize = 32; + impl Shared { pub fn new( sink: Arc, @@ -467,6 +477,7 @@ impl Shared { closed_clients: Mutex::default(), subscriptions: Mutex::default(), diagnostics: Mutex::default(), + log_tabs: Mutex::default(), }) } @@ -565,6 +576,20 @@ impl Shared { self.closed_clients.lock().remove(client); } + /// What the log tab `key` that `plugin`'s code opened in `window` runs. + pub fn log_tab(&self, plugin: &str, window: &str, key: &str) -> Option { + let tabs = self.log_tabs.lock(); + let tabs = tabs.get(&(plugin.to_owned(), window.to_owned()))?; + tabs.iter().find(|(k, _)| k == key).map(|(_, log)| log.clone()) + } + + /// Forgets the log tabs of a closed window, or of a stopped plugin. + pub fn forget_log_tabs(&self, plugin: Option<&str>, window: Option<&str>) { + self.log_tabs + .lock() + .retain(|(p, w), _| plugin.is_some_and(|plugin| plugin != p) || window.is_some_and(|window| window != w)); + } + pub fn client_closed(&self, client: &str) -> bool { self.closed_clients.lock().contains(client) } @@ -1777,6 +1802,29 @@ impl HostApi for PluginApi { Ok(()) } + fn open_log_tab( + &self, + client: &str, + key: String, + title: String, + log: ide_plugin::ServiceLog, + ) -> Result<(), HostError> { + // Fails now, rather than in the tab, when the approved file is gone or changed. + self.approved_program(&log.program)?; + { + let mut all = self.shared.log_tabs.lock(); + let tabs = all.entry((self.id().to_owned(), client.to_owned())).or_default(); + tabs.retain(|(k, _)| *k != key); + tabs.push((key.clone(), log.clone())); + if tabs.len() > MAX_LOG_TABS { + tabs.remove(0); + } + } + let event = PluginEvent::OpenLogTab { plugin_id: self.id().to_owned(), key, title, log }; + self.shared.sink.emit(Some(client), event); + Ok(()) + } + fn locale(&self) -> String { self.shared.locale.read().clone() } diff --git a/crates/ide-host/src/plugins/remote_host.rs b/crates/ide-host/src/plugins/remote_host.rs index be1a6b117..74ee904dd 100644 --- a/crates/ide-host/src/plugins/remote_host.rs +++ b/crates/ide-host/src/plugins/remote_host.rs @@ -118,6 +118,7 @@ pub fn forwarded(event: &PluginEvent) -> bool { | PluginEvent::ViewMessage { .. } | PluginEvent::OpenTerminal { .. } | PluginEvent::OpenLocation { .. } + | PluginEvent::OpenLogTab { .. } | PluginEvent::CopyToClipboard { .. } | PluginEvent::OpenUrl { .. } | PluginEvent::OpenProject { .. } diff --git a/crates/ide-host/src/plugins/types.rs b/crates/ide-host/src/plugins/types.rs index 639babe9e..df58404f3 100644 --- a/crates/ide-host/src/plugins/types.rs +++ b/crates/ide-host/src/plugins/types.rs @@ -224,6 +224,10 @@ pub enum PluginEvent { }, #[serde(rename_all = "camelCase")] OpenLocation { plugin_id: String, path: ide_core::IpcPath, line: u32, column: u32 }, + /// Open (or focus) the log tab `key` following `log` (`views.open-log-tab`); + /// it starts the program with `plugin_log_tab_spawn`. + #[serde(rename_all = "camelCase")] + OpenLogTab { plugin_id: String, key: String, title: String, log: ide_plugin::ServiceLog }, /// Plugin languages, or the servers they may start, changed: re-fetch /// `languages` (the host re-detects open documents: [`crate::Host::plugin_languages_changed`]). LanguagesChanged, diff --git a/crates/ide-host/src/plugins/web.rs b/crates/ide-host/src/plugins/web.rs index eb67a1a98..d3d9514d8 100644 --- a/crates/ide-host/src/plugins/web.rs +++ b/crates/ide-host/src/plugins/web.rs @@ -134,13 +134,47 @@ impl PluginHost { window: &str, command: WebPtyCommand, output: crate::ops::Emit, + ) -> IdeResult { + self.spawn_plugin_pty(host, plugin_id, window, output, || { + self.web_pty_options(plugin_id, view, window, command) + }) + } + + /// Starts the program of the log tab `key` that the plugin's code opened + /// in `window` (`views.open-log-tab`), with the toggles in `on`: the UI + /// names the tab, never what runs. + #[allow(clippy::too_many_arguments)] + pub fn spawn_log_tab_pty( + &self, + host: &std::sync::Arc, + plugin_id: &str, + key: &str, + window: &str, + on: &[String], + (cols, rows): (u16, u16), + output: crate::ops::Emit, + ) -> IdeResult { + self.spawn_plugin_pty(host, plugin_id, window, output, || { + self.log_tab_pty_options(plugin_id, key, window, on, (cols, rows)) + }) + } + + /// The PTY of a plugin, counted against its sessions and ended with its + /// window, project or permission; `options` authorizes it. + fn spawn_plugin_pty( + &self, + host: &std::sync::Arc, + plugin_id: &str, + window: &str, + output: crate::ops::Emit, + options: impl FnOnce() -> IdeResult, ) -> IdeResult { let _guard = self.lifecycle.lock(); let mut sessions = self.web_ptys.lock(); if self.shared.client_closed(window) { return Err(IdeError::cancelled()); } - let options = self.web_pty_options(plugin_id, view, window, command)?; + let options = options()?; let entries = sessions.entry(plugin_id.to_owned()).or_default(); entries.retain(|s| s.host.upgrade().is_some_and(|h| h.terminals.contains(s.id))); if entries.len() >= 8 { @@ -166,6 +200,7 @@ impl PluginHost { } pub(super) fn forget_web_ptys(&self, window: &str) { + self.shared.forget_log_tabs(None, Some(window)); for sessions in self.web_ptys.lock().values_mut() { sessions.retain(|session| { if session.window != window { @@ -180,6 +215,7 @@ impl PluginHost { } pub(super) fn stop_web_ptys(&self, plugin: &str) { + self.shared.forget_log_tabs(Some(plugin), None); if let Some(sessions) = self.web_ptys.lock().remove(plugin) { for session in sessions { if let Some(host) = session.host.upgrade() { @@ -198,8 +234,37 @@ impl PluginHost { command: WebPtyCommand, ) -> IdeResult { self.web_page(plugin_id, view)?; + self.get(plugin_id)?.pkg.manifest.needs_api("web view PTYs", 3).map_err(IdeError::invalid)?; + self.pty_options(plugin_id, window, command) + } + + /// The options of the log tab `key`'s program, with the toggles in `on`. + pub fn log_tab_pty_options( + &self, + plugin_id: &str, + key: &str, + window: &str, + on: &[String], + (cols, rows): (u16, u16), + ) -> IdeResult { + let log = self.shared.log_tab(plugin_id, window, key).ok_or_else(|| { + IdeError::not_found(format!("plugin {plugin_id:?} opened no log tab {key:?} in this window")) + })?; + let toggled = log.toggles.into_iter().filter(|t| on.contains(&t.id)).flat_map(|t| t.args); + let args = log.args.into_iter().chain(toggled).collect(); + let command = WebPtyCommand { program: log.program, args, env: Default::default(), cols, rows }; + self.pty_options(plugin_id, window, command) + } + + /// Checks `command` against the plugin's current grant, the limits and + /// the window's trusted project. + fn pty_options( + &self, + plugin_id: &str, + window: &str, + command: WebPtyCommand, + ) -> IdeResult { let plugin = self.get(plugin_id)?; - plugin.pkg.manifest.needs_api("web view PTYs", 3).map_err(IdeError::invalid)?; let cap = ide_extension::Capability::Process(command.program.clone()); if !plugin.meta.lock().granted.contains(&cap) { return Err(IdeError::new( diff --git a/crates/ide-host/tests/plugins.rs b/crates/ide-host/tests/plugins.rs index 7af03ed04..432737d6f 100644 --- a/crates/ide-host/tests/plugins.rs +++ b/crates/ide-host/tests/plugins.rs @@ -5016,6 +5016,59 @@ fn web_log_tabs_use_the_web_pty_permission_boundary() { } } +/// `views.open-log-tab` from the plugin's code: the host remembers what the tab +/// may run, and the tab starts it by key, through the same PTY checks. +#[test] +fn log_tabs_opened_by_code_run_only_what_the_host_approved() { + let f = fixture(Limits::default()); + f.host.install_from_dir(&sample("web-log"), &Consent::default()).unwrap(); + let err = f.run("web-log", "code", None).unwrap_err(); + assert!(err.contains("process:bash"), "{err}"); + let options = |key: &str, window: &str, on: &[&str]| { + let on: Vec = on.iter().map(|s| s.to_string()).collect(); + f.host.log_tab_pty_options("web-log", key, window, &on, (240, 40)) + }; + assert_eq!(options("code", "main", &[]).unwrap_err().kind, ErrorKind::NotFound); + #[cfg(unix)] + { + f.host.set_capability("web-log", "process:bash", true).unwrap(); + f.take_events(); + assert_eq!(f.run("web-log", "code", None).unwrap(), None); + let events = f.take_events(); + let (client, log) = events + .iter() + .find_map(|(client, event)| match event { + PluginEvent::OpenLogTab { plugin_id, key, title, log } + if plugin_id == "web-log" && key == "code" && title == "Probe logs (code)" => + { + Some((client.clone(), log.clone())) + } + _ => None, + }) + .expect("an OpenLogTab event"); + assert_eq!(client.as_deref(), Some("main")); + assert_eq!((log.program.as_str(), log.toggles[0].id.as_str()), ("bash", "verbose")); + + let got = options("code", "main", &[]).unwrap(); + assert_eq!(got.shell.as_deref(), f.host.program_for("web-log", "bash").unwrap().to_str()); + assert_eq!(got.args, log.args); + assert_eq!(got.cwd.0, f.project); + assert!(got.env.is_empty() && !got.shell_integration); + // Toggles append their arguments; unknown ones add nothing. + let verbose = options("code", "main", &["verbose", "bogus"]).unwrap(); + assert_eq!(verbose.args.last().map(String::as_str), Some("--verbose")); + assert_eq!(verbose.args.len(), log.args.len() + 1); + // Only the window it was opened in, and only that key. + assert_eq!(options("code", "other", &[]).unwrap_err().kind, ErrorKind::NotFound); + assert_eq!(options("probe", "main", &[]).unwrap_err().kind, ErrorKind::NotFound); + // A revoked grant stops it starting, and forgets the tab. + f.host.set_capability("web-log", "process:bash", false).unwrap(); + assert!(options("code", "main", &[]).is_err()); + f.host.set_capability("web-log", "process:bash", true).unwrap(); + assert_eq!(options("code", "main", &[]).unwrap_err().kind, ErrorKind::NotFound); + } +} + #[test] fn web_ptys_require_a_current_exact_program_grant_and_project() { use ide_host::plugins::web::WebPtyCommand; diff --git a/crates/ide-plugin/src/runtime.rs b/crates/ide-plugin/src/runtime.rs index fab0b8ec5..28fa745a3 100644 --- a/crates/ide-plugin/src/runtime.rs +++ b/crates/ide-plugin/src/runtime.rs @@ -245,6 +245,9 @@ pub trait HostApi: Send + Sync { cwd: Option<&Path>, ) -> Result<(), HostError>; fn open_location(&self, client: Option<&str>, path: &Path, line: u32, column: u32) -> Result<(), HostError>; + /// Opens a log tab `key` in `client`'s window following `log` (already + /// checked against the plugin's grants and limits). + fn open_log_tab(&self, client: &str, key: String, title: String, log: ServiceLog) -> Result<(), HostError>; /// The IDE's UI language as a BCP 47 tag. fn locale(&self) -> String; /// A setting of the plugin as JSON (project over global over the schema default). diff --git a/crates/ide-plugin/src/wit_host.rs b/crates/ide-plugin/src/wit_host.rs index 09efa5cca..f1b8008c0 100644 --- a/crates/ide-plugin/src/wit_host.rs +++ b/crates/ide-plugin/src/wit_host.rs @@ -117,8 +117,8 @@ pub(crate) mod views_bindings { } use bindings::openidea::plugin::{ - commit, diagnostics, editor, events, fs, invocation, log, net, process, secrets, settings, storage, tasks, - types as wit, ui, vcs, views, workspace, + commit, detail_types, diagnostics, editor, events, fs, invocation, log, net, process, secrets, settings, storage, + tasks, types as wit, ui, vcs, views, workspace, }; /// A project handle: its root. @@ -661,6 +661,8 @@ impl workspace::HostProject for StoreData { impl editor::Host for StoreData {} +impl detail_types::Host for StoreData {} + impl editor::HostTextEditor for StoreData { fn path(&mut self, this: Resource) -> wasmtime::Result { Ok(to_ipc(&self.table.get(&this)?.ctx.path)) @@ -1151,6 +1153,29 @@ impl views::Host for StoreData { Ok(self.host.open_terminal(client.as_deref(), title, &program, args, cwd.as_deref()).map_err(to_wit)) } + fn open_log_tab(&mut self, key: String, title: String, log: views::ProcessLog) -> Fallible<()> { + if let Err(e) = self.require_interactive().and_then(|()| self.require(Capability::Process(log.program.clone()))) + { + return Ok(Err(e)); + } + if let Err(message) = check_log_tab(&key, &title, &log) { + return Ok(Err(wit::Error::Invalid(message))); + } + let Some(client) = self.invocation.client.clone() else { + return Ok(Err(wit::Error::Invalid("a log tab opens from a command or event of a window".into()))); + }; + let log = crate::ServiceLog { + program: log.program, + args: log.args, + toggles: log + .toggles + .into_iter() + .map(|t| crate::ServiceLogToggle { id: t.id, label: t.label, args: t.args }) + .collect(), + }; + Ok(self.host.open_log_tab(&client, key, title, log).map_err(to_wit)) + } + fn open_location(&mut self, path: String, line: u32, column: u32) -> Fallible<()> { if let Ok(dir) = self.host.plugin_dir() && let Ok(real) = sandbox::resolve_granted(&[Capability::FsRead(to_ipc(&dir))], &path) @@ -1301,6 +1326,27 @@ impl fs::Host for StoreData { } } +/// The limits of a log tab, the same a web view's `open-log-tab` has. +fn check_log_tab(key: &str, title: &str, log: &views::ProcessLog) -> Result<(), String> { + let line = |s: &str, max: usize| !s.is_empty() && s.chars().count() <= max && !s.contains(['\0', '\r', '\n']); + let args = |a: &[String]| a.iter().all(|a| a.len() <= 4096 && !a.contains('\0')); + let total = log.args.len() + log.toggles.iter().map(|t| t.args.len()).sum::(); + let mut ids = std::collections::BTreeSet::new(); + if !line(key, 128) || !line(title, 256) { + Err("a log tab's key (at most 128 characters) and title (256) are one nonempty line".into()) + } else if !line(&log.program, 256) || log.program.trim() != log.program { + Err(format!("{:?} is not a program name", log.program)) + } else if log.toggles.len() > 32 + || log.toggles.iter().any(|t| !line(&t.id, 64) || !line(&t.label, 128) || !ids.insert(t.id.as_str())) + { + Err("at most 32 toggles, with unique ids (at most 64 characters) and labels (128)".into()) + } else if total > 256 || !args(&log.args) || log.toggles.iter().any(|t| !args(&t.args)) { + Err("at most 256 arguments in all, of at most 4096 bytes each".into()) + } else { + Ok(()) + } +} + fn too_much_to_write(path: &str) -> wit::Error { wit::Error::Invalid(format!("{path}: more than {} MiB to write", sandbox::MAX_WRITE_BYTES >> 20)) } diff --git a/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm index 6496bacc8..498018633 100644 Binary files a/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources index 7f491aa8f..60b38a389 100644 --- a/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources @@ -1 +1 @@ -6efbb6db8442a4dec21e65bb8c4c65e08b0da32082ee26e870c0419ebaf4e7fb +6d7d707c7492d487fbd9debae791ad7e3755ac005f1f518efa54e6536ec59f08 diff --git a/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm index c017e04d4..cdcf91b87 100644 Binary files a/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm.sources index 62ce82be6..235ce4fd6 100644 --- a/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm.sources @@ -1 +1 @@ -21520d2522afaa7212e5c9b765110a334b2a1fe69302f602f05a61dcee08cd0b +652977e0bf63bc7664807016998a4d4795d2a7eb841ee1912ef562057e5437cb diff --git a/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm index 349486d0f..6aba123b1 100644 Binary files a/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm.sources index a3254c5c6..2f45ab6e3 100644 --- a/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm.sources @@ -1 +1 @@ -9d79e3f45abf0aaba03d30aa482decd3142d879b5964d12298b0aa6cbac581fd +42d3856b634bdaa69e21f4a42a9f375b73c2d19753a8986b82f2228bf92d37bc diff --git a/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm index 555b7654e..03887b874 100644 Binary files a/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources index f36e82258..a10b690a5 100644 --- a/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources @@ -1 +1 @@ -05442db7ac00e3bcc4670d87d846c46a05c02d101e1651c587245fd5aeb1152f +cacd381d84e1070fffe8f0ac64fad91b768df63b759c0b3e5e2d22a6a2fdeb73 diff --git a/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm index e5a3c066d..a4f2867c3 100644 Binary files a/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources index 27badf73f..3fa85d991 100644 --- a/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources @@ -1 +1 @@ -47c2a8deea407ed7bf6a56af8c118295f4ceed394faaff9b535e97cb4ea31115 +0d418585dca868fbceb6432d06c5c838c3d42a9e99e7a9c7641199f25fb8fbe8 diff --git a/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm index 80915c626..3f37ce42e 100644 Binary files a/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm.sources index 0b087d8b8..22f4d5f57 100644 --- a/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm.sources @@ -1 +1 @@ -884a42bd59ac64d08e9a0d3a72d4f1d1f25e850ef0b828be01142b61eda8db64 +6caa94db0c866c0c7f86011dbf837b3f9edb1c891276a4d435a8d02e8f64e0cb diff --git a/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm index 6498ea44e..865a9b957 100644 Binary files a/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources index 0b14924ff..b9928c8e8 100644 --- a/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources @@ -1 +1 @@ -48d2ab9cc824ed06ae34126d6651a9b7127599626cc00f7aa76d9b3161a2963f +8d9c6bcd572b6fb54dd42c79d66d4971e55ec3dc072fc769f5634212c2927e22 diff --git a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm index 375afe93c..fec17dc6c 100644 Binary files a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources index 528b92804..e62ffb8cd 100644 --- a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources @@ -1 +1 @@ -5233d7518b685e641fdd48ac2a53e6ca7381c182d36d7920607714703ba37a47 +2af65b7db7edce8450f0ca829adb3b79fcb64b1ec3d39b785fb9df9423815e7b diff --git a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm index 89c5841c8..2a468a118 100644 Binary files a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources index c329b3bdd..9cfccba7f 100644 --- a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources @@ -1 +1 @@ -3a77168e971d60ca4248b9bb2c27516614f74409324125821e74e3a236a67fb9 +40f80dcba2f370d02e2d5ad9609b21500fc42fc7c7202b439485a9cbf7baf727 diff --git a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm index 53e1eb079..5bd99d2cd 100644 Binary files a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources index 74a149345..a1c8782be 100644 --- a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources @@ -1 +1 @@ -4261434b2dd234426253f07f7e7e3daf8676fcf5680a578ac1b99f503fea2859 +a5ca61ba85297c52936cf9892ccf1c678383641c84a5a885389b24582fa1abfb diff --git a/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm index c4f202c4f..e8710f3dd 100644 Binary files a/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources index 634cf045e..b77911fb8 100644 --- a/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources @@ -1 +1 @@ -cc07e85ef5324b4d1d706665b0025b6682a581d1b91196f45a64284e238c7297 +5723b90f8a742f912b315209fc597e3ea7f428a2a234cf4ea5b4620454206d71 diff --git a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm index cc5b6df69..7fe3f4277 100644 Binary files a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources index 9ae5dcfed..53d5b7a23 100644 --- a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources @@ -1 +1 @@ -d0c974dee4010d0e3186bd426e3c53d09c2c56861b04e40f2187515bbb81f8d9 +dd5d41490f53a3b9b03d45ee8aba9f3fb08dd9211e835db5d3f9f7c42d3b6f9a diff --git a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm index 9ea3ecf3d..1c6092c27 100644 Binary files a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources index 924d753d0..1612f2bb1 100644 --- a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources @@ -1 +1 @@ -91ed6b9ee1cf77a0c3bee941aafceb252e7acd241f1d85ecb0366de98ee541d5 +185557c699fc00796eb487f50c688c3e91d8895bd05d4185b492352c187f9d98 diff --git a/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm index aac85f19f..b77075715 100644 Binary files a/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm.sources index ab595036a..6f4fdb848 100644 --- a/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm.sources @@ -1 +1 @@ -3eb9c513691e5ad323d4ba6520ccec6305049a39f13e8d74dccb728ab2f0ffca +d047b5618051144ec754b481b6bc75fae428d96c83711e9f0edf78749df59016 diff --git a/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm index 637aff481..d437b5359 100644 Binary files a/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources index a32c64ffe..0d0841088 100644 --- a/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources @@ -1 +1 @@ -6d59a4f37827a64d40cfe34dbcd9fea2b1de40ab39596c364b2cb7f5502de063 +18957534fc011510894544b61e154a9d0972c27ab1872d9d2accd01864984fa0 diff --git a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm index a01e14e81..d270f4772 100644 Binary files a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources index 2903c8d6d..da42ec0b8 100644 --- a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources @@ -1 +1 @@ -5da249c8c78cfcdf8f580099974d2ca654e98debb319f253c42ba18e5082fd0e +3562bc253b0b4f0fa1cb15b9c8a7470a09719b28008aa8f5f459bbaba8eda598 diff --git a/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm index 52be86348..49f377ecb 100644 Binary files a/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm.sources index b7404b29f..7b3b8525e 100644 --- a/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm.sources @@ -1 +1 @@ -961f07c9e1044e3e8c861a85ce4b2020cbbe9ce02b3764d1ec44a83d6c95eba1 +f28b40f3708ec4f828d04f7cec29208b66d0ab6158b26c60c0bd989cdc74f9b4 diff --git a/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm index 4f3a4e544..6dad1e8a6 100644 Binary files a/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources index 9f8c26060..98a0bf52b 100644 --- a/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources @@ -1 +1 @@ -31c61ed0204bd3b2f9152d20f9f275effc305ba7dd095d708c00f34fce023492 +60f6c34375c918ee941dc8f5a1c7851732fd0f5dd2c14ec609db1955c557026a diff --git a/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm index 6308c7612..5d26e6c68 100644 Binary files a/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm.sources index 3d02262d2..afddaf44f 100644 --- a/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm.sources @@ -1 +1 @@ -162cde1d53b72a4c88a98cc51a31a2536d0506aeef60e15295b44c682be50fb7 +edac658f8a54e396f294c74d031a31787508583256659a8effa8cc3d330e9a08 diff --git a/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm index 1bc80c745..44219a22a 100644 Binary files a/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources index 9f19abbc2..02312108a 100644 --- a/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources @@ -1 +1 @@ -aa7327f38c6277cfd7751b921bd43ac63ab8cb615ea428ccdfe9c4750606c6ad +28e1d58c8ab50574e184cb401050a6fd2755c9398cd3a3720dc376f67461a44b diff --git a/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm index 579f2ab81..176b52e99 100644 Binary files a/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm.sources index 2ad738d45..7c51baa74 100644 --- a/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm.sources @@ -1 +1 @@ -afece2c28e5077507159da59a93a73ede869678d168b3eaa7345962183417744 +e80605523d9aaa1c88c83deac439a39a739541bdb327e279352441547de3c232 diff --git a/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm index 348ad27aa..5671f3313 100644 Binary files a/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm.sources index ccf9d0391..f039130a9 100644 --- a/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm.sources @@ -1 +1 @@ -29dd12ef6b9399c3d58b80ca145f6e903b70a9b220be7ca69a2672ee2c3e5582 +efb03a86021bf5690feab0446c77d977c6252a4da1ee41030d923350e6cc5d11 diff --git a/crates/ide-plugin/tests/fixtures/web-log/plugin.toml b/crates/ide-plugin/tests/fixtures/web-log/plugin.toml index 0bfee2f59..d06225c8c 100644 --- a/crates/ide-plugin/tests/fixtures/web-log/plugin.toml +++ b/crates/ide-plugin/tests/fixtures/web-log/plugin.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "Test fixture for a capability-gated streaming editor tab." engines.openidea = "^1.3" wasm = "dist/plugin.wasm" -activation = ["on-command:ping", "on-command:open", "on-command:denied"] +activation = ["on-command:ping", "on-command:open", "on-command:denied", "on-command:code"] [capabilities] optional = ["process:bash"] @@ -27,3 +27,7 @@ title = "Log Probe: Open Stream" [[contributes.commands]] id = "denied" title = "Log Probe: Try Undeclared Program" + +[[contributes.commands]] +id = "code" +title = "Log Probe: Open From Code" diff --git a/crates/ide-plugin/tests/fixtures/web-log/src/lib.rs b/crates/ide-plugin/tests/fixtures/web-log/src/lib.rs index 43d9d3ac8..2fdfb83a0 100644 --- a/crates/ide-plugin/tests/fixtures/web-log/src/lib.rs +++ b/crates/ide-plugin/tests/fixtures/web-log/src/lib.rs @@ -1,5 +1,14 @@ +use openidea_plugin::details::ProcessLog; use openidea_plugin::{Error, Invocation, Plugin, host, register_plugin}; +const SCRIPT: &str = r#"i=0 +while true; do + i=$((i+1)) + printf 'INFO probe line %s — Unicode 日志\n' "$i" + if [ "$1" = "--verbose" ]; then printf 'DEBUG verbose output enabled\n'; fi + read -t 1 ignored || true +done"#; + #[derive(Default)] struct WebLog; @@ -10,6 +19,16 @@ impl Plugin for WebLog { host::post_view_message("logs", id)?; Ok(None) } + // The same stream as the page's, opened by the plugin's code. + "code" => { + let log = ProcessLog::new("bash", &["--noprofile", "--norc", "-c", SCRIPT, "log-probe"]).toggle( + "verbose", + "Verbose", + &["--verbose"], + ); + host::open_log_tab("code", "Probe logs (code)", &log)?; + Ok(None) + } "ping" => Ok(Some("Web log ready".into())), _ => Err(Error::NotFound(id.into())), } diff --git a/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm index aac232f8d..df2e4016d 100644 Binary files a/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources index 9a8893eb3..c59478b01 100644 --- a/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources @@ -1 +1 @@ -ebc3a1213b97addb5abcbbfcf4baaff3f7644d1b60b62e2f215d3a35f36f221b +009f7e069920bcf0e7699b57cd41954dc1dcb620977e081c24ec58ef0a9809d2 diff --git a/plugins/README.md b/plugins/README.md index 71d4af20c..158a7de05 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -1150,11 +1150,29 @@ five-tab retention limit; evicting a hidden tab stops its process. Closing the tab, disabling/updating the plugin, changing grants, switching projects or closing the window stops it, including a pending spawn. +A plugin's code opens the same tab without a page, from a command or event of a +window, with `host::open_log_tab(key, title, &log)` (WIT `views.open-log-tab`) +and the `ProcessLog` the Services pane uses: + +```rust +use openidea_plugin::details::ProcessLog; + +let log = ProcessLog::new("kubectl", &["logs", "-f", "deploy/api"]).toggle("previous", "Previous", &["--previous"]); +host::open_log_tab("api-logs", "api logs", &log)?; +``` + +The call fails at once without the `process:` grant or over the limits +above. The host remembers what each key runs in that window, and the tab starts +it by key, so nothing the tab sends names a program; a plugin keeps at most 32 +such tabs per window (opening more forgets the oldest). Tabs opened by code +don't run in remote projects yet. + The built fixture `crates/ide-plugin/tests/fixtures/web-log` exercises permission refusal, continuous Unicode output and a plugin-defined Verbose toggle. On Unix, grant its optional `process:bash` permission and use its Log Probe tool window. Its `Log Probe: Open Stream` and `Log Probe: Try Undeclared Program` commands -also ask the sandboxed page to open a tab, exercising the Wasm-to-page bridge. +also ask the sandboxed page to open a tab, exercising the Wasm-to-page bridge; +`Log Probe: Open From Code` opens one from the plugin's code. This extends the web bridge without changing released WIT signatures. ### Plugin-backed content editor tabs (API 1.3) diff --git a/plugins/official/ai-completion/dist/plugin.wasm b/plugins/official/ai-completion/dist/plugin.wasm index 6f4d6067e..90c83adc3 100644 Binary files a/plugins/official/ai-completion/dist/plugin.wasm and b/plugins/official/ai-completion/dist/plugin.wasm differ diff --git a/plugins/official/ai-completion/dist/plugin.wasm.sources b/plugins/official/ai-completion/dist/plugin.wasm.sources index 490783642..12a94dd9e 100644 --- a/plugins/official/ai-completion/dist/plugin.wasm.sources +++ b/plugins/official/ai-completion/dist/plugin.wasm.sources @@ -1 +1 @@ -b390c7f6acf5d32cbf451faf7a6393f2d4f130d5131ef0857a2b0904c434f598 +7dfbea1faeae8930b462fe80d431c83c49aeafa45ac7acd36f7d7f05fac3aaa4 diff --git a/plugins/official/checkstyle/dist/plugin.wasm b/plugins/official/checkstyle/dist/plugin.wasm index c20f7e82d..35b84c97c 100644 Binary files a/plugins/official/checkstyle/dist/plugin.wasm and b/plugins/official/checkstyle/dist/plugin.wasm differ diff --git a/plugins/official/checkstyle/dist/plugin.wasm.sources b/plugins/official/checkstyle/dist/plugin.wasm.sources index 009d0a5a7..e3440fcf3 100644 --- a/plugins/official/checkstyle/dist/plugin.wasm.sources +++ b/plugins/official/checkstyle/dist/plugin.wasm.sources @@ -1 +1 @@ -1eea85130f5feacdcd6aade3548ac8e6ca4c1b316441346a89a7be6c13caaba7 +adeef6ba69276d3ac8e9ba349ca6e787befa6532ccac69bc35a381c0428af189 diff --git a/plugins/official/csv-editor/dist/plugin.wasm b/plugins/official/csv-editor/dist/plugin.wasm index 330b32d81..c0e3c433e 100644 Binary files a/plugins/official/csv-editor/dist/plugin.wasm and b/plugins/official/csv-editor/dist/plugin.wasm differ diff --git a/plugins/official/csv-editor/dist/plugin.wasm.sources b/plugins/official/csv-editor/dist/plugin.wasm.sources index b2edd0500..66190f363 100644 --- a/plugins/official/csv-editor/dist/plugin.wasm.sources +++ b/plugins/official/csv-editor/dist/plugin.wasm.sources @@ -1 +1 @@ -d2e1a074d81a4f29935524b6ba11a94d6a9bc301833ea721ed9e7cab461e198e +51def3be1a570ee61cb23f0b738002427298dbdafffcb687896531685bbe3c77 diff --git a/plugins/official/git-toolbox/dist/plugin.wasm b/plugins/official/git-toolbox/dist/plugin.wasm index 13f4ec829..3fde3ebc0 100644 Binary files a/plugins/official/git-toolbox/dist/plugin.wasm and b/plugins/official/git-toolbox/dist/plugin.wasm differ diff --git a/plugins/official/git-toolbox/dist/plugin.wasm.sources b/plugins/official/git-toolbox/dist/plugin.wasm.sources index 99383b167..390cff508 100644 --- a/plugins/official/git-toolbox/dist/plugin.wasm.sources +++ b/plugins/official/git-toolbox/dist/plugin.wasm.sources @@ -1 +1 @@ -150ed795daab29e3a152e50ebdf5e4ec3ca569d792962b77fce5fac428e7e076 +6c4439b36b22289c8dc54498d6e7e72a4c4cc15eac1a4957fc2b71d4fbd3c330 diff --git a/plugins/official/graphviz/dist/plugin.wasm b/plugins/official/graphviz/dist/plugin.wasm index f779f5fb0..eb2f4ce88 100644 Binary files a/plugins/official/graphviz/dist/plugin.wasm and b/plugins/official/graphviz/dist/plugin.wasm differ diff --git a/plugins/official/graphviz/dist/plugin.wasm.sources b/plugins/official/graphviz/dist/plugin.wasm.sources index 2cc437eae..7df840afa 100644 --- a/plugins/official/graphviz/dist/plugin.wasm.sources +++ b/plugins/official/graphviz/dist/plugin.wasm.sources @@ -1 +1 @@ -580e19fd349b6be1f2d4c32e572015337ef8f1e73b1b1f24199fc6d4ea73189f +00d56f431ab8e59c04796cf56054a98ba78040a5e4142c3f727725c31781a008 diff --git a/plugins/official/key-promoter/dist/plugin.wasm b/plugins/official/key-promoter/dist/plugin.wasm index ec8b268a1..8526a5f22 100644 Binary files a/plugins/official/key-promoter/dist/plugin.wasm and b/plugins/official/key-promoter/dist/plugin.wasm differ diff --git a/plugins/official/key-promoter/dist/plugin.wasm.sources b/plugins/official/key-promoter/dist/plugin.wasm.sources index 76dadfcd3..3d0ab2bcf 100644 --- a/plugins/official/key-promoter/dist/plugin.wasm.sources +++ b/plugins/official/key-promoter/dist/plugin.wasm.sources @@ -1 +1 @@ -64d1e9a8bf48355f380feba052ea0ffb5f4ae7957b04d0a62a04e1d49d6ed690 +9fca272eec5a601b8de6071aa358954e9b79db08f45714f077624418e8f616bc diff --git a/plugins/official/license-header/dist/plugin.wasm b/plugins/official/license-header/dist/plugin.wasm index 8715bb6eb..57a9de496 100644 Binary files a/plugins/official/license-header/dist/plugin.wasm and b/plugins/official/license-header/dist/plugin.wasm differ diff --git a/plugins/official/license-header/dist/plugin.wasm.sources b/plugins/official/license-header/dist/plugin.wasm.sources index b10c379ee..60a829496 100644 --- a/plugins/official/license-header/dist/plugin.wasm.sources +++ b/plugins/official/license-header/dist/plugin.wasm.sources @@ -1 +1 @@ -ef76535391c4186a6373ef61197d96cc3ad7c42f47505ec75328ba5385e4df6e +5adfc0d749feabe5fe7e20857fd636d01341b91f3dd8440779af6dcd14435619 diff --git a/plugins/official/mybatis/dist/plugin.wasm b/plugins/official/mybatis/dist/plugin.wasm index 259540c8f..97883f1e2 100644 Binary files a/plugins/official/mybatis/dist/plugin.wasm and b/plugins/official/mybatis/dist/plugin.wasm differ diff --git a/plugins/official/mybatis/dist/plugin.wasm.sources b/plugins/official/mybatis/dist/plugin.wasm.sources index c7b151bc1..bcefdb21e 100644 --- a/plugins/official/mybatis/dist/plugin.wasm.sources +++ b/plugins/official/mybatis/dist/plugin.wasm.sources @@ -1 +1 @@ -246a3ace4e3ccdd9ed4f2499cde275c9ea2402fbb902893cc2d8d3403175bd47 +1b388c40cb6be29c5e10e938c0c8f6848d10cb4bf1f7438db37f53b13314708e diff --git a/plugins/official/regex-tester/dist/plugin.wasm b/plugins/official/regex-tester/dist/plugin.wasm index c12ba0138..6974f2cff 100644 Binary files a/plugins/official/regex-tester/dist/plugin.wasm and b/plugins/official/regex-tester/dist/plugin.wasm differ diff --git a/plugins/official/regex-tester/dist/plugin.wasm.sources b/plugins/official/regex-tester/dist/plugin.wasm.sources index 4bcbe26c9..fd050c58b 100644 --- a/plugins/official/regex-tester/dist/plugin.wasm.sources +++ b/plugins/official/regex-tester/dist/plugin.wasm.sources @@ -1 +1 @@ -75b3b93fe5ca2c5a58052c931f4d9cd824c379227d7f4cd8477fa89d045cda06 +af4478864196b5802f505d26beafe4492bc1335536311bc873b9d18773f9eebb diff --git a/plugins/official/solidity/dist/plugin.wasm b/plugins/official/solidity/dist/plugin.wasm index f476f6ea9..f73c4c5d5 100644 Binary files a/plugins/official/solidity/dist/plugin.wasm and b/plugins/official/solidity/dist/plugin.wasm differ diff --git a/plugins/official/solidity/dist/plugin.wasm.sources b/plugins/official/solidity/dist/plugin.wasm.sources index 8ddc665c2..b414f0c96 100644 --- a/plugins/official/solidity/dist/plugin.wasm.sources +++ b/plugins/official/solidity/dist/plugin.wasm.sources @@ -1 +1 @@ -59f62db0a1c20a1d4fa6b380cd3272ec70ea4789835873d0abe3839ec08c94c3 +46ce9f2d5b4f87a4be26facea4870f3de59952dc95c536fcf33fb0ff0a7c27a7 diff --git a/plugins/official/string-tools/dist/plugin.wasm b/plugins/official/string-tools/dist/plugin.wasm index 6892bb4a0..e9871c762 100644 Binary files a/plugins/official/string-tools/dist/plugin.wasm and b/plugins/official/string-tools/dist/plugin.wasm differ diff --git a/plugins/official/string-tools/dist/plugin.wasm.sources b/plugins/official/string-tools/dist/plugin.wasm.sources index 8c84ce4d3..2d44ef4df 100644 --- a/plugins/official/string-tools/dist/plugin.wasm.sources +++ b/plugins/official/string-tools/dist/plugin.wasm.sources @@ -1 +1 @@ -548c6ed16fcd5d3a053bf6d8417d52ebd0fd74138d0665e9205eb8bf32ec497a +f4e37cdac925f376bac945d5bb86c648b559787aa22737e6440835456f9f74cf diff --git a/sdk/rust/openidea-plugin/src/host.rs b/sdk/rust/openidea-plugin/src/host.rs index 7566965d0..0fe71b92b 100644 --- a/sdk/rust/openidea-plugin/src/host.rs +++ b/sdk/rust/openidea-plugin/src/host.rs @@ -451,6 +451,14 @@ mod imp { wit::views::open_terminal(title, program, args).map_err(error) } + pub fn open_log_tab(key: &str, title: &str, log: &crate::details::ProcessLog) -> Result<(), Error> { + use wit::detail_types::{LogToggle, ProcessLog}; + let toggles = + log.toggles.iter().map(|t| LogToggle { id: t.id.clone(), label: t.label.clone(), args: t.args.clone() }); + let log = ProcessLog { program: log.program.clone(), args: log.args.clone(), toggles: toggles.collect() }; + wit::views::open_log_tab(key, title, &log).map_err(error) + } + pub fn open_location(path: &str, line: u32, column: u32) -> Result<(), Error> { wit::views::open_location(path, line, column).map_err(error) } @@ -705,6 +713,14 @@ pub fn open_terminal(title: &str, program: &str, args: &[String]) -> Result<(), imp::open_terminal(title, program, args) } +/// Opens (or focuses) the read-only log tab `key` in the invoking window, +/// following `log`'s program (the approved `process:`) in the project +/// root, with its toggles above the output. Only from a command or event of a +/// window; `title` at most 256 characters, `key` 128, at most 32 toggles. +pub fn open_log_tab(key: &str, title: &str, log: &crate::details::ProcessLog) -> Result<(), Error> { + imp::open_log_tab(key, title, log) +} + pub fn open_location(path: &str, line: u32, column: u32) -> Result<(), Error> { imp::open_location(path, line, column) } diff --git a/sdk/rust/openidea-plugin/src/mock.rs b/sdk/rust/openidea-plugin/src/mock.rs index fef70b936..46edd3292 100644 --- a/sdk/rust/openidea-plugin/src/mock.rs +++ b/sdk/rust/openidea-plugin/src/mock.rs @@ -122,6 +122,7 @@ struct State { refreshed: Vec, view_messages: Vec<(String, String)>, terminals: Vec, + log_tabs: Vec, locations: Vec<(String, u32, u32)>, settings: BTreeMap, locale: Option, @@ -135,6 +136,14 @@ pub struct MockTerminal { pub args: Vec, } +/// A log tab the plugin opened. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MockLogTab { + pub key: String, + pub title: String, + pub log: crate::details::ProcessLog, +} + /// At most 8 programs a plugin may keep running, as the host allows. const MAX_CHILDREN: usize = 8; @@ -407,6 +416,11 @@ impl MockHost { self.state.borrow().terminals.clone() } + /// Log tabs the plugin opened, the latest of each key last. + pub fn log_tabs(&self) -> Vec { + self.state.borrow().log_tabs.clone() + } + /// The value of one of the plugin's settings, as JSON (`true`, `"text"`). pub fn set_setting(&self, key: &str, json: &str) { self.state.borrow_mut().settings.insert(key.into(), json.into()); @@ -1272,6 +1286,20 @@ pub mod current { }) } + pub fn open_log_tab(key: &str, title: &str, log: &crate::details::ProcessLog) -> Result<(), Error> { + with(|s| { + require(s, &format!("process:{}", log.program))?; + let line = + |t: &str, max: usize| !t.is_empty() && t.chars().count() <= max && !t.contains(['\0', '\r', '\n']); + if !line(key, 128) || !line(title, 256) || log.toggles.len() > 32 { + return Err(Error::Invalid("a log tab's key, title or toggles are over the limits".into())); + } + s.log_tabs.retain(|t| t.key != key); + s.log_tabs.push(MockLogTab { key: key.into(), title: title.into(), log: log.clone() }); + Ok(()) + }) + } + pub fn open_location(path: &str, line: u32, column: u32) -> Result<(), Error> { with(|s| { if !is_private_path(path) { @@ -1824,6 +1852,25 @@ mod tests { assert_eq!(mock.opened_locations().len(), 2); } + #[test] + fn log_tabs_need_the_program_grant() { + let log = crate::details::ProcessLog::new("kubectl", &["logs", "-f", "api"]).toggle( + "previous", + "Previous", + &["--previous"], + ); + let _mock = MockHost::install(); + assert_eq!(host::open_log_tab("api", "api logs", &log), Err(Error::Denied("process:kubectl".into()))); + + let mock = MockHost::install().with_capabilities(&["process:kubectl"]); + host::open_log_tab("api", "api logs", &log).unwrap(); + // Reopening a key replaces that tab. + host::open_log_tab("api", "api logs again", &log).unwrap(); + assert_eq!(mock.log_tabs().len(), 1); + assert_eq!((mock.log_tabs()[0].title.as_str(), &mock.log_tabs()[0].log), ("api logs again", &log)); + assert!(matches!(host::open_log_tab("", "api logs", &log), Err(Error::Invalid(_)))); + } + #[test] fn fs_write_needs_a_grant_of_its_own() { // `fs.read` does not let a plugin write. diff --git a/sdk/rust/openidea-plugin/wit/views.wit b/sdk/rust/openidea-plugin/wit/views.wit index 92db335e7..78da35354 100644 --- a/sdk/rust/openidea-plugin/wit/views.wit +++ b/sdk/rust/openidea-plugin/wit/views.wit @@ -123,6 +123,7 @@ interface view-types { /// What a plugin's code can ask of its views. interface views { use types.{error}; + use detail-types.{process-log}; /// The view (or every custom editor of that id) shows outdated content: /// the IDE renders it again. @@ -136,6 +137,13 @@ interface views { /// `program` (pinned by a `process:` grant) with `args` in the /// project root: long-running output (logs) and interactive sessions. open-terminal: func(title: string, program: string, args: list) -> result<_, error>; + /// Opens a read-only log tab in the editor area of the window the call + /// came from, following `log`'s program (pinned by a `process:` + /// grant) in the project root, its toggles above the output: the tab a + /// web view opens with `open-log-tab`. Reopening `key` focuses that tab. + /// Only from a command or event of a window; `title` at most 256 + /// characters, `key` 128, at most 32 toggles. + open-log-tab: func(key: string, title: string, log: process-log) -> result<_, error>; /// Opens `path` (project-relative or absolute, inside the project) in an /// editor of the window the call came from, the caret at `line` and /// `column` (0-based; UTF-16 column): a view's links to code. diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 0de0ec420..21196cd42 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -502,6 +502,7 @@ pub fn builder() -> tauri_specta::Builder { plugin::plugin_service_log, plugin::plugin_web_page, plugin::plugin_web_pty_spawn, + plugin::plugin_log_tab_spawn, plugin::plugin_web_save_file, plugin::plugin_web_read_file, plugin::plugin_line_markers, diff --git a/src-tauri/src/commands/plugin.rs b/src-tauri/src/commands/plugin.rs index 3051b72c4..1fd07a381 100644 --- a/src-tauri/src/commands/plugin.rs +++ b/src-tauri/src/commands/plugin.rs @@ -565,6 +565,41 @@ pub async fn plugin_web_pty_spawn( .map_err(|e| IdeError::internal(e.to_string()))? } +/// Starts the program of a log tab the plugin's code opened in this window, +/// with the toggles in `on`. +#[tauri::command] +#[specta::specta] +#[allow(clippy::too_many_arguments)] +pub async fn plugin_log_tab_spawn( + window: Window, + state: State<'_, AppState>, + plugin_id: String, + key: String, + on: Vec, + cols: u16, + rows: u16, + on_output: Channel, +) -> IdeResult { + let plugins = state.plugins.clone(); + let host = state.host.clone(); + let window = window.label().to_owned(); + tauri::async_runtime::spawn_blocking(move || { + plugins.spawn_log_tab_pty( + &host, + &plugin_id, + &key, + &window, + &on, + (cols, rows), + Arc::new(move |chunk| { + let _ = on_output.send(chunk); + }), + ) + }) + .await + .map_err(|e| IdeError::internal(e.to_string()))? +} + /// Largest file a web view saves or opens (`openidea.saveFile` / `openFile`). const MAX_WEB_VIEW_FILE: u64 = 64 << 20; diff --git a/src/bindings.ts b/src/bindings.ts index 394b626fc..361021fbc 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -1150,6 +1150,11 @@ export const commands = { /** The page of a plugin's web view or custom editor (`web = …`). */ pluginWebPage: (pluginId: string, view: string) => typedError(__TAURI_INVOKE("plugin_web_page", { pluginId, view })), pluginWebPtySpawn: (pluginId: string, view: string, command: WebPtyCommand, onOutput: Channel) => typedError(__TAURI_INVOKE("plugin_web_pty_spawn", { pluginId, view, command, onOutput })), + /** + * Starts the program of a log tab the plugin's code opened in this window, + * with the toggles in `on`. + */ + pluginLogTabSpawn: (pluginId: string, key: string, on: string[], cols: number, rows: number, onOutput: Channel) => typedError(__TAURI_INVOKE("plugin_log_tab_spawn", { pluginId, key, on, cols, rows, onOutput })), /** * Writes what a web view saves (`openidea.saveFile`) to the path the user * chose in the save dialog, on this machine. @@ -6052,6 +6057,11 @@ actions: NotificationAction[] } | { type: "viewMessage"; pluginId: string; view: string; message: string } | /** Open a terminal running `program` (already approved and verified). */ { type: "openTerminal"; pluginId: string; title: string; program: IpcPath; args: string[]; cwd: IpcPath } | { type: "openLocation"; pluginId: string; path: IpcPath; line: number; column: number } | +/** + * Open (or focus) the log tab `key` following `log` (`views.open-log-tab`); + * it starts the program with `plugin_log_tab_spawn`. + */ +{ type: "openLogTab"; pluginId: string; key: string; title: string; log: ServiceLog } | /** * Plugin languages, or the servers they may start, changed: re-fetch * `languages` (the host re-detects open documents: [`crate::Host::plugin_languages_changed`]). diff --git a/src/features/plugins/__tests__/logEditor.test.tsx b/src/features/plugins/__tests__/logEditor.test.tsx index 68e3b6097..da0293c34 100644 --- a/src/features/plugins/__tests__/logEditor.test.tsx +++ b/src/features/plugins/__tests__/logEditor.test.tsx @@ -9,11 +9,11 @@ import { usePlugins } from "../store"; import { startViewUi } from "../uiPoints"; import { registerLogActions } from "../views/logActions"; import { PluginLogEditor } from "../views/logEditor"; -import { boundedLog, logTabData } from "../views/logTab"; -import { webPtyBackend } from "../views/ptyBackend"; +import { boundedLog, logTabData, type PluginLogEditorData } from "../views/logTab"; +import { logTabPtyBackend, webPtyBackend } from "../views/ptyBackend"; import type { PtyBackend } from "../views/ptyBridge"; -vi.mock("../views/ptyBackend", () => ({ webPtyBackend: vi.fn() })); +vi.mock("../views/ptyBackend", () => ({ webPtyBackend: vi.fn(), logTabPtyBackend: vi.fn() })); const plugin = { id: "probe", enabled: true, capabilities: [{ id: "process:bash", granted: true }] } as PluginInfo; const spec = { @@ -36,7 +36,7 @@ afterEach(() => { vi.restoreAllMocks(); }); -function setup() { +function setup(extra: Partial = {}) { usePlugins.setState({ plugins: [plugin] }); let output: (text: string) => void = () => {}; let exit: Parameters[0] = () => {}; @@ -54,7 +54,8 @@ function setup() { }, }; vi.mocked(webPtyBackend).mockReturnValue(backend); - const data = logTabData("probe", "logs", spec, 100); + vi.mocked(logTabPtyBackend).mockReturnValue(backend); + const data = { ...logTabData("probe", "logs", spec, 100), ...extra }; const input = { kind: "pluginLog", key: "test-log", data }; const rendered = render(); const content = () => rendered.container.querySelector(".cm-content")!; @@ -122,6 +123,16 @@ describe("plugin streaming log tabs", () => { expect(screen.getByRole("textbox", { name: "Find" })).toBeTruthy(); }); + it("starts a tab opened by plugin code by its key, with the toggles that are on", async () => { + const f = setup({ view: "", key: "code", origin: "code" }); + expect(webPtyBackend).not.toHaveBeenCalled(); + expect(logTabPtyBackend).toHaveBeenLastCalledWith("probe", "code", []); + await act(async () => {}); + fireEvent.click(screen.getByRole("switch", { name: "Verbose" })); + await waitFor(() => expect(f.backend.kill).toHaveBeenCalledWith(42)); + expect(logTabPtyBackend).toHaveBeenLastCalledWith("probe", "code", ["verbose"]); + }); + it("kills the previous process on toggle and every lifecycle boundary", async () => { for (const boundary of ["unmount", "disable", "project", "window"] as const) { const f = setup(); diff --git a/src/features/plugins/runtimeUi.tsx b/src/features/plugins/runtimeUi.tsx index f2b2edc89..ecd86c00c 100644 --- a/src/features/plugins/runtimeUi.tsx +++ b/src/features/plugins/runtimeUi.tsx @@ -31,6 +31,8 @@ import { confirm } from "@/workbench/messages"; import { removeTask, upsertTask, useTasks } from "@/workbench/tasks"; import { pluginsBackend } from "./backend"; import { usePluginT } from "./pluginText"; +import { PLUGIN_LOG_EDITOR_KIND } from "./views/logEditor"; +import type { PluginLogEditorData } from "./views/logTab"; import { bumpView, deliverViewMessage } from "./views/viewStore"; import { revealWindowView } from "./views/viewWindows"; import { runPluginCommand, usePlugins } from "./store"; @@ -420,6 +422,22 @@ export function handleRuntimeEvent(e: PluginEvent): boolean { // The program was approved (`process:`) and verified by the host. void executeAction("terminal.runCommand", { program: e.program, args: e.args, name: e.title, cwd: e.cwd }); return true; + case "openLogTab": + void maybeEditorService()?.openInput({ + kind: PLUGIN_LOG_EDITOR_KIND, + key: `pluginLog:${JSON.stringify([e.pluginId, "", e.key])}`, + data: { + pluginId: e.pluginId, + view: "", + key: e.key, + title: e.title, + program: e.log.program, + args: e.log.args, + toggles: e.log.toggles, + origin: "code", + } satisfies PluginLogEditorData, + }); + return true; case "openLocation": void maybeEditorService()?.openFile(e.path, { selection: { line: e.line, col: e.column }, focus: true }); return true; diff --git a/src/features/plugins/views/logEditor.tsx b/src/features/plugins/views/logEditor.tsx index 7bf0ecaf4..d36c762a7 100644 --- a/src/features/plugins/views/logEditor.tsx +++ b/src/features/plugins/views/logEditor.tsx @@ -23,7 +23,7 @@ import type { EditorInput } from "@/workbench/contributions"; import { usePlugins } from "../store"; import { bindLogControls } from "./logActions"; import { boundedLog, type PluginLogEditorData } from "./logTab"; -import { webPtyBackend } from "./ptyBackend"; +import { logTabPtyBackend, webPtyBackend } from "./ptyBackend"; import { ptyBridge } from "./ptyBridge"; export const PLUGIN_LOG_EDITOR_KIND = "pluginLog"; @@ -371,7 +371,11 @@ function PluginLogEditorBody({ input, data }: { input: EditorInput; data: Plugin sequence = undefined; bridge.handle(JSON.stringify({ op: "pty-ack", sessionId: "log", sequence: consumed })); }; - const bridge = ptyBridge(data.pluginId, webPtyBackend(data.pluginId, data.view), (message) => { + const backend = + data.origin === "code" + ? logTabPtyBackend(data.pluginId, data.key ?? "", [...on]) + : webPtyBackend(data.pluginId, data.view); + const bridge = ptyBridge(data.pluginId, backend, (message) => { const m = message as Record; if (stopped) return; if (m.op === "pty-data") { diff --git a/src/features/plugins/views/logTab.ts b/src/features/plugins/views/logTab.ts index 0077803f0..0ed1dc6c9 100644 --- a/src/features/plugins/views/logTab.ts +++ b/src/features/plugins/views/logTab.ts @@ -15,6 +15,8 @@ export interface PluginLogEditorData { program: string; args: string[]; toggles: PluginLogToggleSpec[]; + /** Opened by the plugin's code (`views.open-log-tab`): the host starts what it approved for `key`. */ + origin?: "code"; } const text = (v: unknown, max: number): v is string => diff --git a/src/features/plugins/views/ptyBackend.ts b/src/features/plugins/views/ptyBackend.ts index bb2991b00..741ba0d57 100644 --- a/src/features/plugins/views/ptyBackend.ts +++ b/src/features/plugins/views/ptyBackend.ts @@ -1,11 +1,34 @@ import { Channel } from "@tauri-apps/api/core"; import { tx } from "@/platform/i18n"; +import type { TerminalInfo, WebPtyCommand } from "@/bindings"; import { call, commands, isTauri, onIdeEvent } from "@/platform/ipc"; +import { isRemoteWindow } from "@/platform/remote"; import { usePlugins } from "../store"; import type { PtyBackend } from "./ptyBridge"; /** Both frame terminals and log tabs use the backend's exact, pinned process grant. */ export function webPtyBackend(pluginId: string, view: string): PtyBackend { + return pluginPtyBackend(pluginId, (command, channel) => + call(commands.pluginWebPtySpawn(pluginId, view, command, channel)), + ); +} + +/** + * A log tab the plugin's code opened: the host runs what it approved for `key`, + * with the toggles in `on`; the command the tab builds only lends its size. + */ +export function logTabPtyBackend(pluginId: string, key: string, on: string[]): PtyBackend { + return pluginPtyBackend(pluginId, (command, channel) => { + if (isRemoteWindow()) + throw new Error(tx("Log tabs opened by plugin code are not available in remote projects yet.")); + return call(commands.pluginLogTabSpawn(pluginId, key, on, command.cols, command.rows, channel)); + }); +} + +function pluginPtyBackend( + pluginId: string, + start: (command: WebPtyCommand, channel: Channel) => Promise, +): PtyBackend { return { spawn: async (command, output) => { const current = usePlugins.getState().plugins.find((p) => p.id === pluginId); @@ -17,7 +40,7 @@ export function webPtyBackend(pluginId: string, view: string): PtyBackend { throw new Error(tx`Cannot start PTY: process:${command.program} capability not granted.`); const channel = new Channel(); channel.onmessage = output; - return call(commands.pluginWebPtySpawn(pluginId, view, command, channel)); + return start(command, channel); }, write: (id, data) => call(commands.termWrite(id, data)), resize: (id, cols, rows) => call(commands.termResize(id, cols, rows)), diff --git a/src/platform/locales/text/en.json b/src/platform/locales/text/en.json index 10fa77652..3acad903c 100644 --- a/src/platform/locales/text/en.json +++ b/src/platform/locales/text/en.json @@ -402,5 +402,6 @@ "The plugin did not save the content": "The plugin did not save the content", "Running an action applies pending settings first.": "Running an action applies pending settings first.", "Could not apply settings.": "Could not apply settings.", - "The value exceeds this setting’s limits.": "The value exceeds this setting’s limits." + "The value exceeds this setting’s limits.": "The value exceeds this setting’s limits.", + "Log tabs opened by plugin code are not available in remote projects yet.": "Log tabs opened by plugin code are not available in remote projects yet." } diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index 08c6ceed6..c30b9385d 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -6755,5 +6755,6 @@ "The plugin did not save the content": "插件未保存内容", "Running an action applies pending settings first.": "运行动作前会先应用待保存的设置。", "Could not apply settings.": "无法应用设置。", - "The value exceeds this setting’s limits.": "该值超出了此设置的限制。" + "The value exceeds this setting’s limits.": "该值超出了此设置的限制。", + "Log tabs opened by plugin code are not available in remote projects yet.": "插件代码打开的日志标签页暂不支持远程项目。" } diff --git a/wit/views.wit b/wit/views.wit index 92db335e7..78da35354 100644 --- a/wit/views.wit +++ b/wit/views.wit @@ -123,6 +123,7 @@ interface view-types { /// What a plugin's code can ask of its views. interface views { use types.{error}; + use detail-types.{process-log}; /// The view (or every custom editor of that id) shows outdated content: /// the IDE renders it again. @@ -136,6 +137,13 @@ interface views { /// `program` (pinned by a `process:` grant) with `args` in the /// project root: long-running output (logs) and interactive sessions. open-terminal: func(title: string, program: string, args: list) -> result<_, error>; + /// Opens a read-only log tab in the editor area of the window the call + /// came from, following `log`'s program (pinned by a `process:` + /// grant) in the project root, its toggles above the output: the tab a + /// web view opens with `open-log-tab`. Reopening `key` focuses that tab. + /// Only from a command or event of a window; `title` at most 256 + /// characters, `key` 128, at most 32 toggles. + open-log-tab: func(key: string, title: string, log: process-log) -> result<_, error>; /// Opens `path` (project-relative or absolute, inside the project) in an /// editor of the window the call came from, the caret at `line` and /// `column` (0-based; UTF-16 column): a view's links to code.