Compare commits

...
Author SHA1 Message Date
l0ng-ai 7c69eea7c2 fix(clipboard): read the clipboard natively for Paste from menus
A Paste chosen from a menu (the terminal's context menu, Edit › Paste, Paste as
Plain Text and other paste buttons) has no paste event to read, so it used
navigator.clipboard.readText(), and the terminal tried execCommand("paste") first.
On Linux, WebKitGTK gives neither the text, so the terminal's context-menu Paste
did nothing while middle-click paste worked.

A new clipboard_text command reads the clipboard's text natively, as
clipboard_write_text already writes it: GTK on Linux, NSPasteboard on macOS, and
none on Windows, where the WebView's clipboard is still used.
readClipboardPlainText() wraps it with that fallback, and every menu paste reads
through it. On Linux the terminal pastes the native text before trying
execCommand; an image-only clipboard keeps the existing paths.
2026-11-13 12:51:14 +08:00
l0ng-ai 68dd949df3 fix(terminal): keep the first shifted character a macOS IME commits directly (#3067)
With WeChat or Pinyin on macOS, Shift+/ commits `?` with an insertText that
arrives before the character's keydown, while only Shift is down. xterm's input
gate was already armed by the Shift keydown and dropped the event, so the first
press produced nothing and the key had to be pressed twice.

The macOS input handler now also delivers an insertText that arrives while only
modifiers are down since the last keyup. Shifted keys xterm sends itself are
unchanged: their keydown comes first.
2026-11-13 12:41:17 +08:00
l0ng-ai b98bddbbab fix(vcs): fill the diff dialog with the editors (#3066)
The front diff dialog (the modal commit interface's Show Diff) left the area below a
short diff in the dialog's background: its body wrapper used flex: 1 inside the
dialog body, which is not a flex container, so it only took the content's height.
It now fills the body, and the editors' background reaches the footer.
2026-11-13 12:33:15 +08:00
l0ng-ai 21b05ab73a fix(vcs): show the commit's diff on ⌘D in the Git Log's changed files (#3065)
⌘D / Ctrl+D in the Git Log's changed files ran the global Show Diff, which diffs
the selected file's working copy against HEAD, and the context menu's Show Diff had
no shortcut. The changed files now set a vcsLogChangesFocus context; the log's Show
Diff is bound to the Show Diff key while it is set, so the menu shows it and the key
opens the commit's diff of the selected files, and the global Show Diff steps aside.
2026-11-13 12:31:56 +08:00
l0ng-ai 7d7de0b966 fix(python): exclude virtual environments from detected roots whatever their name (#3058)
Root detection only skipped environments named `venv` / `.venv`. A venv with another
name in the project (e.g. `code10`) was walked into, and any package in its
site-packages that ships `pyproject.toml`, `setup.py` or `requirements.txt` became a
source root. Source roots go on PYTHONPATH for runs, so a package directory with an
`io` package shadowed the standard library and Python 3.10 failed at startup with
`init_sys_streams: module 'io' has no attribute 'open'`.

A directory with `pyvenv.cfg` (venv, virtualenv, uv) or `conda-meta` (conda) is now
an excluded root and not searched for modules; creating one re-runs detection.
Project discovery in the workspace folder picker skips such directories too, so
packages inside an environment are not offered as projects.
2026-11-13 12:21:38 +08:00
Clarence 830f4aa802 perf(editor): don't rescan rendered doc comments on every keystroke (#3052)
With Render Documentation Comments on, every keystroke rescanned all comment nodes and rebuilt every rendered comment, so typing in a large file cost about 10 ms per keystroke against about 1 ms with rendering off.

A keystroke (input.type / delete) that touches no comment's lines now only maps the rendered comments and their decorations through the change and marks the view stale; the file is scanned again 300 ms after typing and parsing pause, by the scanner that already does this while rendering is off (renamed deferredScanner). Paste, drop, undo/redo, formatting, reloads, keystrokes on a comment's lines, toggles and relabels still rebuild at once, and parser progress rebuilds at once unless a scan is already waiting, so files still open with visible comments rendered.

Co-authored-by: Clarence404 <[email protected]>
2026-11-13 12:18:34 +08:00
l0ng-ai c905f1428e fix(maven): go from a dependency, plugin or parent to its own POM (#3057)
Go to Declaration in pom.xml found nothing on a dependency that names its own
version, on a <dependencyManagement> entry (a BOM import, say) or on <parent>.
They now open the artifact's POM: the project module with those coordinates, else
the local repository's POM of that version (properties resolved). A dependency
whose version a management entry provides still goes to that entry.
2026-11-13 12:10:46 +08:00
l0ng-ai a8036ce6fa feat(plugins): open two texts in the diff viewer (API 1.7) (#3055)
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 12:02:36 +08:00
208 changed files with 1126 additions and 366 deletions

No files matched your search

+140 -10
View File
@@ -1680,6 +1680,15 @@ pub enum PomGotoKind {
ManagedDependency,
/// A `<pluginManagement>` entry providing the plugin's version.
ManagedPlugin,
/// The POM of the artifact itself: a project module's, else the local repository's.
ArtifactPom,
}
/// A Maven project of the workspace, for going to a dependency on it.
pub struct ModulePom {
pub group_id: String,
pub artifact_id: String,
pub pom: PathBuf,
}
/// Byte offset of `pos` (0-based line / UTF-16 column), clamped to `text`.
@@ -1759,6 +1768,20 @@ fn coordinates(e: &Element, props: &HashMap<String, String>) -> Option<(String,
/// the local repository when it is not in the project). `None` when there is no
/// resolvable declaration here.
pub fn goto_declaration(text: &str, path: &Path, pos: Pos, local_repo: Option<PathBuf>) -> Option<PomGoto> {
goto_declaration_in(text, path, pos, local_repo, Vec::new)
}
/// [`goto_declaration`], where a `<parent>` and a dependency or plugin whose version no
/// management entry provides (or a management entry itself) go to the artifact's own
/// POM: the project module among `modules` with its coordinates, else the local
/// repository's POM of its version.
pub fn goto_declaration_in(
text: &str,
path: &Path,
pos: Pos,
local_repo: Option<PathBuf>,
modules: impl FnOnce() -> Vec<ModulePom>,
) -> Option<PomGoto> {
let root = xml::parse(text).filter(|e| e.name == "project")?;
let at = offset_at(text, pos);
if let Some((name, _, _)) = property_ref_at(text, at)
@@ -1769,15 +1792,56 @@ pub fn goto_declaration(text: &str, path: &Path, pos: Pos, local_repo: Option<Pa
// A `<dependency>` / `<plugin>` in a declaration position: its version may come
// from a management entry of this POM or an ancestor's.
let elements = element_path(&root, at);
let e = elements.iter().rev().find(|e| e.name == "dependency" || e.name == "plugin")?;
if elements.iter().any(|a| a.name == "dependencyManagement" || a.name == "pluginManagement") {
return None; // Management entries are declarations; there is nothing to go to.
if elements.get(1).is_some_and(|e| e.name == "parent") {
let chain = parent_chain(path, text, local_repo);
let (pom, parent_text) = chain.get(1)?;
let label = elements[1].text_of("artifactId").unwrap_or_default();
return Some(pom_goto(pom.clone(), parent_text, label));
}
let kind = match e.name.as_str() {
"dependency" => PomGotoKind::ManagedDependency,
_ => PomGotoKind::ManagedPlugin,
};
managed_target(text, path, e, kind, local_repo)
let e = elements.iter().rev().find(|e| e.name == "dependency" || e.name == "plugin")?;
// A management entry declares the version: it goes to the artifact (a BOM import, say).
if !elements.iter().any(|a| a.name == "dependencyManagement" || a.name == "pluginManagement") {
let kind = match e.name.as_str() {
"dependency" => PomGotoKind::ManagedDependency,
_ => PomGotoKind::ManagedPlugin,
};
if let Some(found) = managed_target(text, path, e, kind, local_repo.clone()) {
return Some(found);
}
}
artifact_target(text, path, e, local_repo, modules)
}
/// The artifact's own POM for a dependency or plugin element `e`.
fn artifact_target(
text: &str,
path: &Path,
e: &Element,
local_repo: Option<PathBuf>,
modules: impl FnOnce() -> Vec<ModulePom>,
) -> Option<PomGoto> {
let mut resolver = Resolver::new(local_repo.clone(), &[]);
let chain = parent_chain(path, text, local_repo);
let props = properties_at(&mut resolver, &chain, 0);
let (group, artifact) = coordinates(e, &props)?;
let label = format!("{group}:{artifact}");
if let Some(module) = modules().into_iter().find(|m| m.group_id == group && m.artifact_id == artifact) {
let module_text = xml::read_text(&module.pom)?;
return Some(pom_goto(module.pom, &module_text, label));
}
let version = interpolate(&e.text_of("version")?, &props);
let pom = resolver.repo_pom(&group, &artifact, &version)?;
let pom_text = xml::read_text(&pom)?;
Some(pom_goto(pom, &pom_text, label))
}
/// The POM at `path` as a target, at its `<artifactId>`.
fn pom_goto(path: PathBuf, text: &str, label: String) -> PomGoto {
let range = xml::parse(text)
.filter(|e| e.name == "project")
.and_then(|root| root.child("artifactId").map(|a| range_of(text, a)))
.unwrap_or_default();
PomGoto { path, range, kind: PomGotoKind::ArtifactPom, label }
}
/// The parent POMs of the POM in `text` at `path`, child-first (its `<parent>` refs,
@@ -3087,9 +3151,9 @@ mod tests {
let text = std::fs::read_to_string(&path).unwrap();
// The property is declared nowhere along the chain.
assert_eq!(goto_declaration(&text, &path, at(&text, "${missing.key}"), None), None);
// The dependency names its own version: it is the declaration.
// The dependency names its own version and no local repository has its POM.
assert_eq!(goto_declaration(&text, &path, at(&text, "<artifactId>other-lib"), None), None);
// A management entry is a declaration itself, not a reference.
// A management entry declares the version; its artifact's POM is not at hand either.
let parent_path = tmp.path().join("pom.xml");
let parent_text = std::fs::read_to_string(&parent_path).unwrap();
assert_eq!(
@@ -3101,6 +3165,72 @@ mod tests {
assert_eq!(goto_declaration("<project></project>", &path, Pos::new(0, 3), None), None);
}
#[test]
fn artifacts_go_to_their_own_pom_in_the_project_or_the_local_repository() {
let tmp = tempfile::tempdir().unwrap();
let repo = tmp.path().join("repo");
let bom_dir = repo.join("com/alibaba/cloud/spring-cloud-alibaba-dependencies/2.2.6.RELEASE");
std::fs::create_dir_all(&bom_dir).unwrap();
let bom = "<project>\n <groupId>com.alibaba.cloud</groupId>\n <artifactId>spring-cloud-alibaba-dependencies</artifactId>\n</project>";
std::fs::write(bom_dir.join("spring-cloud-alibaba-dependencies-2.2.6.RELEASE.pom"), bom).unwrap();
let parent = format!(
r#"{PARENT_HEADER}<properties><alibaba.version>2.2.6.RELEASE</alibaba.version></properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${{alibaba.version}}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>"#
);
let child = format!(
r#"{CHILD_HEADER}<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>core</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</project>"#
);
write_parent_child(tmp.path(), &parent, &child);
let core = tmp.path().join("core/pom.xml");
std::fs::create_dir_all(core.parent().unwrap()).unwrap();
std::fs::write(
&core,
"<project>\n <groupId>com.example</groupId>\n <artifactId>core</artifactId>\n</project>",
)
.unwrap();
let modules =
|| vec![ModulePom { group_id: "com.example".into(), artifact_id: "core".into(), pom: core.clone() }];
// A BOM import in <dependencyManagement> opens the BOM from the local repository.
let parent_path = tmp.path().join("pom.xml");
let parent_text = std::fs::read_to_string(&parent_path).unwrap();
let at_bom = at(&parent_text, "spring-cloud-alibaba-dependencies<");
let goto = goto_declaration_in(&parent_text, &parent_path, at_bom, Some(repo.clone()), modules).unwrap();
assert_eq!(goto.path, bom_dir.join("spring-cloud-alibaba-dependencies-2.2.6.RELEASE.pom"));
assert_eq!(goto.kind, PomGotoKind::ArtifactPom);
assert_eq!(goto.range.start, pos_at(bom, bom.find("<artifactId>").unwrap()));
// A dependency on a project module opens the module's POM.
let path = tmp.path().join("app/pom.xml");
let text = std::fs::read_to_string(&path).unwrap();
let goto =
goto_declaration_in(&text, &path, at(&text, "core</artifactId>"), Some(repo.clone()), modules).unwrap();
assert_eq!((goto.path.as_path(), goto.label.as_str()), (core.as_path(), "com.example:core"));
// The <parent> section opens the parent POM.
let goto = goto_declaration_in(&text, &path, at(&text, "parent</artifactId>"), Some(repo), modules).unwrap();
assert_eq!(goto.path, canon(&parent_path));
assert_eq!(goto.kind, PomGotoKind::ArtifactPom);
}
#[test]
fn a_property_reference_after_non_ascii_text_is_still_found() {
let text = format!(
+15
View File
@@ -27,6 +27,13 @@ pub fn set_text(text: &str, owner: usize) -> std::io::Result<()> {
imp::set_text(text, owner)
}
/// The clipboard's plain text, None when it holds none. macOS only (main thread);
/// Linux reads it asynchronously through GTK in the app, Windows through the WebView.
#[cfg(target_os = "macos")]
pub fn text() -> Option<String> {
imp::text()
}
/// Size of `DROPFILES`: `pFiles`, `pt` (two `LONG`s), `fNC`, `fWide`.
const HEADER: usize = 20;
@@ -373,6 +380,14 @@ mod imp {
})
}
pub fn text() -> Option<String> {
debug_assert!(MainThreadMarker::new().is_some(), "NSPasteboard access requires the main thread");
autoreleasepool(|_| {
// SAFETY: the type is an AppKit constant, valid for the process' lifetime.
NSPasteboard::generalPasteboard().stringForType(unsafe { NSPasteboardTypeString }).map(|s| s.to_string())
})
}
pub fn set_text(text: &str, _owner: usize) -> std::io::Result<()> {
debug_assert!(MainThreadMarker::new().is_some(), "NSPasteboard access requires the main thread");
autoreleasepool(|_| {
+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.
+15 -2
View File
@@ -455,7 +455,8 @@ pub async fn maven_dependency_declaration(
/// absolute for a parent POM opened from the local repository): the definition of the
/// `${property}` at the caret, or the `<dependencyManagement>` / `<pluginManagement>`
/// entry providing the dependency's / plugin's version, along the parent chain (into
/// the local repository when a parent is not in the project). `None` when nothing
/// the local repository when a parent is not in the project), else the artifact's own
/// POM (a project module's, or the local repository's). `None` when nothing
/// resolvable is under the caret.
pub async fn maven_goto_declaration(host: &Host, window: &str, pom: String, pos: Pos) -> IdeResult<Option<PomGoto>> {
let project = host.project(window)?;
@@ -466,7 +467,19 @@ pub async fn maven_goto_declaration(host: &Host, window: &str, pom: String, pos:
let settings = project.settings.effective();
let repo = ide_sdk::apply::maven_local_repository(&project.root, &settings)
.or_else(ide_build::maven::default_local_repo);
Ok(ide_build::maven::goto_declaration(&text, &path, pos, repo))
let modules = || {
let model = load(&project.root, &project.build_cache_dir(), &settings).maven.unwrap_or_default();
model
.projects
.into_iter()
.map(|p| ide_build::maven::ModulePom {
group_id: p.group_id,
artifact_id: p.artifact_id,
pom: project.root.join(&p.pom),
})
.collect()
};
Ok(ide_build::maven::goto_declaration_in(&text, &path, pos, repo, modules))
})
.await
.map_err(join_err)?
+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());
+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
+16 -1
View File
@@ -108,7 +108,9 @@ fn discover_with_limit(
if name.starts_with('.') || SKIP_DIRS.contains(&name.as_ref()) {
continue;
}
if entry.file_type().is_ok_and(|kind| kind.is_dir()) {
if entry.file_type().is_ok_and(|kind| kind.is_dir())
&& crate::roots::virtual_env_marker(&entry.path()).is_none()
{
children.push(entry.path());
}
}
@@ -170,6 +172,19 @@ mod tests {
assert_eq!(result.direct[0], root);
}
#[test]
fn skips_virtual_environments_whatever_their_name() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
project(root, "code10", "pyvenv.cfg");
project(root, "code10/Lib/site-packages/pkg", "pyproject.toml");
fs::create_dir_all(root.join("conda/conda-meta")).unwrap();
project(root, "conda/pkgs/dep", "setup.py");
let app = project(root, "app", "pyproject.toml");
let result = discover(vec![root.into()], &CancellationToken::new()).unwrap();
assert_eq!(result.projects.iter().map(|p| p.path.to_path_buf()).collect::<Vec<_>>(), [app]);
}
#[test]
fn finds_depth_four_but_not_five_and_retains_empty_folders() {
let tmp = tempfile::tempdir().unwrap();
+53 -1
View File
@@ -17,6 +17,19 @@ const PYTHON_FILES: &[&str] = &["pyproject.toml", "setup.py", "setup.cfg", "Pipf
const SKIP_DIRS: &[&str] =
&["node_modules", "target", "build", "dist", "out", "venv", "__pycache__", "vendor", "bower_components"];
/// A Python virtual environment (`python -m venv`, virtualenv, uv) or conda environment,
/// whatever it is named: its packages are dependencies, not modules of the project.
/// Returns the file that marks it.
pub(crate) fn virtual_env_marker(dir: &Path) -> Option<&'static str> {
if dir.join("pyvenv.cfg").is_file() {
Some("pyvenv.cfg")
} else if dir.join("conda-meta").is_dir() {
Some("conda-meta")
} else {
None
}
}
/// Build files whose changes can change the detected roots.
pub(crate) const BUILD_FILES: &[&str] = &[
"pom.xml",
@@ -65,7 +78,10 @@ pub fn affects_roots(rel: &Path) -> bool {
let Some(name) = names.next() else { return false };
BUILD_FILES.contains(&name)
|| ROOT_NAMES.contains(&name)
|| matches!(name, "Assets" | "Packages" | "ProjectSettings" | "ProjectVersion.txt")
|| matches!(
name,
"Assets" | "Packages" | "ProjectSettings" | "ProjectVersion.txt" | "pyvenv.cfg" | "conda-meta"
)
|| names.next() == Some("src")
}
@@ -131,6 +147,13 @@ pub fn detect(root: &Path) -> Vec<SourceFolder> {
if found.iter().any(|f| *f == child || f.is_empty() && name == "src") {
continue;
}
if let Some(marker) = virtual_env_marker(&root.join(&child)) {
if !out.iter().any(|r| r.path == child) {
let from = join(&child, marker);
out.push(SourceFolder { path: child, kind: RootKind::Excluded, detected_from: Some(from) });
}
continue;
}
queue.push_back((child, depth + 1));
}
}
@@ -505,6 +528,35 @@ mod tests {
);
}
#[test]
fn virtual_environments_are_excluded_whatever_their_name() {
// A package in the environment's site-packages has build files of its own; as a
// source root it would be put on PYTHONPATH and shadow the standard library.
let dir = tree(&[
"requirements.txt",
"app.py",
"code10/pyvenv.cfg",
"code10/Lib/site-packages/pkg/pyproject.toml",
"code10/Lib/site-packages/pkg/io/__init__.py",
"envs/conda/conda-meta/",
"envs/conda/lib/pkg/setup.py",
"svc/pyproject.toml",
"svc/env/pyvenv.cfg",
"svc/env/lib/python3.12/site-packages/dep/setup.py",
]);
assert_eq!(
summary(&detect(dir.path())),
vec![
"[email protected]",
"code10=Excluded@code10/pyvenv.cfg",
"envs/conda=Excluded@envs/conda/conda-meta",
"svc=Source@svc/pyproject.toml",
"svc/env=Excluded@svc/env/pyvenv.cfg",
]
);
assert!(affects_roots(Path::new("code10/pyvenv.cfg")));
}
#[test]
fn first_ecosystem_wins_for_a_shared_folder() {
// A Gradle build with a frontend: `build` stays Gradle's, `src` is not a Node root.
+1 -1
View File
@@ -6,7 +6,7 @@
//! project-relative and `/`-separated; `""` is the project root itself.
mod detect;
pub(crate) use detect::BUILD_FILES;
pub(crate) use detect::{BUILD_FILES, virtual_env_marker};
mod gradle;
mod java;
mod maven;
@@ -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)。
+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.
+1 -1
View File
@@ -1 +1 @@
3317a89d28772830f177e55eff8b2bfe0039f86417af8a82ed3f09965ae53b84
3db5b81c6562e2532c9408d749cf65164247c0ef6d51e4195a44ebdc9a697924
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
e34f77ace31e645473c977184242800d0862f0caf20405a17680df2c8c7e0671
2c31b614b8c942fdfc075aa58da2000c85598f0c5031d565ffafeb60c75eeac5
Binary file not shown.
Loaded 100 of 208 files, more files were not shown because too many files have changed in this diff. Show more