Compare commits

..
Author SHA1 Message Date
l0ng-ai ac7139c94d feat(plugins): open two texts in the diff viewer (API 1.7)
Plugins could show their own content in an editor tab (open-content-tab) but
could open the diff viewer only as the reply to one of that tab's actions. A
review plugin (a merge request's files, say) had no way to show a file's
before and after.

views.open-diff opens two texts in the invoking window's diff viewer, with
optional labels and title and a file name that picks the syntax highlighting;
web views send { op: "open-diff", ... }. The SDK wraps it as host::open_diff
with a mock. The plugin API moves to 1.7.0 (1.6.0 is released) and the checked-in
components are rebuilt.
2026-11-13 11:47:06 +08:00
l0ng-ai 28829ad756 style(vcs): format inline blame (#3051) 2026-11-13 11:22:44 +08:00
l0ng-ai d6c562049f test(e2e): load the app modules of the Branches resize spec through variables (#3050)
knip resolved the literal Vite paths in page.evaluate as imports and failed on them; the other specs pass the module path through a variable.
2026-11-13 11:21:04 +08:00
l0ng-ai 100075166c test(database): let FSEvents deliver the startup events before checking ignored files (#3048)
On macOS, FSEvents can report the creation of .openidea/ made just before the watch started after the watch is running, so ignores_other_files counted one change and failed on CI. The test now waits for those events and compares against the count after them.
2026-11-13 11:18:40 +08:00
l0ng-ai d5332a2197 feat(ui): show Go files with a gopher icon (#3047)
Go files get the gopher icon, as GoLand shows them, instead of a "GO" letter tile; go.mod and go.work keep theirs.
2026-11-13 11:16:32 +08:00
187 changed files with 511 additions and 1103 deletions

No files matched your search

Generated
-14
View File
@@ -4691,20 +4691,6 @@ dependencies = [
"zeromq",
]
[[package]]
name = "ide-p4"
version = "0.1.43"
dependencies = [
"ide-core",
"regex-lite",
"serde",
"serde_json",
"specta",
"tempfile",
"tracing",
"which",
]
[[package]]
name = "ide-plugin"
version = "0.1.43"
-1
View File
@@ -24,7 +24,6 @@ ide-jsonrpc = { path = "crates/ide-jsonrpc" }
ide-lsp = { path = "crates/ide-lsp" }
ide-dap = { path = "crates/ide-dap" }
ide-vcs = { path = "crates/ide-vcs" }
ide-p4 = { path = "crates/ide-p4" }
ide-terminal = { path = "crates/ide-terminal" }
ide-run = { path = "crates/ide-run" }
ide-workspace = { path = "crates/ide-workspace" }
+4 -1
View File
@@ -143,9 +143,12 @@ mod tests {
std::fs::create_dir_all(root.join(".openidea")).unwrap();
let (count, on_change) = counting();
let _watch = SourcesWatch::start(&root, None, on_change).unwrap();
// FSEvents can deliver the creation of `.openidea/` above after the watch started.
std::thread::sleep(Duration::from_secs(1));
let seen = count.load(Ordering::SeqCst);
std::fs::write(root.join(".openidea/settings.toml"), "").unwrap();
std::fs::write(root.join("main.py"), "").unwrap();
std::thread::sleep(SETTLE * 3);
assert_eq!(count.load(Ordering::SeqCst), 0);
assert_eq!(count.load(Ordering::SeqCst), seen);
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ use std::path::{Component, Path, PathBuf};
pub const MANIFEST_FILE: &str = "plugin.toml";
/// The plugin API (engine) version this host implements.
pub const ENGINE_VERSION: &str = "1.6.0";
pub const ENGINE_VERSION: &str = "1.7.0";
/// The 0.1 API, still accepted for contribution-only packages and 0.1 components.
pub const LEGACY_API: &str = "0.1";
/// Marketplace categories, in the order they are shown.
+13
View File
@@ -1531,6 +1531,19 @@ impl HostApi for PluginApi {
}
Ok(())
}
fn open_diff(
&self,
client: &str,
diff: ide_plugin::ContentDiff,
file_name: Option<String>,
) -> Result<(), HostError> {
super::execution::validate_diff(&diff)?;
if let Some(name) = &file_name {
super::execution::label(name, 256)?;
}
self.shared.sink.emit(Some(client), PluginEvent::OpenDiff { plugin_id: self.id().into(), diff, file_name });
Ok(())
}
fn reply_content_tab(
&self,
client: &str,
+13 -8
View File
@@ -72,14 +72,19 @@ pub(super) fn validate_reply(reply: &ContentTabReply) -> Result<(), HostError> {
return Err(HostError::Invalid("content status or error too long".into()));
}
if let Some(diff) = &reply.diff {
body(&diff.left)?;
body(&diff.right)?;
for value in [&diff.left_label, &diff.right_label].into_iter().flatten() {
label(value, 128)?;
}
if let Some(title) = &diff.title {
label(title, 256)?;
}
validate_diff(diff)?;
}
Ok(())
}
pub(super) fn validate_diff(diff: &ide_plugin::ContentDiff) -> Result<(), HostError> {
body(&diff.left)?;
body(&diff.right)?;
for value in [&diff.left_label, &diff.right_label].into_iter().flatten() {
label(value, 128)?;
}
if let Some(title) = &diff.title {
label(title, 256)?;
}
Ok(())
}
@@ -188,6 +188,7 @@ pub fn forwarded(event: &PluginEvent) -> bool {
PluginEvent::RunTargetsChanged
| PluginEvent::WindowRequest { .. }
| PluginEvent::ContentTabReply { .. }
| PluginEvent::OpenDiff { .. }
| PluginEvent::DecorationsChanged { .. }
| PluginEvent::EditorOpen { .. }
| PluginEvent::EditorRanges { .. }
+3
View File
@@ -218,6 +218,9 @@ pub enum PluginEvent {
/// Reply to a content action in this window.
#[serde(rename_all = "camelCase")]
ContentTabReply { plugin_id: String, request_id: String, reply: ide_plugin::ContentTabReply },
/// Open two texts in this window's diff viewer.
#[serde(rename_all = "camelCase")]
OpenDiff { plugin_id: String, diff: ide_plugin::ContentDiff, file_name: Option<String> },
/// Plugins, their status or their contributions changed: re-fetch `plugin_list`.
CommandsChanged,
+20
View File
@@ -6352,6 +6352,26 @@ fn api14_stage3_configurations_test_results_and_content_actions_use_real_exports
assert!(f.host.parse_test_results("main", "plugin:execution:dynamic", "trap".into()).unwrap().is_none());
}
#[cfg(unix)]
#[test]
fn a_plugin_command_opens_a_diff_in_its_window() {
let f = execution_fixture();
f.take_events();
f.run("execution", "diff", None).unwrap();
f.host.flush_events_at(Instant::now() + Duration::from_secs(3));
assert!(eventually(|| {
f.events.lock().iter().any(|(client, event)| {
client.as_deref() == Some("main")
&& matches!(event, PluginEvent::OpenDiff { plugin_id, diff, file_name }
if plugin_id == "execution"
&& diff.left == "before\n"
&& diff.right == "after\n"
&& diff.title.as_deref() == Some("Review")
&& file_name.as_deref() == Some("src/review.rs"))
})
}));
}
#[test]
fn api14_fixture_exports_test_selection() {
let f = fixture(Limits::default());
-18
View File
@@ -1,18 +0,0 @@
[package]
name = "ide-p4"
description = "Perforce (Helix Core / P4) integration built on the installed p4 CLI"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
ide-core.workspace = true
serde.workspace = true
serde_json.workspace = true
specta.workspace = true
tracing.workspace = true
which = "8"
regex-lite = "0.1"
[dev-dependencies]
tempfile.workspace = true
-88
View File
@@ -1,88 +0,0 @@
//! Thin wrapper around the `p4` executable used for every operation.
//!
//! Like the Git backend, every Perforce call goes through the installed CLI so
//! `P4CONFIG`, ticket authentication and connection settings behave exactly as
//! on the command line. `-ztag` keeps output machine-readable for the parsers
//! in [`crate::opened`] and [`crate::status`].
use ide_core::{IdeError, IdeResult};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::RwLock;
static P4_EXECUTABLE: RwLock<Option<PathBuf>> = RwLock::new(None);
/// Explicit `p4` executable from Settings › Version Control › Perforce
/// (`None` = auto-detect). Set once when settings load.
#[allow(dead_code)]
pub fn set_p4_executable(path: Option<&Path>) {
*P4_EXECUTABLE.write().unwrap_or_else(|p| p.into_inner()) = path.map(|p| p.to_path_buf());
}
/// The configured or detected `p4` executable.
pub fn p4(exe: Option<&Path>) -> IdeResult<PathBuf> {
if let Some(p) = exe {
return Ok(p.to_path_buf());
}
if let Some(p) = P4_EXECUTABLE.read().unwrap_or_else(|p| p.into_inner()).clone() {
return Ok(p);
}
crate::detected_p4_executable().ok_or_else(|| IdeError::missing_tool("p4"))
}
/// Runs `p4 args` in `cwd`, returning stdout. Stderr is folded into the error
/// message; `p4` prints diagnostics there for a nonzero exit. Connection and
/// authentication failures surface as `Unavailable` so the UI can offer to
/// configure the server.
pub fn run(exe: &Path, args: &[&str], cwd: Option<&Path>) -> IdeResult<String> {
let mut cmd = Command::new(exe);
cmd.args(args);
if let Some(cwd) = cwd {
cmd.current_dir(cwd);
}
let out = cmd.output().map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound { IdeError::missing_tool("p4") } else { IdeError::from(e) }
})?;
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
if !out.status.success() {
let msg = stderr.trim().to_string();
let msg = if msg.is_empty() { stdout.trim().to_string() } else { msg };
return Err(classify(&msg));
}
// `p4` can still report a recoverable error on stdout with a zero exit
// (e.g. "… - file(s) not on client").
if stderr.trim().is_empty() {
return Ok(stdout.into_owned());
}
let warn = stderr.trim();
if warn.starts_with("warning:") {
tracing::warn!(target: "p4", "{warn}");
return Ok(stdout.into_owned());
}
Err(classify(warn))
}
fn classify(msg: &str) -> IdeError {
let m = msg.to_lowercase();
if m.contains("connect to server failed")
|| m.contains("can't connect")
|| m.contains("operation timed out")
|| m.contains("host unknown")
{
return IdeError::unavailable(msg);
}
if m.contains("please login")
|| m.contains("ticket") && m.contains("expired")
|| m.contains("password") && m.contains("required")
{
return IdeError::unavailable(format!("Perforce login required: {msg}"));
}
if m.contains("not on client") || m.contains("no such file") || m.contains("not in client view") {
return IdeError::not_found(msg);
}
if m.contains("can't edit") && m.contains("opened") || m.contains("also opened by") {
return IdeError::new(ide_core::error::ErrorKind::AlreadyExists, msg);
}
IdeError::internal(msg)
}
-322
View File
@@ -1,322 +0,0 @@
//! The Perforce client: connection settings and the write operations
//! (`edit`/`add`/`delete`/`revert`/`sync`/`submit`) plus `changelist` handling.
//!
//! Field names follow `p4 -ztag` output: a tagged record is a set of
//! `... <name> <value>` lines terminated by a blank line, and `code`/`severity`
//! mark informational vs error records.
use ide_core::{IdeError, IdeResult};
use serde::{Deserialize, Serialize};
use specta::Type;
use std::path::{Path, PathBuf};
use crate::cli;
/// Server, user and client workspace of a `p4 info` — everything the settings
/// page needs to configure and confirm a connection.
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct Connection {
/// `P4PORT` the client talks to (`ssl:host:port` when TLS).
pub server_address: String,
/// `P4USER`.
pub user_name: String,
/// `P4CLIENT`, the workspace spec name.
pub client_name: String,
/// Client root on disk (the local top folder of the view).
pub client_root: String,
/// The host the client spec is bound to, when set (`clientHost`).
pub client_host: Option<String>,
/// Server version string from `p4 info` (`serverVersion`).
pub server_version: Option<String>,
}
/// A file opened for a Perforce action (`p4 opened`).
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct OpenedFile {
/// Local path, resolved against the client root.
pub path: ide_core::IpcPath,
/// `depotFile` (`//depot/…`).
pub depot_file: String,
/// `rev` (`#3`) without the `#`.
pub rev: Option<u32>,
/// `action` — the open operation.
pub action: ChangeAction,
/// `change` — the changelist number, `0` for `default`.
pub change: u32,
/// `type` (`text`, `binary`, …).
pub file_type: Option<String>,
/// Locked by this client (`ourLock`) or another (`otherLock`).
pub locked: bool,
/// Other users/clients holding the file open (`otherOpen`).
pub other_open: Vec<String>,
}
/// What an opened file is being changed for (`p4 opened` / `p4 fstat action`).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub enum ChangeAction {
Edit,
Add,
Delete,
Branch,
Integrate,
MoveAdd,
MoveDelete,
/// `archive`/`purge` or an action not on this list.
Other,
}
impl ChangeAction {
pub fn parse(s: &str) -> Self {
match s {
"edit" => Self::Edit,
"add" => Self::Add,
"delete" => Self::Delete,
"branch" => Self::Branch,
"integrate" => Self::Integrate,
"move/add" => Self::MoveAdd,
"move/delete" => Self::MoveDelete,
_ => Self::Other,
}
}
}
/// A Perforce client workspace rooted at `root`, bound to `conn`'s settings.
/// Method calls run `p4` with `-c client -u user -p port` so a `Client` can
/// point at a non-default workspace.
#[derive(Clone)]
pub struct Client {
pub root: PathBuf,
pub conn: Connection,
exe: PathBuf,
}
impl Client {
/// Binds `exe` (`None` = auto-detect) to the workspace `conn` describes.
pub fn new(conn: Connection, exe: Option<&Path>) -> IdeResult<Self> {
let exe = cli::p4(exe)?;
let root = PathBuf::from(&conn.client_root);
Ok(Self { root, conn, exe })
}
/// `p4 info` in `cwd`: the connection the workspace `cwd` belongs to, or
/// `Ok(None)` when `cwd` is not inside a Perforce client.
pub fn info(exe: Option<&Path>, cwd: &Path) -> IdeResult<Option<Connection>> {
let exe = cli::p4(exe)?;
let out = match cli::run(&exe, &["-ztag", "info"], Some(cwd)) {
Ok(o) => o,
Err(e) if matches!(e.kind, ide_core::error::ErrorKind::Unavailable) => return Err(e),
Err(_) => return Ok(None),
};
Ok(Some(parse_info(&out)))
}
fn args(&self, cmd: &str, extra: &[&str]) -> Vec<String> {
let mut a = vec![
"-ztag".to_string(),
"-p".to_string(),
self.conn.server_address.clone(),
"-u".to_string(),
self.conn.user_name.clone(),
"-c".to_string(),
self.conn.client_name.clone(),
cmd.to_string(),
];
a.extend(extra.iter().map(|s| s.to_string()));
a
}
fn run(&self, cmd: &str, extra: &[&str]) -> IdeResult<String> {
let args = self.args(cmd, extra);
cli::run(&self.exe, &args.iter().map(|s| s.as_str()).collect::<Vec<_>>(), Some(&self.root))
}
/// `p4 opened` of `paths` (`//depot/…` or local paths; `[]` = the whole client).
pub fn opened(&self, paths: &[&str]) -> IdeResult<Vec<OpenedFile>> {
let mut extra = vec!["opened"];
extra.extend_from_slice(paths);
let out = self.run("opened", &extra[1..])?;
Ok(crate::opened::parse(&out, &self.conn.client_root))
}
/// `p4 fstat` of `paths`: depot metadata for opened checks and status.
pub fn fstat(&self, paths: &[&str]) -> IdeResult<Vec<OpenedFile>> {
let out = self.run("fstat", paths)?;
Ok(crate::opened::parse_fstat(&out, &self.conn.client_root))
}
/// `p4 edit` — open for edit, `-c change` when not default.
pub fn edit(&self, paths: &[&str], change: Option<u32>) -> IdeResult<()> {
self.open("edit", paths, change)
}
/// `p4 add` — open for add.
pub fn add(&self, paths: &[&str], change: Option<u32>) -> IdeResult<()> {
self.open("add", paths, change)
}
/// `p4 delete` — open for delete.
pub fn delete(&self, paths: &[&str], change: Option<u32>) -> IdeResult<()> {
self.open("delete", paths, change)
}
fn open(&self, action: &str, paths: &[&str], change: Option<u32>) -> IdeResult<()> {
let mut extra: Vec<String> = Vec::new();
if let Some(c) = change.filter(|c| *c != 0) {
extra.push("-c".to_string());
extra.push(c.to_string());
}
extra.extend(paths.iter().map(|s| s.to_string()));
let out = self.run(action, &extra.iter().map(|s| s.as_str()).collect::<Vec<_>>())?;
// `p4 edit` succeeds even when a file is already opened or "not on
// client"; report those lines so callers don't read silence as success.
let problems = out
.lines()
.filter(|l| l.contains("can't") || l.contains("not on client") || l.contains("not in client view"))
.collect::<Vec<_>>();
if !problems.is_empty() {
return Err(IdeError::invalid(problems.join("\n")));
}
Ok(())
}
/// `p4 revert` — restore files to the depot revision, `-a` reverts
/// unchanged files only.
pub fn revert(&self, paths: &[&str], unchanged_only: bool) -> IdeResult<()> {
let mut extra = Vec::new();
if unchanged_only {
extra.push("-a");
}
extra.extend_from_slice(paths);
self.run("revert", &extra).map(|_| ())
}
/// `p4 sync` — bring the workspace to `paths`' head revisions.
pub fn sync(&self, paths: &[&str], force: bool) -> IdeResult<String> {
let mut extra = Vec::new();
if force {
extra.push("-f");
}
extra.extend_from_slice(paths);
self.run("sync", &extra)
}
/// `p4 describe -s change`: files and the description of a submitted or
/// pending changelist.
pub fn describe(&self, change: u32) -> IdeResult<String> {
self.run("describe", &["-s", &change.to_string()])
}
/// `p4 changes -m N -s pending -c client` — the client's pending changelists.
pub fn changelists(&self, max: u32) -> IdeResult<Vec<P4Changelist>> {
let out = self.run("changes", &["-m", &max.to_string(), "-s", "pending", "-c", &self.conn.client_name])?;
Ok(parse_changelists(&out))
}
/// `p4 submit -d "desc"` — submits the default or `-c change` list.
pub fn submit(&self, description: &str, change: Option<u32>) -> IdeResult<String> {
let mut extra = vec!["-d".to_string(), description.to_string()];
if let Some(c) = change.filter(|c| *c != 0) {
extra.push("-c".to_string());
extra.push(c.to_string());
}
self.run("submit", &extra.iter().map(|s| s.as_str()).collect::<Vec<_>>())
}
}
/// A pending changelist summary (`p4 changes`).
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct P4Changelist {
pub change: u32,
pub user: String,
pub client: String,
/// First line of the description.
pub description: String,
}
/// `p4 -ztag info` → [`Connection`]. Field names are those `info` emits:
/// `serverAddress`, `userName`, `clientName`, `clientRoot`, `clientHost`,
/// `serverVersion`.
fn parse_info(out: &str) -> Connection {
let field = |name: &str| -> Option<String> {
out.lines()
.find_map(|l| l.strip_prefix(&format!("... {name} ")).map(str::to_string))
.or_else(|| out.lines().find_map(|l| l.strip_prefix(&format!("{name}: ")).map(str::to_string)))
};
Connection {
server_address: field("serverAddress").unwrap_or_default(),
user_name: field("userName").unwrap_or_default(),
client_name: field("clientName").unwrap_or_default(),
client_root: field("clientRoot").unwrap_or_default(),
client_host: field("clientHost"),
server_version: field("serverVersion"),
}
}
/// `p4 changes -ztag` records: `... change N`, `... user`, `... client`,
/// `... desc` (the description's first line, often followed by `...` continuations).
fn parse_changelists(out: &str) -> Vec<P4Changelist> {
let mut list = Vec::new();
let mut cur = P4Changelist { change: 0, user: String::new(), client: String::new(), description: String::new() };
let mut seen = false;
for line in out.lines() {
let Some(f) = line.strip_prefix("... ") else { continue };
let (name, value) = f.split_once(' ').map(|(a, b)| (a, b.trim())).unwrap_or((f, ""));
match name {
"change" => {
if seen {
list.push(std::mem::replace(
&mut cur,
P4Changelist {
change: 0,
user: String::new(),
client: String::new(),
description: String::new(),
},
));
}
seen = true;
cur.change = value.parse().unwrap_or(0);
}
"user" => cur.user = value.to_string(),
"client" => cur.client = value.to_string(),
"desc" if cur.description.is_empty() => cur.description = value.to_string(),
_ => {}
}
}
if seen {
list.push(cur);
}
list
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_tagged_info() {
let out = "... serverAddress ssl:perforce:1666\n... userName build\n... clientName build-ws\n\
... clientRoot /ws/build\n... serverVersion P4D/LINUX26X86_64/2024.1\n\n";
let c = parse_info(out);
assert_eq!(c.server_address, "ssl:perforce:1666");
assert_eq!(c.user_name, "build");
assert_eq!(c.client_name, "build-ws");
assert_eq!(c.client_root, "/ws/build");
assert_eq!(c.server_version.as_deref(), Some("P4D/LINUX26X86_64/2024.1"));
}
#[test]
fn parses_changes() {
let out = "... change 12\n... user dev\n... client dev-ws\n... desc 'fix login'\n\n\
... change 7\n... user dev\n... client dev-ws\n... desc 'wip'\n\n";
let l = parse_changelists(out);
assert_eq!(l.len(), 2);
assert_eq!(l[0].change, 12);
assert_eq!(l[0].description, "'fix login'");
assert_eq!(l[1].change, 7);
}
}
-53
View File
@@ -1,53 +0,0 @@
//! Perforce (Helix Core / P4) integration, built on the installed `p4` CLI.
//!
//! Reads (`p4 info`, `p4 fstat`, `p4 opened`, `p4 describe`) and writes
//! (`p4 edit`, `add`, `delete`, `revert`, `sync`, `submit`, `shelve`) all shell
//! out so credentials, tickets and `P4CONFIG` behave exactly as on the command
//! line. The [`Client`] is configured like the `p4` binary itself: `P4PORT`,
//! `P4USER`, `P4CLIENT` come from the environment or a `p4 set` override.
mod cli;
mod client;
mod opened;
mod status;
pub use client::{ChangeAction, Client, Connection, OpenedFile, P4Changelist};
pub use status::{P4FileStatus, Workspace};
use ide_core::IdeResult;
use std::path::Path;
/// The detected `p4` executable, or `None` when it is not on `PATH`.
pub fn detected_p4_executable() -> Option<std::path::PathBuf> {
which::which("p4").ok().or_else(|| {
[
r"C:\Program Files\Perforce\p4.exe",
r"C:\Program Files (x86)\Perforce\p4.exe",
"/usr/local/bin/p4",
"/usr/bin/p4",
"/opt/perforce/bin/p4",
]
.iter()
.map(std::path::Path::new)
.find(|p| p.exists())
.map(|p| p.to_path_buf())
})
}
/// `p4 -V` of `exe` (`None` = auto-detect), for Settings › Version Control › Perforce.
pub fn p4_version(exe: Option<&Path>) -> IdeResult<String> {
let exe = cli::p4(exe)?;
let out = cli::run(&exe, &["-V"], None)?;
Ok(out
.lines()
.find(|l| l.contains("Rev.") || l.contains("Version"))
.map(str::trim)
.map(str::to_string)
.unwrap_or_else(|| out.trim().to_string()))
}
/// The workspace `path` belongs to (`p4 info` walks up for a `P4CONFIG`), or
/// `Ok(None)` when `path` is not inside a Perforce client.
pub fn discover_workspace(path: &Path) -> IdeResult<Option<Workspace>> {
Workspace::discover(path)
}
-123
View File
@@ -1,123 +0,0 @@
//! `p4 opened` / `p4 fstat` tagged-output parsing → [`OpenedFile`].
//!
//! `p4 -ztag` emits one record per file as `... <name> <value>` lines ending in
//! a blank line. `opened` reports `depotFile`, `rev`, `action`, `change`,
//! `client`, `type` plus lock fields (`ourLock`, `otherLock`, `otherOpen`);
//! `fstat` reports the same `depotFile`/`action` plus `clientFile`/`isMapped`
//! for files not yet opened.
use crate::client::{ChangeAction, OpenedFile};
use std::path::{Path, PathBuf};
/// One `-ztag` record's fields.
fn records(out: &str) -> Vec<Vec<(String, String)>> {
let mut all = Vec::new();
let mut cur = Vec::new();
for line in out.lines() {
if let Some(f) = line.strip_prefix("... ") {
let (name, value) = f
.split_once(' ')
.map(|(a, b)| (a.to_string(), b.to_string()))
.unwrap_or((f.to_string(), String::new()));
cur.push((name, value));
} else if line.trim().is_empty() && !cur.is_empty() {
all.push(std::mem::take(&mut cur));
}
}
if !cur.is_empty() {
all.push(cur);
}
all
}
fn field<'a>(rec: &'a [(String, String)], name: &str) -> Option<&'a str> {
rec.iter().find(|(n, _)| n == name).map(|(_, v)| v.as_str())
}
fn field_all<'a>(rec: &'a [(String, String)], name: &str) -> Vec<&'a str> {
rec.iter().filter(|(n, _)| n == name).map(|(_, v)| v.as_str()).collect()
}
/// `p4 opened -ztag` records → opened files, local paths resolved under `root`.
pub fn parse(out: &str, root: &str) -> Vec<OpenedFile> {
records(out).iter().filter(|rec| field(rec, "depotFile").is_some()).map(|rec| to_opened(rec, root)).collect()
}
/// `p4 fstat -ztag` records → opened files (files with an `action`), plus
/// mapped-but-unopened entries when `clientFile` is present.
pub fn parse_fstat(out: &str, root: &str) -> Vec<OpenedFile> {
records(out)
.iter()
.filter(|rec| field(rec, "depotFile").is_some() || field(rec, "clientFile").is_some())
.map(|rec| to_opened(rec, root))
.collect()
}
fn to_opened(rec: &[(String, String)], root: &str) -> OpenedFile {
let depot = field(rec, "depotFile").unwrap_or_default().to_string();
let client_file = field(rec, "clientFile").or_else(|| field(rec, "depotFile")).unwrap_or_default();
let path = local_path(root, client_file, &depot);
OpenedFile {
path: ide_core::IpcPath(path),
depot_file: depot,
rev: field(rec, "rev").and_then(|r| r.trim_start_matches('#').parse().ok()),
action: ChangeAction::parse(field(rec, "action").unwrap_or("")),
change: field(rec, "change").and_then(|c| c.parse().ok()).unwrap_or(0),
file_type: field(rec, "type").map(str::to_string),
locked: field(rec, "ourLock").is_some() || field(rec, "otherLock").is_some(),
other_open: field_all(rec, "otherOpen").into_iter().map(str::to_string).collect(),
}
}
/// `clientFile` is a local absolute path on the client; `depotFile` is
/// `//depot/…` and must be mapped through the client view. Without a view we
/// fall back to the depot path minus its `//depot/` prefix under `root`.
fn local_path(root: &str, client_file: &str, depot_file: &str) -> PathBuf {
let cf = client_file.trim();
if !cf.is_empty() && !cf.starts_with("//") {
let p = PathBuf::from(cf);
if p.is_absolute() {
return p;
}
}
// depot → client: strip the leading `//<depot>/` and join under root.
let rel = depot_file.trim_start_matches("//").split_once('/').map(|(_, r)| r).unwrap_or(depot_file);
Path::new(root).join(rel)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_opened_records() {
let out = "... depotFile //depot/src/a.rs\n... rev 3\n... action edit\n\
... change default\n... type text\n... user dev\n... client dev-ws\n\n\
... depotFile //depot/src/b.rs\n... rev 1\n... action add\n\
... change 42\n... type text\n... ourLock\n\n";
let files = parse(out, "/ws/dev");
assert_eq!(files.len(), 2);
assert_eq!(files[0].depot_file, "//depot/src/a.rs");
assert_eq!(files[0].action, ChangeAction::Edit);
assert_eq!(files[0].rev, Some(3));
assert_eq!(files[1].action, ChangeAction::Add);
assert_eq!(files[1].change, 42);
assert!(files[1].locked);
}
#[test]
fn local_path_prefers_client_file() {
assert_eq!(local_path("/ws", "/ws/src/a.rs", "//depot/src/a.rs"), PathBuf::from("/ws/src/a.rs"));
assert_eq!(local_path("/ws", "", "//depot/src/a.rs"), PathBuf::from("/ws/src/a.rs"));
}
#[test]
fn parses_fstat_unopened() {
let out = "... depotFile //depot/src/c.rs\n... clientFile /ws/src/c.rs\n\
... isMapped\n... headRev 5\n... type text\n\n";
let files = parse_fstat(out, "/ws");
assert_eq!(files.len(), 1);
assert_eq!(files[0].path.0, PathBuf::from("/ws/src/c.rs"));
assert_eq!(files[0].action, ChangeAction::Other);
}
}
-193
View File
@@ -1,193 +0,0 @@
//! Perforce file status: which files are opened, modified against have, or
//! untracked, mapped onto the shared [`ide_vcs`]-style status model.
//!
//! Perforce has no index: a file is *opened* (staged) or it is not, and a
//! opened file that differs on disk is also *modified* (unstaged). Untracked is
//! a file under the client root that `p4 fstat` reports unmapped — matching
//! `FileStatus`'s staged/unstaged split so the VCS tool window can show P4
//! changes with the same rows it uses for Git.
use crate::client::{Client, Connection};
use ide_core::IdeResult;
use serde::{Deserialize, Serialize};
use specta::Type;
use std::path::{Path, PathBuf};
use crate::client::ChangeAction;
/// One file's Perforce status, aligned to the Git `FileStatus` shape.
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct P4FileStatus {
pub path: ide_core::IpcPath,
pub rel_path: String,
/// Opened to a changelist (`staged` equivalent).
pub opened: Option<ChangeAction>,
/// Differs from the have revision on disk (`unstaged` equivalent).
pub modified: bool,
/// Under the client root but not in the depot (`untracked`).
pub untracked: bool,
/// Changelist the file is opened to (`0` = default).
pub change: u32,
}
/// A Perforce workspace (client root + its connection), discovered under a path.
#[derive(Clone)]
pub struct Workspace {
pub root: PathBuf,
pub conn: Connection,
exe: Option<PathBuf>,
}
impl Workspace {
/// The workspace `path` belongs to (`p4 info`), or `Ok(None)` when `path`
/// is not inside a Perforce client root.
pub fn discover(path: &Path) -> IdeResult<Option<Self>> {
let exe = crate::detected_p4_executable();
Self::discover_with(path, exe.as_deref())
}
/// `discover` with an explicit `p4` executable (tests, settings override).
pub fn discover_with(path: &Path, exe: Option<&Path>) -> IdeResult<Option<Self>> {
let start = if path.is_file() { path.parent().unwrap_or(path) } else { path };
let mut dir = start;
loop {
if let Some(conn) = Client::info(exe, dir)? {
if conn.client_root.is_empty() {
return Ok(None);
}
let root = PathBuf::from(&conn.client_root);
return Ok(Some(Workspace { root, conn, exe: exe.map(|p| p.to_path_buf()) }));
}
match dir.parent() {
Some(p) => dir = p,
None => return Ok(None),
}
}
}
/// The bound [`Client`] for write operations.
pub fn client(&self) -> IdeResult<Client> {
Client::new(self.conn.clone(), self.exe.as_deref())
}
/// Status of every file under `paths` (relative or absolute; `[]` = the
/// whole client). Combines `p4 opened` (staged) with `p4 fstat` (modified
/// against have) and a walk for untracked files.
pub fn status(&self, paths: &[&str]) -> IdeResult<Vec<P4FileStatus>> {
let client = self.client()?;
let specs: Vec<String> = paths.iter().map(|p| self.to_spec(p)).collect();
let specs_ref: Vec<&str> = specs.iter().map(|s| s.as_str()).collect();
let opened = client.opened(&specs_ref).unwrap_or_default();
let mut by_path: std::collections::HashMap<PathBuf, P4FileStatus> = std::collections::HashMap::new();
for of in opened {
let rel = self.rel(&of.path.0);
by_path.insert(
of.path.0.clone(),
P4FileStatus {
path: of.path.clone(),
rel_path: rel,
opened: Some(of.action),
modified: of.action != ChangeAction::Other,
untracked: false,
change: of.change,
},
);
}
// fstat marks files differing from have (`status`/`...diff`) so a synced
// but locally-edited file shows Modified even before `p4 edit`.
for of in client.fstat(&specs_ref).unwrap_or_default() {
let entry = by_path.entry(of.path.0.clone()).or_insert_with(|| P4FileStatus {
path: of.path.clone(),
rel_path: self.rel(&of.path.0),
opened: None,
modified: false,
untracked: false,
change: 0,
});
if of.action != ChangeAction::Other {
entry.opened = Some(of.action);
entry.modified = true;
}
}
Ok(by_path.into_values().collect())
}
/// Local path or depot spec → a `p4` path spec. Absolute local paths and
/// `//depot/…` pass through; bare names become `root/name`.
fn to_spec(&self, p: &str) -> String {
if p.starts_with("//") || Path::new(p).is_absolute() {
return p.to_string();
}
self.root.join(p).to_string_lossy().into_owned()
}
fn rel(&self, path: &Path) -> String {
path.strip_prefix(&self.root)
.map(|r| r.to_string_lossy().into_owned())
.unwrap_or_else(|_| path.to_string_lossy().into_owned())
}
}
/// Marks a file read-write so `p4 edit` can open it (Perforce keeps unopened
/// files read-only). Called by the checkout prompt for read-only depot files.
#[allow(dead_code)]
pub fn make_writable(path: &Path) -> IdeResult<()> {
let mut perms = std::fs::metadata(path)?.permissions();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
perms.set_mode(perms.mode() | 0o200);
}
#[cfg(windows)]
{
#[allow(clippy::permissions_set_readonly_false)]
perms.set_readonly(false);
}
std::fs::set_permissions(path, perms)?;
Ok(())
}
/// Whether `path` is read-only on disk — the signal for "checkout on edit".
#[allow(dead_code)]
pub fn is_readonly(path: &Path) -> bool {
std::fs::metadata(path).map(|m| m.permissions().readonly()).unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn to_spec_maps_relative_paths() {
let w = Workspace {
root: PathBuf::from("/ws"),
conn: crate::client::Connection {
server_address: "perforce:1666".into(),
user_name: "u".into(),
client_name: "c".into(),
client_root: "/ws".into(),
client_host: None,
server_version: None,
},
exe: None,
};
assert_eq!(w.to_spec("//depot/x.rs"), "//depot/x.rs");
assert_eq!(w.to_spec("/abs/x.rs"), "/abs/x.rs");
assert_eq!(w.to_spec("src/x.rs"), Path::new("/ws").join("src/x.rs").to_string_lossy());
}
#[test]
fn readonly_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let f = dir.path().join("a.txt");
std::fs::write(&f, "x").unwrap();
assert!(!is_readonly(&f));
let mut p = std::fs::metadata(&f).unwrap().permissions();
p.set_readonly(true);
std::fs::set_permissions(&f, p).unwrap();
assert!(is_readonly(&f));
make_writable(&f).unwrap();
assert!(!is_readonly(&f));
}
}
+6 -1
View File
@@ -55,7 +55,7 @@ use views_bindings::exports::openidea::plugin::view_provider as wit_view;
use views_bindings::openidea::plugin::view_types as wit_vt;
/// The API version of the world above, `major.minor` of [`host_api`].
pub const API_VERSION: &str = "1.6";
pub const API_VERSION: &str = "1.7";
const EPOCH_TICK: Duration = Duration::from_millis(10);
pub(crate) const MAX_TEXT: usize = 1 << 20;
pub(crate) const MAX_TITLE: usize = 256;
@@ -170,6 +170,11 @@ pub trait HostApi: Send + Sync {
let _ = (client, tab, cancel, timeout);
Err(HostError::Failed("content tabs unavailable".into()))
}
/// Opens a diff of two texts in the invoking window.
fn open_diff(&self, client: &str, diff: crate::ContentDiff, file_name: Option<String>) -> Result<(), HostError> {
let _ = (client, diff, file_name);
Err(HostError::Failed("diffs unavailable".into()))
}
/// Answers a content action in the invoking window.
fn reply_content_tab(&self, client: &str, request: &str, reply: crate::ContentTabReply) -> Result<(), HostError> {
let _ = (client, request, reply);
+6
View File
@@ -1770,6 +1770,12 @@ impl views::Host for StoreData {
)
.map_err(to_wit))
}
fn open_diff(&mut self, diff: views::ContentDiff, file_name: Option<String>) -> Fallible<()> {
if let Err(e) = self.require_window() {
return Ok(Err(e));
}
Ok(self.host.open_diff(self.invocation.client.as_deref().unwrap(), diff.into(), file_name).map_err(to_wit))
}
fn reply_content_tab(&mut self, request_id: String, reply: views::ContentTabReply) -> Fallible<()> {
if let Err(e) = self.require_window() {
return Ok(Err(e));
+5 -5
View File
@@ -91,15 +91,15 @@ fn the_sdk_carries_the_hosts_wit() {
#[test]
fn the_version_check_runs_compatible_components_and_explains_the_rest() {
let host = v("1.6.0");
for ok in ["1.0.0", "1.3.0", "1.4.7", "1.5.0", "1.6.0", "1.0.0-rc.1"] {
let host = v("1.7.0");
for ok in ["1.0.0", "1.3.0", "1.4.7", "1.5.0", "1.6.0", "1.7.0", "1.0.0-rc.1"] {
assert_eq!(check_api(&v(ok), &host), Ok(()), "{ok}");
}
let newer = check_api(&v("1.7.0"), &host).unwrap_err();
assert!(newer.contains("1.7") && newer.contains("update OpenIDEA"), "{newer}");
let newer = check_api(&v("1.8.0"), &host).unwrap_err();
assert!(newer.contains("1.8") && newer.contains("update OpenIDEA"), "{newer}");
assert!(check_api(&v("2.0.0"), &host).unwrap_err().contains("update OpenIDEA"));
let older = check_api(&v("0.2.0"), &host).unwrap_err();
assert!(older.contains("rebuild it with the 1.6 SDK"), "{older}");
assert!(older.contains("rebuild it with the 1.7 SDK"), "{older}");
// 0.x: every minor is its own API.
assert_eq!(check_api(&v("0.2.0"), &v("0.2.5")), Ok(()));
assert!(check_api(&v("0.1.0"), &v("0.2.0")).is_err());
Binary file not shown.
@@ -1 +1 @@
005d9da29eb7d713a97ff872051e408f653030434d837835843298553c9e12e0
3c7f35facd15ee8380ccb6475b7c051d500e38346b711e1805077becd0c75f75
Binary file not shown.
@@ -1 +1 @@
6359ef4271f38374889412bd926d9aa735f59a8ce047ffcb06c47976444196cf
3879822d975695c6258e95286b0e64c5ee1e3265876501efaf0e577b38f8222b
Binary file not shown.
@@ -1 +1 @@
6f560f3d0d7737150d2dfa09c3b801bd3b4c3a52cef9893ed8fc045809fe33e1
228ae888bf556ce5cedc7843eb20e92f221d9e1f640f3c6c595bed3fd6be6051
Binary file not shown.
@@ -1 +1 @@
4024af2d55d7fe4e8c456efd7e0c497ade4b2c5959e9108ff218f4a669e920f4
41de239b6f723020b38eb2b67f642b7c88cc88c79117c3072ba122633afc418c
Binary file not shown.
@@ -1 +1 @@
4ca977f53a18e9e8f869d34e027a15abece9e97b9331e23d4f71af721b104611
78c5ed3c756498c7619b46c3622ae7a59461c61c747b45caecd79b341ee654de
Binary file not shown.
@@ -1 +1 @@
3d12a00f81e0db1b0902c2ddebfd6454a056f15b97add80b22a0333722568dc8
ee848321e53cb181536e6158570dbb4ab02a6fd2a1a064b068803ac7da7f3448
Binary file not shown.
@@ -1 +1 @@
95d89e411216f50d0b34bee110c6bfc9b8fbcb2ffaae2e912c3609f7bf678b2c
b587de945875a98f1d4ca20fab7cf94fb4dc6d848136aa19ab0e0e5319155a80
@@ -40,6 +40,10 @@ title = "write"
id = "content"
title = "content"
[[contributes.commands]]
id = "diff"
title = "diff"
[[contributes.commands]]
id = "events"
title = "events"
+12
View File
@@ -78,6 +78,18 @@ impl Plugin for Execution {
})?;
Ok(None)
}
"diff" => {
host::open_diff(
&ContentDiff {
left: "before\n".into(),
right: "after\n".into(),
title: Some("Review".into()),
..Default::default()
},
Some("src/review.rs"),
)?;
Ok(None)
}
"events" => host::storage::get("events"),
"validation" => host::storage::get("validation"),
_ => Err(Error::NotFound(id.into())),
Binary file not shown.
@@ -1 +1 @@
dd4b6c5153892a4f3facb3f23cb1d92da39a3977985b8abf4eaa9558decfa4a9
1c8776dcae35d2d49dd6c1e32b0cdd354de1eb0f72a473245a2c47368b89ec30
Binary file not shown.
@@ -1 +1 @@
325bc15cd28933d9687bc3c316008f921f48a4a69d7528c6b367fa0ff22489b8
2a517cbffd6d3cdda37117d8ca8815978a09995923fc3d5aa5af7020afdac56a
Binary file not shown.
@@ -1 +1 @@
abe706392e04ef5a14f9feab88e0c0d6a9e96bee7de771b61deeb65e7d9e0a7c
dbd912b1f8488ecd446af07b5dcaef55d44bce57c1a55be5c0b0fb33e2697903
Binary file not shown.
@@ -1 +1 @@
fb3677946021b30d226e2e3d422c352645c2065c93057c0a645246520860fc90
efdc84cd2bedb3989b03d19b569d09958f26e502db65db4dd786eb3438a7c407
Binary file not shown.
@@ -1 +1 @@
a01852ead9004ac1558028da19a1ae96ccd035775484089fd39f14263082daf1
be6bc15ee42a5e21781ba54f548007351a46b669882ee4491da20ff556927a06
Binary file not shown.
@@ -1 +1 @@
bf00050b7bb9911680a94fd36aaeb83fc96d94030e8ceef51d1723c3950253f9
e5c3a3beda750dff27d7b823676d53f15b7c76b8ad539594cd913bd178d750da
Binary file not shown.
@@ -1 +1 @@
d00a07d9d51e7ca117cec59070d2812d4d1efb2730b60a6f171c8fd12903d055
322de3299f0413101c02358836e7df7e06e58cbb22c11897664564abbb8af090
Binary file not shown.
@@ -1 +1 @@
0aa0ee56e8bacd4d218bafecdec7f4b6249381fad6e31f8b2ee89fd88b16d710
d768ff6a4a466cc6f1d72499df3ccb4ae76872436b8eddff27f62aa156331a37
Binary file not shown.
@@ -1 +1 @@
e06955818969808e6695fba9178727239e343bc4542efbf56262dd407175fd8a
57aea107b2c10e4d30fa3785ac4c6566f1b40908a18a3859aca8be047fac9518
@@ -1 +1 @@
7af67673471d49f97aa2cc131b5deddfc78ee7fc203901eba12f4970a5b13f75
c98bcc89750e26e738d7115a7c0454fde25367a0a23bdc2ff95fb5a5dacc9f98
Binary file not shown.
@@ -1 +1 @@
e81d2454a61977ab69dc8b5c3c617627c559c4e340337a3d43309daeb9b1c12d
a4c23b93a14eda705cb18c88bbc2373d1ef3f96031ff2df21b8b092b50633996
+3 -3
View File
@@ -5,9 +5,9 @@ wit_bindgen::generate!({
inline: "
package fixture:misbehave;
world misbehave {
include openidea:plugin/plugin-syntax@1.6.0;
export openidea:plugin/providers@1.6.0;
export openidea:plugin/file-actions@1.6.0;
include openidea:plugin/plugin-syntax@1.7.0;
export openidea:plugin/providers@1.7.0;
export openidea:plugin/file-actions@1.7.0;
}
",
path: "../../../../../wit",
Binary file not shown.
@@ -1 +1 @@
123ae68df6baa3b5983349ba05fb441ec67fddcb32cab566b0d02e443bb4800e
aec002b3dd310068e69a6ae67a49d30655bf8baea9153a110a407e9d408e6880
Binary file not shown.
@@ -1 +1 @@
7b26a4ec36b496f9a4f5e029ef557707961a281e31508863c989a5a5abd5e225
08ece563c3b55c2b86e68fbb0815ddcbb63beb6f1f6c32b8dbdbee558a4a66c1
Binary file not shown.
@@ -1 +1 @@
67f278ade12d11b1445ac3d448853ec46282f2f3e2b2299c6c8fe0dfb4fc86cd
450845edf234a1cc78cd6cfc0b812a89f3a2e41484689cc1e1b05be66873d4eb
Binary file not shown.
@@ -1 +1 @@
8cf9210844a1968f485e6e747f6dff4a1fdf0a628e2945f731d8289dd01f8385
61b735de5ef61dcd0ea8e640c2ad004e84a63fe4dfd7872ffb5e04d6d81dc9a9
Binary file not shown.
@@ -1 +1 @@
263e6ab4b54593f09ae2cf71e9a191acb95ed744cc16bbaf9a9263e41614e99a
36776367782eaa4b8b8121f4a665dd8e68525a4836bff1c92bc16770cce14929
Binary file not shown.
@@ -1 +1 @@
682a712b6c75ad0c0da24476f87c77a76b248c201d4d31923a79fda053b76b90
f6311d232a99356b02a398b2ce3db0865c425f435a8d201157719178ba2b97c7
Binary file not shown.
@@ -1 +1 @@
c5718b3978f21291cfff7a00233ba68d545a5977bc3e90c546b4380528295446
fe1d26665e3c816ed43fc720acabad03a37b7924bccd4dd8c357fd156947b3c7
Binary file not shown.
@@ -1 +1 @@
186b30c785fae23d18c5efe8fa63f6526d23d9d141580deef7ee7009310a88c3
76bab8e48235f9f066f5b9bbfdd9398916b6c3ed73c072c543d80595130519f7
Binary file not shown.
@@ -1 +1 @@
cb3a1c4a0518aa3f3e689337e8f4249180e5b3753b0e7bfaba8c974f89f6ef7a
f9ed2eac306229f76b05d0c4a4fb2f0ed8d81a1dfc5eb50d044bbe10c9206a5f
Binary file not shown.
@@ -1 +1 @@
fa97682ca30cb0ad1392ff1bab4d99ea6d9ccb2654feedfdc130d11adb7df93b
f2d83d9f56da072d38a1552195af8a9d4edc11888c645627d5dd1e4a46afc4df
@@ -19,7 +19,7 @@ The plugin host runs **where the project is**: in the desktop app for local proj
## The interface
The API is defined in WIT, in `wit/` (package `openidea:plugin@1.6.0`). A plugin is a component targeting the `plugin` world (or `plugin-syntax`). The guest SDK `sdk/rust/openidea-plugin` wraps it in a typed Rust API and carries a copy of `wit/`; `crates/ide-plugin/tests/api_compat.rs` checks the copies are identical.
The API is defined in WIT, in `wit/` (package `openidea:plugin@1.7.0`). A plugin is a component targeting the `plugin` world (or `plugin-syntax`). The guest SDK `sdk/rust/openidea-plugin` wraps it in a typed Rust API and carries a copy of `wit/`; `crates/ide-plugin/tests/api_compat.rs` checks the copies are identical.
Released API versions are frozen. wasmtime resolves interface names by semver, but function types must match exactly, so a released 1.x only grows: new interfaces and new optional exports, never changed records or variants. See _Evolving the plugin API_ in `plugins/README.md` before touching `wit/`.
+1 -1
View File
@@ -3,7 +3,7 @@ title: "API reference"
description: "Every interface of the openidea:plugin WIT package, with a one-line summary and a link to its source."
---
The plugin API is the WIT package `openidea:plugin@1.6.0` in [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit). The WIT files are the reference: each interface, type and function is documented in comments there. 1.6.0 is the version on `main`; the latest released version is 1.4.0, whose WIT is kept in [`sdk/rust/openidea-plugin/tests/api/released/`](https://github.com/l0ng-ai/openidea/tree/main/sdk/rust/openidea-plugin/tests/api/released).
The plugin API is the WIT package `openidea:plugin@1.7.0` in [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit). The WIT files are the reference: each interface, type and function is documented in comments there. 1.7.0 is the version on `main`; the latest released version is 1.6.0, whose WIT is kept in [`sdk/rust/openidea-plugin/tests/api/released/`](https://github.com/l0ng-ai/openidea/tree/main/sdk/rust/openidea-plugin/tests/api/released).
<Info>
A generated API reference is planned. Until then, use the WIT files below, `cargo doc` for the Rust SDK
+1 -1
View File
@@ -9,7 +9,7 @@ A code plugin is a WebAssembly **component** (component model, WASI preview 2) t
| Part | Where | Role |
| ---------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Plugin API | [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit) | The contract: the WIT package `openidea:plugin@1.6.0`, its interfaces and worlds |
| Plugin API | [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit) | The contract: the WIT package `openidea:plugin@1.7.0`, its interfaces and worlds |
| Rust SDK | [`sdk/rust/openidea-plugin`](https://github.com/l0ng-ai/openidea/tree/main/sdk/rust/openidea-plugin) | Typed wrappers over the WIT, the `Plugin` trait, `register_plugin!`, and a mock host for `cargo test` |
| Packages | [`crates/ide-extension`](https://github.com/l0ng-ai/openidea/tree/main/crates/ide-extension) | The manifest, capabilities and their risk, the grant ledger, contribution points, `.oiplugin` packages and the marketplace index |
| Runtime | [`crates/ide-plugin`](https://github.com/l0ng-ai/openidea/tree/main/crates/ide-plugin) | The wasmtime engine, one `Store` per plugin, limits, and one actor thread per active plugin |
+11 -9
View File
@@ -281,6 +281,7 @@ With `engines.openidea = "^1.3"`, a page can send JSON messages through
| `pty-spawn`, `pty-input`, `pty-resize`, `pty-kill`, `pty-ack` | An interactive PTY owned by the frame; output arrives as `pty-data`, `pty-exit`, `pty-error`. At most 8 sessions per plugin. |
| `open-log-tab` | A read-only editor tab following a program's output, with argument toggles. |
| `open-content-tab` | An editor tab on content the plugin supplies, with toolbar actions answered by `content-tab-reply`. |
| `open-diff` | API 1.7: the diff viewer on two texts (`left`, `right`, optional labels, `title` and `fileName` for highlighting). |
The message fields and limits are in the
[plugin guide](https://github.com/l0ng-ai/openidea/blob/main/plugins/README.md#web-frame-ptys-api-13);
@@ -338,15 +339,16 @@ divider. The host asks after the item's `select` event and again after `refresh_
These are called from a command or event of a window:
| Function | Opens |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `host::open_location(path, line, column)` | A project file in an editor, caret at the position (`project.read`; a file in the plugin's private directory also works). |
| `host::open_editor(path, selection)` | API 1.4: open a project file and select a range. |
| `host::open_terminal(title, program, args)` | A Terminal tab running an approved `process:<program>` in the project root. |
| `host::open_terminal_session(key, title, &cmd)` | API 1.4: a keyed terminal; reopening the key focuses it. Write to it with `host::write_terminal(key, text)`; its exit arrives as a `terminal-exited` event. |
| `host::open_log_tab(key, title, &log)` | API 1.3: the read-only log tab, from a `ProcessLog`. At most 32 per window; not yet in remote projects. |
| `host::open_content_tab(&ContentTab { .. })` | API 1.4: a content editor tab without a web view; actions arrive as `content-tab-action` events, answered with `host::reply_content_tab`. |
| `host::output_append(channel, text)` | API 1.4: a read-only **Output** tab per channel (32 channels, 1 MiB each); `output_clear`, `output_reveal`. |
| Function | Opens |
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `host::open_location(path, line, column)` | A project file in an editor, caret at the position (`project.read`; a file in the plugin's private directory also works). |
| `host::open_editor(path, selection)` | API 1.4: open a project file and select a range. |
| `host::open_terminal(title, program, args)` | A Terminal tab running an approved `process:<program>` in the project root. |
| `host::open_terminal_session(key, title, &cmd)` | API 1.4: a keyed terminal; reopening the key focuses it. Write to it with `host::write_terminal(key, text)`; its exit arrives as a `terminal-exited` event. |
| `host::open_log_tab(key, title, &log)` | API 1.3: the read-only log tab, from a `ProcessLog`. At most 32 per window; not yet in remote projects. |
| `host::open_content_tab(&ContentTab { .. })` | API 1.4: a content editor tab without a web view; actions arrive as `content-tab-action` events, answered with `host::reply_content_tab`. |
| `host::open_diff(&ContentDiff { .. }, file_name)` | API 1.7: the diff viewer on two texts, such as a review's file before and after; `file_name` picks the syntax highlighting. |
| `host::output_append(channel, text)` | API 1.4: a read-only **Output** tab per channel (32 channels, 1 MiB each); `output_clear`, `output_reveal`. |
```rust
use openidea_plugin::{ContentAction, ContentTab, host};
+2 -2
View File
@@ -36,8 +36,8 @@ Code plugins are WebAssembly components targeting the `plugin` world in [`wit/`]
| | |
| ------------------------- | ------------------------------------------------------------ |
| Latest released API | **1.4.0**, shipped with OpenIDEA 0.1.40 |
| In development | **1.6.0** on the `main` branch, planned for the next release |
| Latest released API | **1.6.0**, shipped with OpenIDEA 0.1.43 |
| In development | **1.7.0** on the `main` branch, planned for the next release |
| Format of the WIT package | `openidea:plugin@<version>` |
Declare the API your plugin needs with `engines.openidea` in the manifest, for example `engines.openidea = "^1.4"`.
+11 -10
View File
@@ -3,7 +3,7 @@ title: "API versions and compatibility"
description: "engines.openidea, the plugin API versions from 1.0 to 1.6, and the rules that keep released plugins loading in newer IDEs."
---
The plugin API has one version number: the version of the WIT package in [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit) (`openidea:plugin@1.6.0` on `main`). The host's engine version and the SDK's `API_VERSION` always agree with it, and the SDK carries an identical copy of the WIT files.
The plugin API has one version number: the version of the WIT package in [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit) (`openidea:plugin@1.7.0` on `main`). The host's engine version and the SDK's `API_VERSION` always agree with it, and the SDK carries an identical copy of the WIT files.
<Note>
The plugin system is a preview. The compatibility rules below hold for released 1.x versions; what is still unreleased
@@ -12,15 +12,16 @@ The plugin API has one version number: the version of the WIT package in [`wit/`
## Versions
| API | Shipped with | Highlights |
| ----- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1.0.0 | OpenIDEA 0.1.36 | The base API: commands, events, storage, tasks, project and editor access, VCS, diagnostics, network and secrets, processes, declarative views and custom editors, settings, editor providers, syntax provider |
| 1.1.0 | OpenIDEA 0.1.37 | `vcs.remotes`, `ui.copy-to-clipboard`, `ui.open-url`, opening a project file at a line from a view |
| 1.2.0 | OpenIDEA 0.1.38 | Background document indexers, pickers, input boxes and confirmations, plugin context keys for `when` clauses, the files selected in the Project view, a manifest `icon`, the `viewItem.context` menu |
| 1.3.0 | OpenIDEA 0.1.39 | Commit message and before-commit checks, Services detail panes, diagnostics while typing, hover, inlay hints, semantic tokens, file-changing code actions, HTTP endpoints, files outside the project (`fs.read`/`fs.write`, `project.write`, private plugin directory), batched `files-changed` events and path filters, workspace symbols, long-running processes, notifications with buttons, translations, multi-page settings, file template variables, web view PTYs, log tabs and content tabs |
| 1.4.0 | OpenIDEA 0.1.40 | `net.http:`, `ipc:`, `ide.commands` and `database.sources` capabilities; editor and project snapshots, decorations, text search, Problems, Git log/blame/diff; output channels; OAuth loopback; runs, terminal sessions, run configurations, test result parsing and run targets; formatting and other language features; embedded languages; snippets; more menu locations; database sources; MCP tools |
| 1.5.0 | Planned for the next release | `Project::notify` (a notification in one project's window), the `net:ask` capability, run target validation, per-command run target working-directory arguments |
| 1.6.0 | In development on `main` | Bounded, project-bound background WebSockets for event-stream plugins |
| API | Shipped with | Highlights |
| ----- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1.0.0 | OpenIDEA 0.1.36 | The base API: commands, events, storage, tasks, project and editor access, VCS, diagnostics, network and secrets, processes, declarative views and custom editors, settings, editor providers, syntax provider |
| 1.1.0 | OpenIDEA 0.1.37 | `vcs.remotes`, `ui.copy-to-clipboard`, `ui.open-url`, opening a project file at a line from a view |
| 1.2.0 | OpenIDEA 0.1.38 | Background document indexers, pickers, input boxes and confirmations, plugin context keys for `when` clauses, the files selected in the Project view, a manifest `icon`, the `viewItem.context` menu |
| 1.3.0 | OpenIDEA 0.1.39 | Commit message and before-commit checks, Services detail panes, diagnostics while typing, hover, inlay hints, semantic tokens, file-changing code actions, HTTP endpoints, files outside the project (`fs.read`/`fs.write`, `project.write`, private plugin directory), batched `files-changed` events and path filters, workspace symbols, long-running processes, notifications with buttons, translations, multi-page settings, file template variables, web view PTYs, log tabs and content tabs |
| 1.4.0 | OpenIDEA 0.1.40 | `net.http:`, `ipc:`, `ide.commands` and `database.sources` capabilities; editor and project snapshots, decorations, text search, Problems, Git log/blame/diff; output channels; OAuth loopback; runs, terminal sessions, run configurations, test result parsing and run targets; formatting and other language features; embedded languages; snippets; more menu locations; database sources; MCP tools |
| 1.5.0 | OpenIDEA 0.1.41 | `Project::notify` (a notification in one project's window), the `net:ask` capability, run target validation, per-command run target working-directory arguments |
| 1.6.0 | OpenIDEA 0.1.43 | Bounded, project-bound background WebSockets for event-stream plugins |
| 1.7.0 | In development on `main` | `views.open-diff`: two texts in the diff viewer, such as a review's file before and after |
The full list of what each version added is in [`plugins/README.md`](https://github.com/l0ng-ai/openidea/blob/main/plugins/README.md).
@@ -19,7 +19,7 @@ description: "插件系统的内部实现:扩展内核、每个插件一个 ac
## 接口
API 用 WIT 定义,位于 `wit/`(包名 `openidea:plugin@1.6.0`)。插件是面向 `plugin` world(或 `plugin-syntax`)的组件。插件端 SDK `sdk/rust/openidea-plugin` 把它封装成类型化的 Rust API,并带有一份 `wit/` 副本;`crates/ide-plugin/tests/api_compat.rs` 检查两份是否一致。
API 用 WIT 定义,位于 `wit/`(包名 `openidea:plugin@1.7.0`)。插件是面向 `plugin` world(或 `plugin-syntax`)的组件。插件端 SDK `sdk/rust/openidea-plugin` 把它封装成类型化的 Rust API,并带有一份 `wit/` 副本;`crates/ide-plugin/tests/api_compat.rs` 检查两份是否一致。
已发布的 API 版本是冻结的。wasmtime 按 semver 解析接口名,但函数类型必须完全一致,所以已发布的 1.x 只能增加:新的接口和新的可选导出,不能修改已有的 record 或 variant。改 `wit/` 之前请先读 `plugins/README.md` 里的 _Evolving the plugin API_。
+1 -1
View File
@@ -3,7 +3,7 @@ title: "API 参考"
description: "openidea:plugin WIT 包中的每个 interface:一句话说明及源码链接。"
---
插件 API 是 [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit) 中的 WIT 包 `openidea:plugin@1.6.0`。WIT 文件就是参考文档:每个 interface、类型和函数都在注释中有说明。1.6.0 是 `main` 上的版本;最新已发布版本是 1.4.0,其 WIT 保存在 [`sdk/rust/openidea-plugin/tests/api/released/`](https://github.com/l0ng-ai/openidea/tree/main/sdk/rust/openidea-plugin/tests/api/released)。
插件 API 是 [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit) 中的 WIT 包 `openidea:plugin@1.7.0`。WIT 文件就是参考文档:每个 interface、类型和函数都在注释中有说明。1.7.0 是 `main` 上的版本;最新已发布版本是 1.6.0,其 WIT 保存在 [`sdk/rust/openidea-plugin/tests/api/released/`](https://github.com/l0ng-ai/openidea/tree/main/sdk/rust/openidea-plugin/tests/api/released)。
<Info>
自动生成的 API 参考正在计划中。在此之前,请参考下面的 WIT 文件、Rust SDK 的 `cargo
+1 -1
View File
@@ -9,7 +9,7 @@ description: "插件代码如何运行:WebAssembly 组件、WIT world、宿主
| 部分 | 位置 | 作用 |
| -------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| 插件 API | [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit) | 契约:WIT 包 `openidea:plugin@1.6.0`、其中的 interface 和 world |
| 插件 API | [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit) | 契约:WIT 包 `openidea:plugin@1.7.0`、其中的 interface 和 world |
| Rust SDK | [`sdk/rust/openidea-plugin`](https://github.com/l0ng-ai/openidea/tree/main/sdk/rust/openidea-plugin) | WIT 的类型化封装、`Plugin` trait、`register_plugin!`,以及给 `cargo test` 用的模拟宿主 |
| 插件包 | [`crates/ide-extension`](https://github.com/l0ng-ai/openidea/tree/main/crates/ide-extension) | 清单、能力及其风险等级、授权记录、贡献点、`.oiplugin` 包和插件市场索引 |
| 运行时 | [`crates/ide-plugin`](https://github.com/l0ng-ai/openidea/tree/main/crates/ide-plugin) | wasmtime 引擎、每个插件一个 `Store`、各项限制、每个活跃插件一个 actor 线程 |
+16 -14
View File
@@ -248,11 +248,12 @@ Vite + React 页面由 `plugins/build.sh official/graphviz` 构建到 `web/`,
声明 `engines.openidea = "^1.3"` 后,页面可以通过 `openidea.postMessage` 发送 JSON 消息,请 IDE 打开更多东西。
这些协议只运行通过 `process:<program>` 授权的程序,工作目录是窗口所在的受信任项目根目录,从不经过 shell:
| `op` | 打开什么 |
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `pty-spawn`、`pty-input`、`pty-resize`、`pty-kill`、`pty-ack` | 归该 frame 所有的交互式 PTY;输出以 `pty-data`、`pty-exit`、`pty-error` 返回。每个插件最多 8 个会话。 |
| `open-log-tab` | 只读编辑器标签页,持续显示程序输出,可带参数开关。 |
| `open-content-tab` | 内容由插件提供的编辑器标签页,带工具栏操作,用 `content-tab-reply` 回复。 |
| `op` | 打开什么 |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `pty-spawn`、`pty-input`、`pty-resize`、`pty-kill`、`pty-ack` | 归该 frame 所有的交互式 PTY;输出以 `pty-data`、`pty-exit`、`pty-error` 返回。每个插件最多 8 个会话。 |
| `open-log-tab` | 只读编辑器标签页,持续显示程序输出,可带参数开关。 |
| `open-content-tab` | 内容由插件提供的编辑器标签页,带工具栏操作,用 `content-tab-reply` 回复。 |
| `open-diff` | API 1.7:在差异查看器中对比两段文本(`left`、`right`,可选标签、`title`,以及用于高亮的 `fileName`)。 |
消息字段和限制见 [插件指南](https://github.com/l0ng-ai/openidea/blob/main/plugins/README.md#web-frame-ptys-api-13);
`crates/ide-plugin/tests/fixtures/` 下已构建的 `web-pty`、`web-log`、`web-content` 测试插件演示了这些协议。
@@ -303,15 +304,16 @@ impl Details for MyPlugin {
以下函数需在窗口的命令或事件中调用:
| 函数 | 打开什么 |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `host::open_location(path, line, column)` | 在编辑器中打开项目文件并定位光标(`project.read`;插件私有目录中的文件也可以)。 |
| `host::open_editor(path, selection)` | API 1.4:打开项目文件并选中一个范围。 |
| `host::open_terminal(title, program, args)` | 在项目根目录运行已批准 `process:<program>` 的终端标签页。 |
| `host::open_terminal_session(key, title, &cmd)` | API 1.4:带 key 的终端;再次使用同一 key 会聚焦它。用 `host::write_terminal(key, text)` 写入;退出时收到 `terminal-exited` 事件。 |
| `host::open_log_tab(key, title, &log)` | API 1.3:基于 `ProcessLog` 的只读日志标签页。每个窗口最多 32 个;暂不支持远程项目。 |
| `host::open_content_tab(&ContentTab { .. })` | API 1.4:不依赖网页视图的内容编辑器标签页;操作以 `content-tab-action` 事件送达,用 `host::reply_content_tab` 回复。 |
| `host::output_append(channel, text)` | API 1.4:每个通道一个只读 **输出** 标签页(32 个通道,各 1 MiB);另有 `output_clear`、`output_reveal`。 |
| 函数 | 打开什么 |
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `host::open_location(path, line, column)` | 在编辑器中打开项目文件并定位光标(`project.read`;插件私有目录中的文件也可以)。 |
| `host::open_editor(path, selection)` | API 1.4:打开项目文件并选中一个范围。 |
| `host::open_terminal(title, program, args)` | 在项目根目录运行已批准 `process:<program>` 的终端标签页。 |
| `host::open_terminal_session(key, title, &cmd)` | API 1.4:带 key 的终端;再次使用同一 key 会聚焦它。用 `host::write_terminal(key, text)` 写入;退出时收到 `terminal-exited` 事件。 |
| `host::open_log_tab(key, title, &log)` | API 1.3:基于 `ProcessLog` 的只读日志标签页。每个窗口最多 32 个;暂不支持远程项目。 |
| `host::open_content_tab(&ContentTab { .. })` | API 1.4:不依赖网页视图的内容编辑器标签页;操作以 `content-tab-action` 事件送达,用 `host::reply_content_tab` 回复。 |
| `host::open_diff(&ContentDiff { .. }, file_name)` | API 1.7:在差异查看器中对比两段文本,例如评审中某个文件的修改前后;`file_name` 决定语法高亮。 |
| `host::output_append(channel, text)` | API 1.4:每个通道一个只读 **输出** 标签页(32 个通道,各 1 MiB);另有 `output_clear`、`output_reveal`。 |
```rust
use openidea_plugin::{ContentAction, ContentTab, host};
+2 -2
View File
@@ -36,8 +36,8 @@ OpenIDEA 插件可以为 IDE 增加命令、编辑器功能、工具窗口、主
| | |
| -------------- | ----------------------------------------------- |
| 最新已发布 API | **1.4.0**,随 OpenIDEA 0.1.40 发布 |
| 开发中 | `main` 分支上的 **1.6.0**,计划随下一个版本发布 |
| 最新已发布 API | **1.6.0**,随 OpenIDEA 0.1.43 发布 |
| 开发中 | `main` 分支上的 **1.7.0**,计划随下一个版本发布 |
| WIT 包名 | `openidea:plugin@<version>` |
在清单里用 `engines.openidea` 声明插件需要的 API,例如 `engines.openidea = "^1.4"`。
+11 -10
View File
@@ -3,21 +3,22 @@ title: "API 版本与兼容"
description: "engines.openidea、插件 API 从 1.0 到 1.6 的各个版本,以及让已发布插件在新 IDE 中继续加载的规则。"
---
插件 API 只有一个版本号:[`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit) 中 WIT 包的版本(`main` 上是 `openidea:plugin@1.6.0`)。宿主的引擎版本和 SDK 的 `API_VERSION` 始终与它一致,SDK 里也带着一份完全相同的 WIT 文件。
插件 API 只有一个版本号:[`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit) 中 WIT 包的版本(`main` 上是 `openidea:plugin@1.7.0`)。宿主的引擎版本和 SDK 的 `API_VERSION` 始终与它一致,SDK 里也带着一份完全相同的 WIT 文件。
<Note>插件系统目前是预览版。下面的兼容规则适用于已发布的 1.x 版本;`main` 上尚未发布的内容在发布前仍可能变化。</Note>
## 版本历史
| API | 随哪个版本发布 | 主要内容 |
| ----- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1.0.0 | OpenIDEA 0.1.36 | 基础 API:命令、事件、存储、任务、项目和编辑器访问、VCS、诊断、网络与密钥、进程、声明式视图和自定义编辑器、设置、编辑器 provider、syntax provider |
| 1.1.0 | OpenIDEA 0.1.37 | `vcs.remotes`、`ui.copy-to-clipboard`、`ui.open-url`,从视图中打开项目文件的指定行 |
| 1.2.0 | OpenIDEA 0.1.38 | 后台文档索引器;选择器、输入框和确认框;插件自定义的 `when` 上下文键;项目视图中选中的文件;清单 `icon`;`viewItem.context` 菜单 |
| 1.3.0 | OpenIDEA 0.1.39 | 提交信息和提交前检查、Services 详情面板、输入时诊断、悬停、内嵌提示、语义 token、改动文件的代码操作、HTTP 端点、项目外文件(`fs.read`/`fs.write`、`project.write`、插件私有目录)、批量 `files-changed` 事件和路径过滤、工作区符号、长时间运行的进程、带按钮的通知、翻译、多页设置、文件模板变量、Web 视图 PTY、日志标签页和内容标签页 |
| 1.4.0 | OpenIDEA 0.1.40 | `net.http:`、`ipc:`、`ide.commands` 和 `database.sources` 能力;编辑器与项目快照、装饰、文本搜索、问题视图、Git log/blame/diff;输出通道;OAuth 回环;运行、终端会话、运行配置、测试结果解析和运行目标;格式化等语言功能;嵌入语言;代码片段;更多菜单位置;数据库数据源;MCP 工具 |
| 1.5.0 | 计划随下一个版本发布 | `Project::notify`(只在某个项目的窗口中显示通知)、`net:ask` 能力、运行目标启动前校验、运行目标按命令设置工作目录参数 |
| 1.6.0 | `main` 上开发中 | 面向事件流插件、受容量限制且绑定项目的后台 WebSocket |
| API | 随哪个版本发布 | 主要内容 |
| ----- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1.0.0 | OpenIDEA 0.1.36 | 基础 API:命令、事件、存储、任务、项目和编辑器访问、VCS、诊断、网络与密钥、进程、声明式视图和自定义编辑器、设置、编辑器 provider、syntax provider |
| 1.1.0 | OpenIDEA 0.1.37 | `vcs.remotes`、`ui.copy-to-clipboard`、`ui.open-url`,从视图中打开项目文件的指定行 |
| 1.2.0 | OpenIDEA 0.1.38 | 后台文档索引器;选择器、输入框和确认框;插件自定义的 `when` 上下文键;项目视图中选中的文件;清单 `icon`;`viewItem.context` 菜单 |
| 1.3.0 | OpenIDEA 0.1.39 | 提交信息和提交前检查、Services 详情面板、输入时诊断、悬停、内嵌提示、语义 token、改动文件的代码操作、HTTP 端点、项目外文件(`fs.read`/`fs.write`、`project.write`、插件私有目录)、批量 `files-changed` 事件和路径过滤、工作区符号、长时间运行的进程、带按钮的通知、翻译、多页设置、文件模板变量、Web 视图 PTY、日志标签页和内容标签页 |
| 1.4.0 | OpenIDEA 0.1.40 | `net.http:`、`ipc:`、`ide.commands` 和 `database.sources` 能力;编辑器与项目快照、装饰、文本搜索、问题视图、Git log/blame/diff;输出通道;OAuth 回环;运行、终端会话、运行配置、测试结果解析和运行目标;格式化等语言功能;嵌入语言;代码片段;更多菜单位置;数据库数据源;MCP 工具 |
| 1.5.0 | OpenIDEA 0.1.41 | `Project::notify`(只在某个项目的窗口中显示通知)、`net:ask` 能力、运行目标启动前校验、运行目标按命令设置工作目录参数 |
| 1.6.0 | OpenIDEA 0.1.43 | 面向事件流插件、受容量限制且绑定项目的后台 WebSocket |
| 1.7.0 | `main` 上开发中 | `views.open-diff`:在差异查看器中对比两段文本,例如评审中某个文件的修改前后 |
每个版本新增内容的完整列表见 [`plugins/README.md`](https://github.com/l0ng-ai/openidea/blob/main/plugins/README.md)。
+4 -2
View File
@@ -9,7 +9,8 @@ const ROOT = "/dev/acme-api";
async function openBranches(page: Page) {
await page.evaluate(async () => {
const { executeAction } = await import(/* @vite-ignore */ "/src/platform/actions.ts");
const actionsModule = "/src/platform/actions.ts";
const { executeAction } = await import(/* @vite-ignore */ actionsModule);
await executeAction("vcs.branches");
});
const popup = page.getByRole("dialog", { name: "Git Branches" });
@@ -48,7 +49,8 @@ test.beforeEach(async ({ workbench: page }) => {
branch("main", true),
...Array.from({ length: 40 }, (_, i) => branch(`task-${i}-a-long-branch-name-that-gets-cut-off`)),
];
const { useVcs, refreshNow, loadBranches } = await import(/* @vite-ignore */ "/src/features/vcs/store.ts");
const vcsModule = "/src/features/vcs/store.ts";
const { useVcs, refreshNow, loadBranches } = await import(/* @vite-ignore */ vcsModule);
useVcs.setState({ available: true, root, repo: root, branchesLoaded: false, stashesLoaded: false });
await refreshNow();
await loadBranches();
+15 -3
View File
@@ -36,7 +36,7 @@ sdk/rust/openidea-plugin/ # the Rust SDK (host API, Plugin trait, mock host)
crates/ide-plugin/tests/fixtures/ # test plugins: hello, jobs, language-toy, misbehave, semantic-tokens, hover, commit, inlay-hints
```
The plugin API is defined in [`wit/`](../wit/) (`openidea:plugin@1.6.0`, world
The plugin API is defined in [`wit/`](../wit/) (`openidea:plugin@1.7.0`, world
`plugin`).
## 1. Quick start
@@ -418,9 +418,20 @@ are always available. Calling a gated function without its capability returns
## 3. Writing code (Rust SDK)
### API 1.7
API 1.7.0 is in development on `main`. Use `engines.openidea = "^1.7"` and the
matching SDK. Released 1.6 plugins keep working. New in 1.7:
- `host::open_diff(&ContentDiff { .. }, file_name)` (`views.open-diff`) opens
two texts in the invoking window's diff viewer, for example a merge request's
file before and after; `file_name` picks the syntax highlighting. Web views
send `{ op: "open-diff", left, right, leftLabel?, rightLabel?, title?, fileName? }`.
Commands and events only; each side at most 1 MiB.
### API 1.5
API 1.5.0 is planned for the next OpenIDEA release. Use
API 1.5.0 shipped with OpenIDEA 0.1.41. Use
`engines.openidea = "^1.5"` and the matching SDK. Released 1.4 plugins keep
working. New in 1.5:
@@ -638,6 +649,7 @@ then run `openidea-plugin check`.
| `host::write_terminal(key, text)` | `views.write-terminal` | API 1.4: write only to this plugin's still-running terminal in this window; trusted project, commands/events only. |
| `host::open_content_tab(tab)` | `views.open-content-tab` | API 1.4: the shared content editor protocol, without a web view; actions arrive as `content-tab-action`. |
| `host::reply_content_tab(request_id, reply)` | `views.reply-content-tab` | API 1.4: reply to an owned action in the originating window. No capability; commands/events only. |
| `host::open_diff(diff, file_name)` | `views.open-diff` | API 1.7: two texts in the invoking window's diff viewer; `file_name` (at most 256 characters) picks the highlighting. Commands/events only; each side at most 1 MiB. |
| `RunConfigurations::configurations(root)` | export `run-configurations.configurations` | API 1.4: optional `plugin-run-configurations` world; SDK registration `run_configurations`. Full project list, refreshed on configuration listing. |
| `TestResults::parse(config, chunk)` | export `test-results.parse` | API 1.4: optional `plugin-test-results` world; SDK registration `test_results`. Bounded output batches for `framework = "plugin"`; failures use built-in parsers. |
| `TestResults::select_tests(config, tests)` | export `test-results.select-tests` | API 1.4: full replacement argv for test-tree name paths, or `None` when unsupported (the SDK default). Enables rerunning failed tests and running tree selections. |
@@ -2417,7 +2429,7 @@ the API itself follows _Evolving the plugin API_ below.
Released versions are frozen: 1.0.0 shipped with OpenIDEA 0.1.36, 1.1.0
with 0.1.37, 1.2.0 with 0.1.38, 1.3.0 with 0.1.39 and 1.4.0 with 0.1.40.
API 1.5.0 is planned with the next release; API 1.6.0 is in development on `main`. wasmtime decides what stays compatible: it resolves interface names
API 1.5.0 shipped with 0.1.41 and 1.6.0 with 0.1.43; API 1.7.0 is in development on `main`. wasmtime decides what stays compatible: it resolves interface names
by semver (a component importing `openidea:plugin/[email protected]` links against a
host defining `@1.1.0`, and the host finds its exports the same way), but
function types must match exactly — a record with another field count, or a
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
4d1d89c8b3c734f19b97c132a8aad1e042c7e894d695b7f751f2cfbe785c03f0
e8e6dbe6dba09de2fd71a9d84eed8c01b16253f9247bf882361c6f49b8a4d51d
Binary file not shown.
@@ -1 +1 @@
a7e3cf1fce43f673132f8b5a26d8ab549e85d0f3e40c901b2a93eec5280c2a27
784a4e01725063fc2949a88bd209e7fcc8ecb0f721acd5f03d92db39ece091a9
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
dafabda672d2f0921daf246ace54fff7752ad233003c012f95aaca91a413384a
73670a049d8c108511af35b5030d08f1e04a9198cba0da54268e75a06747d953
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
ed15d10fd0c5b68fd31aa95b67ffe7fc752c7897ff7361581dac54f3edeea65a
a17332b7d5becc4ddccffdba1f9b009e07f75f171bc2519b6c1038fdda2bfd71
Binary file not shown.
Loaded 100 of 187 files, more files were not shown because too many files have changed in this diff. Show more