Compare commits

...
Author SHA1 Message Date
Daniel AdámekandDanielADK a05a40e0a3 fix(editor): end a <<- heredoc at its tab-indented delimiter in shell highlighting (#3259)
The stock CodeMirror shell mode ends a heredoc body only on a line equal to the delimiter and drops the `-` of `<<-`, so a tab-indented closing delimiter highlighted the rest of the file as heredoc text. shellInterpolation now tracks heredocs itself: it reads the delimiter as bash does (quotes removed, ends at `|`, `;`, `>`…), strips leading tabs for `<<-`, queues several heredocs on one line, starts bodies on the next line, and leaves `<<` in `$(( ))` and `<<<` here-strings as operators.

Co-authored-by: DanielADK <[email protected]>
2026-11-14 09:08:41 +08:00
acaoandamphetamlne 3b978721e3 test(plugins): exercise remote diagnostics through the host snapshot (#3254)
The remote plugin API test still filled the UI Problems reports after #3103 moved plugin diagnostics to the host-owned snapshot, so it failed on main with an empty result. It now publishes through the project's recording event sink and checks the snapshot's per-file / per-producer semantics, project close and RPC disconnect.

Co-authored-by: amphetamlne <[email protected]>
2026-11-14 09:07:46 +08:00
l0ng-ai 9793857024 fix(plugins): rebuild the diagnostics test fixture after formatting its source (#3264)
#3263 ran cargo fmt on the fixture's src/lib.rs, which changes its source fingerprint, so plugins/build.sh --check reported dist/plugin.wasm as stale.
2026-11-14 09:05:35 +08:00
l0ng-ai 8fb3b7b084 fix(vcs): keep F7 working in the diff it opens for the next file (#3263)
F7 / Shift+F7 past the last difference opened the neighbouring file with focus on the editor tab's host, so the diff's keymap no longer applied; it had only worked through a stale navigator registration that #3137 removed. The diff now forwards the host's focus to its focused pane, as the image and hex viewers do. Also fixes the htmlFrame test's race with its listener effect and formats the diagnostics plugin fixture, so main's Frontend and Plugins jobs pass.
2026-11-14 09:01:07 +08:00
Deng Liuanddengliu 22fe1702be feat(plugins): dynamic file and folder icons by path (API 1.8) (#3257)
Static [[contributes.fileIcons]] entries match file names only, so a plugin could not tell a Helm chart folder, its templates or a Kustomize overlay from any other YAML file or folder. API 1.8 adds decorations.set-file-icon and the batched set-file-icons (up to 10,000 paths per set, requires project.read): a plugin assigns one of its declared file icons to an exact file or plain-folder path, shown in the project tree and editor tabs after the user's own file type icons. Under engines.openidea = "^1.8" a fileIcons entry may omit patterns, for icons used only this way. Both WIT copies, the SDK (host functions and mock), the plugins README and the bundled components are updated.

Co-authored-by: dengliu <[email protected]>
2026-11-14 07:20:33 +08:00
János Kranczlerandjanoskranczler ad434af78c feat(agents): import sessions from other apps from the Agents tool window (#3255)
Importing Claude Code / Codex sessions started outside OpenIDEA was only reachable from the history popup of a docked session and imported one row at a time. A new Import Sessions from Other Apps… action is listed in the Agents tool window's options menu and in the empty task list. It opens a chooser that rescans on open, filters by title or folder, selects several sessions (modifier-click, Shift-click, Cmd/Ctrl-A, Select All) and imports them in one go: a single session opens as before, several are added to the list without opening tabs, failures are reported together, and Cancel stops the batch after the import in flight.

The set of sessions being imported now lives in the store, so neither the chooser nor the docked popup can import a session twice, and late scan results after a project switch are dropped.

Co-authored-by: janoskranczler <[email protected]>
2026-11-14 05:56:28 +08:00
l0ng-ai 6d103df5cf feat(run): show what each Java test printed, for Maven and Gradle (#3256)
Selecting a test in the Run tool window's test tree shows only that test's
output, and a class shows its own output followed by its tests'.

- Maven: Surefire's console names failures but not what passed tests printed.
  The JUnit XML reports carry each test case's <system-out>/<system-err>;
  they are now read into a separate `stdout` field and shown for every test,
  before the failure details. Failures already reported on the console get
  the printed lines in front of their stack trace.
- Gradle: output of a class's own setup and teardown (@BeforeAll, @AfterAll)
  arrived under the class descriptor and showed up as an extra child named
  after the class. The init script now sends it without a test name and it
  belongs to the class node.
2026-11-14 05:51:58 +08:00
LunarFellerandLunarFeller e2d089a0c4 fix(agents): preserve prompt text during native image paste (#3250)
When the clipboard event carried no data (WebKitGTK pasting a screenshot), the composer started reading the image from the native clipboard but returned false, so CodeMirror still handled the paste and replaced the selected prompt text with an empty string. The handler now prevents the default paste and reports it as handled; the image is still attached when the native read returns one, and the prompt and selection stay as they were in every other outcome. The clipboard fixtures in the tests now implement getData so they go through CodeMirror's own paste path. This is the only native-clipboard paste fallback in the frontend.

Co-authored-by: LunarFeller <[email protected]>
2026-11-14 05:41:38 +08:00
l0ng-ai 47fa0e6f79 feat(typescript): run TypeScript 7's own language server when it is the selected TypeScript (#3252)
TypeScript 7 has no tsserver or JavaScript API; its platform package's native executable serves LSP itself. When the TypeScript setting selects one (the project's or a chosen directory), vtsls / typescript-language-server launch that executable with --lsp --stdio instead. The project's TypeScript 7 is listed as "TypeScript 7.x (native)".
2026-11-14 04:39:59 +08:00
l0ng-ai 921f1f2b2d fix(lsp): run a server chosen from a Node.js installation on that Node (#3251)
A server Path pointing at a script in a Node.js installation's bin (nvm's bin/tsc, bin/npx) ran through #!/usr/bin/env node on the project's Node.js interpreter or the first node on PATH; TypeScript 7's extensionless ESM tsc crashed on Node 20.9. That installation's bin now comes first on the server's PATH.
2026-11-14 04:32:29 +08:00
l0ng-ai 88f73630a6 feat(tools): offer updates for the language servers and debuggers the IDE installed (#3247)
Settings › Languages & Frameworks › Language Servers looks for newer versions of the tools in the IDE's tools folder once per session and from its refresh button: release downloads resolve their latest release as an install does, npm packages ask npm view through the user's registry. A tool with a newer version gets an Update tag, the available version in its details and an Update button that installs it.
2026-11-14 03:38:51 +08:00
l0ng-ai 504ffac10a test(rpc): check handler argument names against the client bindings (#3244)
Remote command handlers read arguments by name; a name the desktop client doesn't send reads as null (#3231). The test checks every c.arg / c.sender / c.channel name of each .add("command", …) registration in crates/ide-host/src against that command's arguments in src/bindings.ts.
2026-11-14 03:21:40 +08:00
l0ng-ai 5ee558d1a7 fix(database): keep expanded nodes loaded after editing a data source (#3243)
saveSource forgot the source's cached metadata before writing the new settings, so the following refresh found nothing loaded and reloaded only the first level; expanded schemas and tables stayed on Loading. saveSource now captures the loaded levels before forgetting them and passes them to refresh.
2026-11-14 03:16:32 +08:00
147 changed files with 2419 additions and 258 deletions

No files matched your search

+12 -1
View File
@@ -6,7 +6,7 @@ use crate::rpc::Dispatcher;
use ide_core::{IdeError, IdeResult, RequestId};
use ide_sdk::Sdk;
use ide_tools::jdk::{JdkReleases, JdkVendor};
use ide_tools::{InstallEvent, ToolInfo, Tools};
use ide_tools::{InstallEvent, ToolInfo, ToolUpdate, Tools};
use std::path::PathBuf;
/// The installer, reporting the executables chosen in the window's settings.
@@ -89,6 +89,16 @@ pub async fn tools_install(
Ok(info)
}
/// Newer versions of the tools the IDE installed (release downloads and npm packages).
/// Cancel with `cancel_request(request)`.
pub async fn tools_updates(host: &Host, window: &str, request: RequestId) -> IdeResult<Vec<ToolUpdate>> {
let tools = tools(host, window)?;
let cancel = host.begin_request(request);
let result = tokio::task::spawn_blocking(move || tools.updates(&cancel)).await.map_err(join_err);
host.end_request(request);
result?
}
/// Removes a tool the IDE installed.
pub async fn tools_uninstall(host: &Host, window: &str, name: String) -> IdeResult<ToolInfo> {
let tools = tools(host, window)?;
@@ -185,6 +195,7 @@ pub(crate) fn register(d: &mut Dispatcher) {
tools_install(&c.host, &c.window, c.arg("name")?, c.arg("request")?, c.sender("onEvent")?).await
});
d.add("tools_uninstall", |c| async move { tools_uninstall(&c.host, &c.window, c.arg("name")?).await });
d.add("tools_updates", |c| async move { tools_updates(&c.host, &c.window, c.arg("request")?).await });
d.add("jdk_download_vendors", |c| async move { Ok(jdk_download_vendors(&c.host, &c.window)) });
d.add("jdk_download_releases", |c| async move {
jdk_download_releases(&c.host, &c.window, c.arg("vendor")?, c.arg("request")?).await
+77 -13
View File
@@ -1642,6 +1642,9 @@ impl HostApi for PluginApi {
) -> Result<(), HostError> {
self.set_decorations(path, set, Vec::new(), item, true)
}
fn file_icon_decorations(&self, set: &str, items: Vec<(PathBuf, String)>) -> Result<(), HostError> {
self.set_file_icon_decorations(set, items)
}
fn vcs_log(
&self,
root: &Path,
@@ -4124,6 +4127,9 @@ impl PluginApi {
{
return Err(HostError::Invalid("a file note is one non-empty line of at most 200 characters".into()));
}
if let Some(icon) = file.as_ref().and_then(|f| f.icon.as_ref()) {
ide_extension::validate_item_id(icon, "file icon id").map_err(HostError::Invalid)?;
}
items.retain(|d| decoration_style(&d.style));
if items.iter().any(|d| d.range.start > d.range.end) {
return Err(HostError::Invalid("reversed decoration range".into()));
@@ -4132,24 +4138,14 @@ impl PluginApi {
// The decoration follows the file, not the window's project: editors render
// whatever they have open, including files outside every project root. The
// set's root stays the path's own (deepest open) project for tree filtering.
let root = self
.shared
.workspace()
.map(|w| {
w.projects()
.into_iter()
.filter(|root| path.starts_with(root))
.max_by_key(|root| root.components().count())
})
.unwrap_or_default()
.unwrap_or_else(|| path.parent().map(Path::to_path_buf).unwrap_or_else(|| path.to_path_buf()));
let root = self.decoration_root(path);
Some(super::DecorationSet {
root: root.into(),
path: ide_core::IpcPath(path.to_path_buf()),
set_id: set.into(),
tree,
items,
file: file.filter(|f| f.note.is_some() || decoration_style(&f.style)),
file: file.filter(|f| f.note.is_some() || f.icon.is_some() || decoration_style(&f.style)),
})
} else {
None
@@ -4177,6 +4173,60 @@ impl PluginApi {
self.shared.sink.emit(None, PluginEvent::DecorationsChanged { plugin_id: self.id().into(), sets });
Ok(())
}
fn decoration_root(&self, path: &Path) -> PathBuf {
self.shared
.workspace()
.map(|w| {
w.projects()
.into_iter()
.filter(|root| path.starts_with(root))
.max_by_key(|root| root.components().count())
})
.unwrap_or_default()
.unwrap_or_else(|| path.parent().map(Path::to_path_buf).unwrap_or_else(|| path.to_path_buf()))
}
fn set_file_icon_decorations(&self, set: &str, items: Vec<(PathBuf, String)>) -> Result<(), HostError> {
if set.is_empty() || set.len() > 128 {
return Err(HostError::Invalid("set id must be 1-128 bytes".into()));
}
if items.len() > 10_000 {
return Err(HostError::Invalid("at most 10000 file icons per set".into()));
}
let mut by_path = BTreeMap::new();
for (path, icon) in items {
ide_extension::validate_item_id(&icon, "file icon id").map_err(HostError::Invalid)?;
by_path.insert(path, icon);
}
let mut all = self.shared.decorations.lock();
let mut sets = all.get(self.id()).cloned().unwrap_or_default();
sets.retain(|s| !(s.tree && s.set_id == set));
for (path, icon) in by_path {
let root = self.decoration_root(&path);
sets.push(super::DecorationSet {
root: root.into(),
path: ide_core::IpcPath(path),
set_id: set.into(),
tree: true,
items: Vec::new(),
file: Some(ide_plugin::FileDecoration {
badge: None,
style: String::new(),
tooltip: None,
note: None,
icon: Some(icon),
}),
});
}
let names: BTreeSet<_> = sets.iter().filter(|s| s.tree).map(|s| &s.set_id).collect();
if names.len() > 50 || serde_json::to_vec(&sets).map_err(failed)?.len() > 4 << 20 {
return Err(HostError::Invalid("decoration sets exceed plugin limits".into()));
}
all.insert(self.id().into(), sets.clone());
self.shared.sink.emit(None, PluginEvent::DecorationsChanged { plugin_id: self.id().into(), sets });
Ok(())
}
}
fn decoration_style(style: &str) -> bool {
matches!(
@@ -4330,7 +4380,8 @@ mod workbench_tests {
badge: Some("abc".into()),
style: "info".into(),
tooltip: None,
note: None
note: None,
icon: None,
})
)
.is_err()
@@ -4340,6 +4391,7 @@ mod workbench_tests {
style: String::new(),
tooltip: Some("why".into()),
note: Some(text.into()),
icon: None,
};
api.file_decorations(Some(&path), "notes", Some(note("entry point"))).unwrap();
let notes = |api: &PluginApi| {
@@ -4353,6 +4405,18 @@ mod workbench_tests {
for bad in ["", "two\nlines", &"x".repeat(201)] {
assert!(api.file_decorations(Some(&path), "notes", Some(note(bad))).is_err(), "{bad:?}");
}
api.file_icon_decorations(
"icons",
vec![(path.clone(), "helm-chart".into()), (root.join("chart"), "helm-dir".into())],
)
.unwrap();
let icons = api.shared.decoration_sets()["test"]
.iter()
.filter(|s| s.tree && s.set_id == "icons")
.filter_map(|s| s.file.as_ref().and_then(|f| f.icon.clone()))
.collect::<Vec<_>>();
assert_eq!(icons.len(), 2);
assert!(api.file_icon_decorations("icons", vec![(path.clone(), "BAD ID".into())]).is_err());
api.shared.clear_plugin("test", true);
assert!(api.shared.decoration_sets().is_empty());
}
+20 -5
View File
@@ -743,6 +743,7 @@ pub(super) fn custom_editor(pkg: &Package, decl: toml::Value) -> Result<UiDecl,
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct FileIconDecl {
id: String,
#[serde(default)]
patterns: Vec<String>,
icon: PathBuf,
#[serde(default)]
@@ -754,13 +755,19 @@ pub fn file_icon_asset(id: &str, dark: bool) -> String {
format!("fileIcon/{id}/{}", if dark { "dark" } else { "light" })
}
/// `[[contributes.fileIcons]]`: an icon for the files matching `patterns`,
/// whatever icon theme is in use (the user's own file type icons still win).
/// `[[contributes.fileIcons]]`: an icon for the files matching `patterns` (or
/// assigned dynamically through `decorations.set-file-icon` when `patterns` is
/// omitted in API 1.8), whatever icon theme is in use (the user's own file type
/// icons still win).
pub(super) fn file_icon(pkg: &Package, decl: toml::Value) -> Result<UiDecl, String> {
let d: FileIconDecl = decode(FILE_ICONS, decl)?;
validate_item_id(&d.id, "file icon id")?;
let what = format!("file icon {:?}", d.id);
file_patterns(&d.patterns, &what)?;
if d.patterns.is_empty() {
pkg.manifest.needs_api("contributes.fileIcons without patterns", 8)?;
} else {
file_patterns(&d.patterns, &what)?;
}
let mut files = Vec::new();
let mut assets = Vec::new();
for (path, dark) in [(Some(&d.icon), false), (d.icon_dark.as_ref(), true)] {
@@ -769,8 +776,16 @@ pub(super) fn file_icon(pkg: &Package, decl: toml::Value) -> Result<UiDecl, Stri
assets.push((file_icon_asset(&d.id, dark), svg));
files.push(path.clone());
}
let json = serde_json::json!({ "id": d.id, "patterns": d.patterns, "dark": d.icon_dark.is_some() });
let mut out = UiDecl::new(d.id.clone(), d.patterns.join(", "), json);
let bytes = serde_json::to_vec(&assets).map_err(|e| e.to_string())?;
let revision = blake3::hash(&bytes).to_hex().to_string();
let json = serde_json::json!({
"id": d.id,
"patterns": d.patterns,
"dark": d.icon_dark.is_some(),
"revision": revision,
});
let label = if d.patterns.is_empty() { d.id.clone() } else { d.patterns.join(", ") };
let mut out = UiDecl::new(d.id.clone(), label, json);
out.files = files;
out.assets = assets;
Ok(out)
+37
View File
@@ -3760,6 +3760,43 @@ path = "templates/new.draw"
assert!(f.host.web_page("web-demo", "draw").is_err());
}
#[test]
fn file_icons_without_patterns_require_api_1_8_and_project_assets() {
let f = fixture(Limits::default());
let tmp = tempfile::tempdir().unwrap();
let pkg = tmp.path().join("dynamic-icons");
std::fs::create_dir_all(pkg.join("icons")).unwrap();
std::fs::write(pkg.join("icons/helm-dir.svg"), r#"<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="6"/></svg>"#)
.unwrap();
let write_manifest = |api: &str| {
std::fs::write(
pkg.join("plugin.toml"),
format!(
r#"id = "dynamic-icons"
name = "Dynamic Icons"
version = "0.1.0"
engines.openidea = "{api}"
[[contributes.fileIcons]]
id = "helm-dir"
icon = "icons/helm-dir.svg"
"#
),
)
.unwrap();
};
write_manifest("^1.7");
let err = f.host.install_from_dir(&pkg, &Consent::all()).unwrap_err();
assert!(err.message.contains("1.8"), "{err:?}");
write_manifest("^1.8");
let info = f.install(&pkg);
assert!(info.error.is_none(), "{:?}", info.error);
let assets = f.host.assets("dynamic-icons", "fileIcon/helm-dir/").unwrap();
assert_eq!(assets.len(), 1);
assert!(assets[0].1.contains("<circle"));
}
#[test]
fn file_templates_fixture_installs_without_write_capabilities() {
let f = fixture(Limits::default());
+49 -16
View File
@@ -367,6 +367,7 @@ async fn remote_projects_get_plugin_views_and_custom_editors() {
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn api14_remote_calls_keep_browser_listeners_and_secrets_on_the_desktop() {
use ide_core::{Diagnostic, Pos, Range, Severity};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let tmp = tempfile::tempdir().unwrap();
let desktop = Arc::new(
@@ -426,42 +427,70 @@ async fn api14_remote_calls_keep_browser_listeners_and_secrets_on_the_desktop()
"severity": "warning", "message": message, "source": "remote-check", "code": null
}]})
};
let publish = |path: &Path, source: &str, message: Option<&str>| {
let items = message
.map(|message| Diagnostic {
range: Range::new(Pos::new(0, 1), Pos::new(0, 3)),
severity: Severity::Warning,
message: message.into(),
source: Some(source.into()),
code: None,
unnecessary: false,
deprecated: false,
server: None,
})
.into_iter()
.collect();
host.project("main").unwrap().sink.emit(IdeEvent::Diagnostics {
path: path.to_path_buf().into(),
source: source.into(),
items,
});
};
let files = json!([
problem(&remote_file, "remote warning"),
problem(&other_file, "other warning"),
problem(&outside_file, "outside warning")
]);
invoke(&peer, "mcp_report_problems", json!({"reset": true, "files": files})).await.unwrap();
// UI reports are not the source of the plugin's host-owned snapshot.
assert_eq!(invoke(&peer, "plugin_run_command", run("problems", None)).await.unwrap(), json!("[]"));
publish(&remote_file, "remote-check", Some("remote warning"));
publish(&other_file, "remote-check", Some("other warning"));
publish(&outside_file, "remote-check", Some("outside warning"));
let problems = invoke(&peer, "plugin_run_command", run("problems", None)).await.unwrap();
let problems = problems.as_str().unwrap();
assert!(problems.contains("remote warning"), "{problems}");
assert!(problems.contains("other warning"), "{problems}");
assert!(!problems.contains("outside warning"), "{problems}");
assert!(problems.contains("outside warning"), "{problems}");
for path in ["remote.txt", remote_file.to_str().unwrap()] {
let problems = invoke(&peer, "plugin_run_command", run("problems", Some(path))).await.unwrap();
let problems = problems.as_str().unwrap();
assert!(problems.contains("remote warning"), "{problems}");
assert!(!problems.contains("other warning"), "{problems}");
assert!(!problems.contains("outside warning"), "{problems}");
assert!(problems.contains(remote_file.to_str().unwrap()), "{problems}");
}
assert_eq!(invoke(&peer, "plugin_run_command", run("problems", Some("missing.txt"))).await.unwrap(), json!("[]"));
invoke(&peer, "mcp_report_problems", json!({"reset": false, "files": [{"path": remote_file, "items": []}]}))
.await
.unwrap();
publish(&remote_file, "second-check", Some("second warning"));
invoke(&peer, "mcp_report_problems", json!({"reset": true, "files": []})).await.unwrap();
let problems = invoke(&peer, "plugin_run_command", run("problems", None)).await.unwrap();
assert!(problems.as_str().unwrap().contains("remote warning"));
assert!(problems.as_str().unwrap().contains("second warning"));
publish(&remote_file, "remote-check", None);
let problems = invoke(&peer, "plugin_run_command", run("problems", None)).await.unwrap();
assert!(!problems.as_str().unwrap().contains("remote warning"));
assert!(problems.as_str().unwrap().contains("second warning"));
assert!(problems.as_str().unwrap().contains("other warning"));
invoke(
&peer,
"mcp_report_problems",
json!({"reset": true, "files": [problem(&remote_file, "replacement warning")]}),
)
.await
.unwrap();
publish(&remote_file, "second-check", Some("replacement warning"));
let problems = invoke(&peer, "plugin_run_command", run("problems", None)).await.unwrap();
assert!(problems.as_str().unwrap().contains("replacement warning"));
assert!(!problems.as_str().unwrap().contains("other warning"));
invoke(&peer, "mcp_report_problems", json!({"reset": true, "files": []})).await.unwrap();
assert!(!problems.as_str().unwrap().contains("second warning"));
assert!(problems.as_str().unwrap().contains("other warning"));
publish(&remote_file, "second-check", None);
publish(&other_file, "remote-check", None);
publish(&outside_file, "remote-check", None);
assert_eq!(invoke(&peer, "plugin_run_command", run("problems", None)).await.unwrap(), json!("[]"));
invoke(&peer, "plugin_run_command", run("open-editor", Some("remote.txt"))).await.unwrap();
invoke(&peer, "plugin_run_command", run("decorations", remote_file.to_str())).await.unwrap();
@@ -513,11 +542,11 @@ async fn api14_remote_calls_keep_browser_listeners_and_secrets_on_the_desktop()
stream.read_to_string(&mut response).await.unwrap();
assert!(response.contains("You can close this tab"));
assert_eq!(oauth.await.unwrap().unwrap(), json!("code=desktop%2Bcode"));
invoke(&peer, "mcp_report_problems", json!({"reset": true, "files": files})).await.unwrap();
publish(&remote_file, "remote-check", Some("remote warning"));
invoke(&peer, "close_project", json!({})).await.unwrap();
invoke(&peer, "open_project", json!({"path": project})).await.unwrap();
assert_eq!(invoke(&peer, "plugin_run_command", run("problems", None)).await.unwrap(), json!("[]"));
invoke(&peer, "mcp_report_problems", json!({"reset": true, "files": files})).await.unwrap();
publish(&remote_file, "remote-check", Some("remote warning"));
peer.kill();
tokio::time::timeout(Duration::from_secs(5), server).await.unwrap().unwrap();
let plugin_host = plugins.host().clone();
@@ -537,7 +566,11 @@ async fn api14_remote_calls_keep_browser_listeners_and_secrets_on_the_desktop()
})
.await
.unwrap();
assert_eq!(result.as_deref(), Some("[]"));
// Disconnect clears desktop reports, not the still-live host project snapshot.
let problems = result.as_deref().unwrap();
assert!(problems.contains("remote warning"), "{problems}");
assert!(problems.contains("remote-check"), "{problems}");
assert!(problems.contains(remote_file.to_str().unwrap()), "{problems}");
plugins.host().shutdown();
desktop.shutdown();
}
+64
View File
@@ -531,3 +531,67 @@ async fn renames_a_branch_with_the_clients_argument_names() {
invoke(&peer, "close_project", json!({})).await.unwrap();
peer.kill();
}
/// A handler reads its arguments by the names the desktop client sends: `src/bindings.ts`
/// passes them in camelCase, and a name the client never sends reads as `null` (an
/// `Option` silently `None`, anything else an "invalid type: null" error).
#[test]
fn handlers_read_the_argument_names_the_client_sends() {
use std::collections::{BTreeSet, HashMap};
use std::path::Path;
let repo = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let bindings = std::fs::read_to_string(repo.join("src/bindings.ts")).unwrap();
let mut sent = HashMap::<&str, BTreeSet<&str>>::new();
for part in bindings.split("__TAURI_INVOKE(\"").skip(1) {
let (command, rest) = part.split_once('"').unwrap();
let args = rest.strip_prefix(", {").and_then(|r| r.split_once('}')).map_or("", |(args, _)| args);
let names = args.split(',').map(|a| a.split(':').next().unwrap().trim()).filter(|a| !a.is_empty());
sent.insert(command, names.collect());
}
fn sources(dir: &Path, out: &mut Vec<std::path::PathBuf>) {
for entry in std::fs::read_dir(dir).unwrap() {
let path = entry.unwrap().path();
if path.is_dir() {
sources(&path, out);
} else if path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}
let mut files = Vec::new();
sources(&repo.join("crates/ide-host/src"), &mut files);
let mut checked = 0;
let mut wrong = Vec::new();
for file in files {
let src = std::fs::read_to_string(&file).unwrap();
// `.add("name", …)`, also with the name on the next line.
let starts: Vec<usize> = src
.match_indices(".add(")
.map(|(i, m)| i + m.len())
.filter_map(|i| src[i..].trim_start().strip_prefix('"').map(|rest| src.len() - rest.len()))
.collect();
for (n, &start) in starts.iter().enumerate() {
let body = &src[start..starts.get(n + 1).copied().unwrap_or(src.len())];
let (command, body) = body.split_once('"').unwrap();
let Some(sent) = sent.get(command) else { continue };
checked += 1;
for reader in ["c.arg", "c.sender", "c.channel"] {
for (at, _) in body.match_indices(reader) {
let rest = &body[at + reader.len()..];
let rest = match rest.strip_prefix("::<") {
Some(generic) => &generic[generic.find(">(").unwrap() + 1..],
None => rest,
};
let Some(name) = rest.strip_prefix("(\"").and_then(|r| r.split_once('"')).map(|(n, _)| n) else {
continue;
};
if !sent.contains(name) {
wrong.push(format!("{command} reads `{name}`; the client sends {sent:?}"));
}
}
}
}
}
assert!(checked > 400, "found only {checked} handlers with client bindings");
assert!(wrong.is_empty(), "{wrong:#?}");
}
+5
View File
@@ -234,6 +234,11 @@ pub trait HostApi: Send + Sync {
let _ = (path, set, item);
Err(HostError::Failed("decorations unavailable".into()))
}
/// Replace all `(path, icon)` entries of a tree decoration set at once (API 1.8).
fn file_icon_decorations(&self, set: &str, items: Vec<(PathBuf, String)>) -> Result<(), HostError> {
let _ = (set, items);
Err(HostError::Failed("decorations unavailable".into()))
}
/// Trusted repository history.
fn vcs_log(
&self,
+3
View File
@@ -932,6 +932,9 @@ pub struct FileDecoration {
/// Text shown after the name (API 1.7 `set-file-note`).
#[serde(default)]
pub note: Option<String>,
/// `[[contributes.fileIcons]]` id for this file or folder (API 1.8 `set-file-icon`).
#[serde(default)]
pub icon: Option<String>,
}
/// Commit metadata, date in Unix seconds.
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
+35 -2
View File
@@ -2505,7 +2505,13 @@ impl bindings::openidea::plugin::decorations::Host for StoreData {
.file_decorations(
Some(&real),
&set_id,
Some(crate::FileDecoration { badge: item.badge, style: item.style, tooltip: item.tooltip, note: None }),
Some(crate::FileDecoration {
badge: item.badge,
style: item.style,
tooltip: item.tooltip,
note: None,
icon: None,
}),
)
.map_err(to_wit))
}
@@ -2523,9 +2529,36 @@ impl bindings::openidea::plugin::decorations::Host for StoreData {
Ok(real) => real,
Err(e) => return Ok(Err(e)),
};
let item = crate::FileDecoration { badge: None, style: String::new(), tooltip, note: Some(note) };
let item = crate::FileDecoration { badge: None, style: String::new(), tooltip, note: Some(note), icon: None };
Ok(self.host.file_decorations(Some(&real), &set_id, Some(item)).map_err(to_wit))
}
fn set_file_icon(&mut self, path: String, set_id: String, icon: String, tooltip: Option<String>) -> Fallible<()> {
if let Err(e) = self.require(Capability::ProjectRead) {
return Ok(Err(e));
}
let real = match self.decoration_path(&path) {
Ok(real) => real,
Err(e) => return Ok(Err(e)),
};
let item = crate::FileDecoration { badge: None, style: String::new(), tooltip, note: None, icon: Some(icon) };
Ok(self.host.file_decorations(Some(&real), &set_id, Some(item)).map_err(to_wit))
}
fn set_file_icons(&mut self, set_id: String, items: Vec<(String, String)>) -> Fallible<()> {
if let Err(e) = self.require(Capability::ProjectRead) {
return Ok(Err(e));
}
if items.len() > 10_000 {
return Ok(Err(wit::Error::Invalid("at most 10000 file icons per set".into())));
}
let mut resolved = Vec::with_capacity(items.len());
for (path, icon) in items {
match self.decoration_path(&path) {
Ok(real) => resolved.push((real, icon)),
Err(e) => return Ok(Err(e)),
}
}
Ok(self.host.file_icon_decorations(&set_id, resolved).map_err(to_wit))
}
}
pub(crate) mod language_features_bindings {
Binary file not shown.
@@ -1 +1 @@
6c50332c868a890b91a7f92ef7f92e76a4818b7435291e99c77c0b9f0931b81c
e616b87b24371581ad648f9c7072259de7732d122b5887b488f7bd480ebd69c2
Binary file not shown.
@@ -1 +1 @@
fdcb208365db90bcd06d0d0df3d436b69d1229c40f7c63dd6f1ab768e0d236b8
75eeecc467bff141bda14f1f080633348af348f7259e793be81e6548d2008669
Binary file not shown.
@@ -1 +1 @@
3108abe8d381b224ec4b44ecf4c43c045671bd5e221ee21582e25497f8c14dff
4aaf7754e895f022d34dad05bd2b8018e3a61ba18771b8c95d9f070cb212530c
Binary file not shown.
@@ -1 +1 @@
876a84b7da5a4b27fce47cf71a4dc28eb1ea6c9d54283c1b73a2ae2411653bf5
e573144879a7caa0975e17b233476eb317ff54bef23fd8fa64280d3649bad700
Binary file not shown.
@@ -1 +1 @@
234dc693e01e484b91fa7f2d0ac0e7757fb69c9fba81f06bf8ef1cd3624228d8
36dd40ef751a617f1ad59491312889c12481b05666e580f21f7c2552df76edbc
+2 -6
View File
@@ -39,12 +39,8 @@ impl Plugin for Fixture {
hover: None,
after: Some(after.into()),
};
host::set_decorations(
path,
"fixture",
&[item("typo", "typo message"), item("weak", "weak message")],
)
.map(|()| None)
host::set_decorations(path, "fixture", &[item("typo", "typo message"), item("weak", "weak message")])
.map(|()| None)
}
_ => Err(Error::NotFound(id.into())),
}
Binary file not shown.
@@ -1 +1 @@
927c329fd21d12fd5f423b937e1be756be45a4c6dceee08679e762d5d9f511f2
724ed3bbd828e4fc1925c1b555bdaec9268633cb587806a661f8838e5a540545
Binary file not shown.
@@ -1 +1 @@
e7c77fde915e0765f91929dba2aa8eff606f2302e078b7305d272a4de5b9d80b
1a3685234f1b3b0957184c44c603c0eefe7e3084b88d0df32da268b29292ee92
Binary file not shown.
@@ -1 +1 @@
3feb9df8b408dedbc3586864db00b92c8cb244c8ad5b8f10c95edb8eb3d30df1
4283de821d69a18438f58062e4bc28b63ba5025cc5f9fa51f21ff475b677a6a5
Binary file not shown.
@@ -1 +1 @@
3bfce29f90b9d2f20082b9bdd0957c6c58c265a346305ec948f514537a4579c8
0f25487b8e488e5ec01fabd2951bdb03d80bc26e4e9aa508394a7f9cd09d1759
Binary file not shown.
@@ -1 +1 @@
ca72c5cfa2f76a93bc07495d50e58181a9c6c2fc75854cf548bb0278814ddb87
80f9dc28e4f168dd22ccee1c53e1f6b08c446622eb10f07de29be5fc45e9fb1c
Binary file not shown.
@@ -1 +1 @@
8a4a7df8fd6cf397750ac24cedf06174ed50e4214ef44029e29f7d0557d426ba
5355cb4ef373eba3c54806eb00294fcd5fccb3cf05dae3e1c2a4ad45ab958f03
Binary file not shown.
@@ -1 +1 @@
8d12313675e0869e087ac4c79bdfb1a9738867e28af2437eff27cb07254f572b
fc75ede42e6db7619fa28fc8999f091d39e3c1ed401356db3a3f07de54287c99
Binary file not shown.
@@ -1 +1 @@
1b2ad52d4cbb4abb662be5772f050844836f8e3f0c525edd0de19ff45e95a148
8793601496f1727cf912e4cdf86ec48fc340a17585098e98dee52a125cb8a16d
Binary file not shown.
@@ -1 +1 @@
13babf73d8d3d029aba26641b01a4add3abb3dab84394d8bd676edb064dc0c19
fb2db14a645cad42941d20a25a3ff87ca6fdee635fdb40953383ff30ce16af8a
Binary file not shown.
@@ -1 +1 @@
a7790bf28783d57ed47f635a58981b80ddaecd7074cf88c539465b57132a9f8f
fc66b3b56a1ab1c9d547e3efe6bb4693dca908bb18be8b8ebd26f2fb41e4436c
Binary file not shown.
@@ -1 +1 @@
af7070f6ca3a14cf480260aa21b7a6e95038996d224d40e53a487c96a8f3acf2
c76830df9dee3caba932ffaddaabce88ac7ef23b3957b3e8e570b4939dd58d9d
@@ -1 +1 @@
16930ecfe74d096e6dcaa5fbf35664128b4d3844f47993a947aabe9d2a9650de
3a41a06d54939a9e85fcf6b0bf15b336f928a3caf66576bd2da62c5b8b177c1d
Binary file not shown.
@@ -1 +1 @@
25eaf7ac2837ade4b3204698d3af41854324ece6ec196daba84bea6bb41f6cde
f30a8c0a7893519872628c001314efd567f87f4dfaf718a737ea3631b55881b0
Binary file not shown.
@@ -1 +1 @@
f7cd477be706ee5da2656ffb5435cb2b52443aec1270b3b1d804b2bdc6ed8bc8
cb65533984ffd8684c9e9f5ddeef92a8f387735c12310b25282ee7bbf2d73cd7
Binary file not shown.
@@ -1 +1 @@
c97989134a8e8a0a12013909f4726288b6d9cdaa44d02a58ff505a6cbb1fb9a1
d11f9cb1130f3f3aaa89514bf47ec8ece9b2ca0edf1d82c11f80b262325f16be
Binary file not shown.
@@ -1 +1 @@
67660f5ebbfdd3967fabcc0b5c41f215d4a38d9092b91eb0166f12bccfdffffa
48ea4414469ebd70a89da8a5df7b7bddc4d32f5c50f1759ee3cea14558073c92
Binary file not shown.
@@ -1 +1 @@
09c810ce7ebd820ea21525dc9faea99b13e7665b308cb131510e02b86e763c9f
0c02303182be04ed3776fb532fd8b72e6057aa0fdb92b7d235000146fef4120e
Binary file not shown.
@@ -1 +1 @@
9b3a210440560e05326947c7d549ce691315839daea2931c6a78720f7b0d9e7c
8162a70e848172663463119e6357984a73e54bc71a99b2845621d0733cf9333e
Binary file not shown.
@@ -1 +1 @@
57d8b00b5853ca789fe704f21e4394d1524dd73dfd27ea4a0b34f4b0e4f1a5f8
4524cab0ceac631153a945f6b76f7d40efd3c854740ae22c64580381e9532397
Binary file not shown.
@@ -1 +1 @@
53e355922819b8034c93070323c7eb2b1505d3bf561dd5de6abb930fe514748c
a92f4def072ca4cc805414516ab4f046fc54bcf11394a5413a27ea151c156174
Binary file not shown.
@@ -1 +1 @@
7af929a1adf5d89929d66e78068253ccde9ad5cedee329c2d4db6b7a5e29790e
994fca61bc5a10f0229301406af76c188a9cb2f593db85a403c1ece746e9f7b5
Binary file not shown.
@@ -1 +1 @@
511fab979382c5d64054ba4f5fdfc98d50e252dfd5896265ee589336bf8e7d70
48eaf97f57cc08c3854a1effa37ebf29defca4782f76c7de1da6ef9a02c23441
Binary file not shown.
@@ -1 +1 @@
444497128ec5e1e01229a93d2340b49ccc5bc27e67defb2c4fe19683042442a3
b896043cf261ff4bb19977f4648a736907fa897b8d4f0804077678a5895d1845
+14 -4
View File
@@ -26,8 +26,10 @@ pub struct TestCaseReport {
pub duration_ms: Option<f64>,
/// Failure / error / skip message.
pub message: Option<String>,
/// Failure stack trace and captured output.
/// Failure / error stack trace.
pub output: Option<String>,
/// What the test printed (`<system-out>`, `<system-err>`).
pub stdout: Option<String>,
}
fn decode(s: &str) -> String {
@@ -124,11 +126,13 @@ pub fn parse_report(xml: &str) -> Vec<TestCaseReport> {
if let Some(name) = tag.strip_prefix('/') {
match name.trim() {
"testcase" => out.extend(current.take()),
"failure" | "error" | "rerunFailure" | "rerunError" | "skipped" | "system-out" | "system-err" => {
name @ ("failure" | "error" | "rerunFailure" | "rerunError" | "skipped" | "system-out"
| "system-err") => {
if let (Some(t), Some(c)) = (text.take(), current.as_mut()) {
let t = t.trim_matches('\n');
if !t.trim().is_empty() {
let o = c.output.get_or_insert_with(String::new);
let field = if name.starts_with("system-") { &mut c.stdout } else { &mut c.output };
let o = field.get_or_insert_with(String::new);
if !o.is_empty() {
o.push('\n');
}
@@ -160,6 +164,7 @@ pub fn parse_report(xml: &str) -> Vec<TestCaseReport> {
duration_ms,
message: None,
output: None,
stdout: None,
};
if self_closing {
out.push(case);
@@ -238,6 +243,8 @@ mod tests {
<failure message="expected: &lt;1&gt; but was: &lt;2&gt;" type="org.opentest4j.AssertionFailedError"><![CDATA[org.opentest4j.AssertionFailedError: expected: <1> but was: <2>
at com.example.demo.todo.TodoServiceTests.fails(TodoServiceTests.java:30)
]]></failure>
<system-out><![CDATA[printed before failing
]]></system-out>
</testcase>
<testcase name="later" classname="com.example.demo.todo.TodoServiceTests" time="0">
<skipped message="not yet"/>
@@ -254,10 +261,13 @@ mod tests {
assert_eq!(cases[0].status, TestCaseStatus::Passed);
assert_eq!(cases[0].duration_ms, Some(31.0));
assert_eq!(cases[1].name, "deleteReturnsFalseForUnknownId");
assert_eq!(cases[1].output.as_deref(), Some("hello <world>"));
assert_eq!(cases[1].stdout.as_deref(), Some("hello <world>"));
assert_eq!(cases[1].output, None);
assert_eq!(cases[2].status, TestCaseStatus::Failed);
assert_eq!(cases[2].message.as_deref(), Some("expected: <1> but was: <2>"));
assert!(cases[2].output.as_deref().unwrap().contains("TodoServiceTests.java:30"));
assert!(!cases[2].output.as_deref().unwrap().contains("printed"));
assert_eq!(cases[2].stdout.as_deref(), Some("printed before failing"));
assert_eq!(cases[2].duration_ms, Some(1_234_500.0));
assert_eq!(cases[3].status, TestCaseStatus::Skipped);
assert_eq!(cases[3].message.as_deref(), Some("not yet"));
+27 -2
View File
@@ -3,7 +3,7 @@
use crate::discover::{project_venv_python, workspace_tsdk};
use crate::exec::{path_with, search_path, which};
use crate::npm::{has_ts_api, own_typescript};
use crate::npm::{has_ts_api, native_typescript, own_typescript};
use crate::{angular, astro, godot, pinned, vue};
use crate::{bin_dir, exe, venv_of};
use ide_core::settings::{InspectionLevel, InspectionSettings, LanguageServerSettings, Settings};
@@ -722,6 +722,16 @@ pub fn tsdk(root: &Path, s: &Settings) -> Option<PathBuf> {
}
}
/// TypeScript 7's native executable when the selected TypeScript is one (see
/// [`native_typescript`]); `None` for the bundled one and TypeScript 6 and older.
pub fn native_tsdk(root: &Path, s: &Settings) -> Option<PathBuf> {
match s.languages.typescript.version.trim() {
"" | "bundled" => None,
"workspace" => native_typescript(&root.join("node_modules/typescript")),
custom => native_typescript(&setting_path(root, custom)?),
}
}
const PYTHON_SERVERS: [&str; 7] = ["basedpyright", "pyright", "ty", "pyrefly", "pylsp", "jedi-language-server", "ruff"];
pub(crate) fn merge(base: &mut Value, layer: Value) {
@@ -957,10 +967,15 @@ pub fn resolve_run_command(config: &mut RunConfig, root: &Path, s: &Settings) {
}
pub fn configure_server(cfg: &mut LanguageServerConfig, root: &Path, server_root: &Path, s: &Settings) {
let mut path_dirs: Vec<PathBuf> = Vec::new();
if let Some(custom) = tool_command(root, s, &cfg.command) {
// An executable chosen from a Node.js installation (`~/.nvm/versions/node/v26/bin/tsc`,
// its `npx`) runs on that Node: its `#!/usr/bin/env node` finds the `node` beside it.
if let Some(bin) = custom.parent().filter(|bin| bin.join(exe("node")).is_file()) {
path_dirs.push(bin.to_path_buf());
}
cfg.command = custom.to_string_lossy().into_owned();
}
let mut path_dirs: Vec<PathBuf> = Vec::new();
let name = cfg.name.clone();
let name = name.as_str();
@@ -1135,6 +1150,16 @@ pub fn configure_server(cfg: &mut LanguageServerConfig, root: &Path, server_root
_ => {}
}
}
// TypeScript 7 serves LSP itself: it replaces the tsserver-based server.
if matches!(name, "vtsls" | "typescript-language-server")
&& let Some(native) = native_tsdk(server_root, s).or_else(|| native_tsdk(root, s))
{
cfg.command = native.to_string_lossy().into_owned();
cfg.args = vec!["--lsp".into(), "--stdio".into()];
cfg.initialization_options = None;
cfg.tsserver_project = false;
return;
}
let vue_command = s.tool_path(vue::SERVER).and_then(|p| setting_path(root, p));
let vue_command = vue_command.as_deref().map_or(vue::SERVER.into(), Path::to_string_lossy);
if name == "vtsls"
+14 -2
View File
@@ -749,14 +749,26 @@ pub(crate) fn workspace_tsdk(root: &Path) -> Option<PathBuf> {
fn typescript(root: Option<&Path>) -> Vec<Sdk> {
let mut out = Vec::new();
if let Some(lib) = root.and_then(workspace_tsdk) {
// TypeScript 7 has no `tsserver.js`; its package directory stands for it (`native_tsdk`).
let native = || {
let package = root?.join("node_modules/typescript");
crate::npm::native_typescript(&package).map(|_| package.join("lib"))
};
if let Some(lib) = root.and_then(workspace_tsdk).or_else(native) {
let version = std::fs::read_to_string(lib.parent().unwrap_or(&lib).join("package.json"))
.ok()
.and_then(|t| serde_json::from_str::<serde_json::Value>(&t).ok())
.and_then(|v| v.get("version").and_then(|v| v.as_str()).map(str::to_owned));
out.push(Sdk {
path: lib.clone().into(),
name: format!("TypeScript {}", version.clone().unwrap_or_default()).trim().to_owned(),
name: format!(
"TypeScript {}{}",
version.clone().unwrap_or_default(),
if crate::npm::has_ts_api(&lib) { "" } else { " (native)" }
)
.replace(" ", " ")
.trim()
.to_owned(),
version,
source: "workspace".into(),
home: lib.parent().map(Into::into),
+35
View File
@@ -10,6 +10,41 @@ pub(crate) fn has_ts_api(lib: &Path) -> bool {
lib.join("typescript.js").is_file()
}
/// The native executable of TypeScript 7 (`typescript` 7+, whose `tsc` is native, or
/// `@typescript/native-preview`'s `tsgo`), from its package directory or its `lib`.
/// These packages have no JavaScript API (`lib/typescript.js`); their platform package
/// (`@typescript/typescript-darwin-arm64`) holds the executable, which serves LSP itself
/// (`--lsp --stdio`).
pub(crate) fn native_typescript(dir: &Path) -> Option<PathBuf> {
let package = if dir.join("package.json").is_file() { dir } else { dir.parent()? };
if has_ts_api(&package.join("lib")) {
return None;
}
let manifest: serde_json::Value =
serde_json::from_slice(&std::fs::read(package.join("package.json")).ok()?).ok()?;
let (base, bin) = match manifest.get("name")?.as_str()? {
"typescript" => ("typescript", "tsc"),
"@typescript/native-preview" => ("native-preview", "tsgo"),
_ => return None,
};
let os = match std::env::consts::OS {
"macos" => "darwin",
"windows" => "win32",
os => os,
};
let arch = match std::env::consts::ARCH {
"aarch64" => "arm64",
"x86_64" => "x64",
arch => arch,
};
let platform = format!("{base}-{os}-{arch}");
// Next to the package (`node_modules/@typescript/…`) or nested in it.
package
.ancestors()
.map(|a| a.join("node_modules/@typescript").join(&platform).join("lib").join(crate::exe(bin)))
.find(|exe| exe.is_file())
}
/// The usable TypeScript library Node resolves from an installed server package.
pub(crate) fn own_typescript(package: &Path) -> Option<PathBuf> {
package.ancestors().map(|a| a.join("node_modules/typescript/lib")).find(|l| has_ts_api(l))
+79
View File
@@ -1325,6 +1325,85 @@ fn custom_tool_path_replaces_the_command() {
assert_eq!(cfg.command, "zls");
}
/// A server picked from a Node.js installation (nvm's `bin/tsc`, `bin/npx`) is a
/// `#!/usr/bin/env node` script: it runs on that installation's Node, ahead of the
/// project's Node.js interpreter.
#[test]
fn a_server_chosen_from_a_node_installation_runs_on_that_node() {
let tmp = tempfile::tempdir().unwrap();
let v26 = tmp.path().join("nvm/versions/node/v26.4.0/bin");
let v20 = tmp.path().join("node20/bin");
for dir in [&v26, &v20] {
std::fs::create_dir_all(dir).unwrap();
std::fs::write(dir.join(exe("node")), "").unwrap();
}
std::fs::write(v26.join("tsc"), "#!/usr/bin/env node\n").unwrap();
let mut s = Settings::default();
s.languages.node.interpreter = v20.join(exe("node")).to_string_lossy().into_owned();
s.tools.insert(
"typescript-language-server".into(),
ide_core::settings::ToolSettings { path: v26.join("tsc").to_string_lossy().into_owned() },
);
let mut cfg = server("typescript-language-server");
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
let path: Vec<std::path::PathBuf> = std::env::split_paths(&cfg.env["PATH"]).collect();
assert_eq!(path[..2], [v26.clone(), v20.clone()]);
// Elsewhere, the project's Node.js interpreter comes first.
s.tools.insert(
"typescript-language-server".into(),
ide_core::settings::ToolSettings { path: tmp.path().join("tools/tsc").to_string_lossy().into_owned() },
);
let mut cfg = server("typescript-language-server");
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
assert_eq!(std::env::split_paths(&cfg.env["PATH"]).next(), Some(v20));
}
/// TypeScript 7 (`typescript` 7+, `@typescript/native-preview`) has no tsserver: choosing it
/// runs its native executable's own language server instead of vtsls / typescript-language-server.
#[test]
fn typescript_7_runs_its_own_language_server() {
let tmp = tempfile::tempdir().unwrap();
let modules = tmp.path().join("node_modules");
let os = match std::env::consts::OS {
"macos" => "darwin",
"windows" => "win32",
os => os,
};
let arch = match std::env::consts::ARCH {
"aarch64" => "arm64",
"x86_64" => "x64",
arch => arch,
};
std::fs::create_dir_all(modules.join("typescript/lib")).unwrap();
std::fs::write(modules.join("typescript/package.json"), r#"{"name":"typescript","version":"7.0.2"}"#).unwrap();
let native = modules.join(format!("@typescript/typescript-{os}-{arch}/lib")).join(exe("tsc"));
std::fs::create_dir_all(native.parent().unwrap()).unwrap();
std::fs::write(&native, "").unwrap();
let mut s = Settings::default();
s.languages.typescript.version = "workspace".into();
for name in ["vtsls", "typescript-language-server"] {
let mut cfg = server(name);
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
assert_eq!(Path::new(&cfg.command), native, "{name}");
assert_eq!(cfg.args, ["--lsp", "--stdio"]);
assert!(cfg.initialization_options.is_none());
}
// Picked as a directory (its `lib`, as for older versions), too.
s.languages.typescript.version = modules.join("typescript/lib").to_string_lossy().into_owned();
let mut cfg = server("vtsls");
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
assert_eq!(Path::new(&cfg.command), native);
// TypeScript 6 keeps the tsserver-based server, pointed at it.
std::fs::write(modules.join("typescript/lib/typescript.js"), "").unwrap();
std::fs::write(modules.join("typescript/lib/tsserver.js"), "").unwrap();
s.languages.typescript.version = "workspace".into();
let mut cfg = server("vtsls");
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
assert_eq!(cfg.command, "vtsls");
}
#[test]
fn the_godot_setting_applies_to_server_debugger_and_run_configurations() {
let tmp = tempfile::tempdir().unwrap();
+82
View File
@@ -104,6 +104,17 @@ pub enum InstallEvent {
Progress { fraction: Option<f64>, message: String },
}
/// A newer version of a tool the IDE installed, as its install source offers it now.
#[derive(Clone, Debug, PartialEq, Serialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct ToolUpdate {
pub name: String,
/// The installed version.
pub current: String,
/// The version installing the tool again would get.
pub latest: String,
}
#[derive(Serialize, Deserialize)]
struct Manifest {
version: Option<String>,
@@ -1437,6 +1448,77 @@ impl Tools {
self.write_manifest(spec.name, &Manifest { version: Some(dl.version.clone()), source: Some(dl.url.clone()) })
}
/// Newer versions of the tools the IDE installed into its tools folder, for the install
/// sources that can be asked: release downloads (GitHub, Open VSX, Maven Central, …) and
/// npm packages. A tool whose lookup fails (offline, rate limited) is left out.
pub fn updates(&self, cancel: &CancellationToken) -> IdeResult<Vec<ToolUpdate>> {
let installed: Vec<(&ToolSpec, Method, String)> = TOOLS
.iter()
.filter_map(|spec| {
let method = self.install_method(spec);
Some((spec, method, self.installed_version(spec, method)?))
})
.collect();
let updates = std::thread::scope(|scope| {
let lookups: Vec<_> = installed
.iter()
.map(|(spec, method, current)| {
scope.spawn(move || match self.for_method(*method).latest_version(*method, cancel) {
Ok(Some(latest)) if latest != *current => {
Some(ToolUpdate { name: spec.name.into(), current: current.clone(), latest })
}
Ok(_) => None,
Err(e) => {
tracing::info!("update check for {}: {}", spec.name, e.message);
None
}
})
})
.collect();
lookups.into_iter().filter_map(|lookup| lookup.join().ok().flatten()).collect()
});
if cancel.is_cancelled() {
return Err(IdeError::cancelled());
}
Ok(updates)
}
/// The version of `spec` in the IDE's tools folder, when `method` (how it was installed)
/// can tell the latest one.
fn installed_version(&self, spec: &ToolSpec, method: Method) -> Option<String> {
match method {
Method::Release { .. } => {
managed_bin_dir(&self.root, spec, self.platform).and_then(|dir| self.exe_in(&dir, spec.commands[0]))?;
self.manifest(spec.name)?.version
}
// A pinned package (`typescript@6`) stays on its major version.
Method::Npm { packages } if npm_name(packages[0]) == packages[0] => self.npm_package_version(packages[0]),
_ => None,
}
}
/// The version installing with `method` would get now.
fn latest_version(&self, method: Method, cancel: &CancellationToken) -> IdeResult<Option<String>> {
match method {
Method::Release { source, assets, .. } => {
let http = Http::new(&self.system_path)?;
Ok(Some(self.resolve_release(&http, source, assets, cancel)?.version))
}
Method::Npm { packages } => {
let (manager, _) = self.npm_manager()?;
let mut c = self.command(manager);
// The registry the user's npm configuration names (a mirror, a proxy). A cold npm
// on a slow registry takes several seconds per package, more when run side by side.
c.args(["view", packages[0], "version"]).current_dir(&self.root);
match run_quiet(c, Duration::from_secs(60)) {
Some((true, out)) => Ok(out.lines().map(str::trim).rfind(|l| !l.is_empty()).map(String::from)),
_ => Err(IdeError::unavailable(format!("cannot look up the latest {}", packages[0]))),
}
}
_ => Ok(None),
}
}
/// Removes a tool the IDE installed.
pub fn uninstall(&self, name: &str, langs: &[LanguageConfig]) -> IdeResult<ToolInfo> {
match registry::find(name) {
+23
View File
@@ -935,3 +935,26 @@ fn a_cancelled_install_leaves_the_previous_one_in_place() {
assert!(dest.join("new").exists() && !dest.join("old").exists());
assert!(aside.join("old").exists(), "the previous install is moved aside");
}
/// An npm tool the IDE installed is offered the version the user's npm registry has now.
#[cfg(unix)]
#[test]
fn npm_tools_report_a_newer_registry_version() {
let env = Env::new();
let sdk = env._tmp.path().join("node-sdk/bin");
let modules = env.root.join("node_modules");
env.script(&modules.join(".bin"), "bash-language-server", "echo 5.4.0");
fs::create_dir_all(modules.join("bash-language-server")).unwrap();
fs::write(modules.join("bash-language-server/package.json"), r#"{"version":"5.4.0"}"#).unwrap();
env.script(&sdk, "node", "exit 0");
let tools = |latest: &str| {
env.script(&sdk, "npm", &format!(r#"[ "$1 $2 $3" = "view bash-language-server version" ] && echo {latest}"#));
env.tools().with_runtime(BTreeMap::from([("node".into(), sdk.join("node"))]), BTreeMap::new())
};
let cancel = CancellationToken::new();
assert_eq!(
tools("5.6.1").updates(&cancel).unwrap(),
vec![ToolUpdate { name: "bash-language-server".into(), current: "5.4.0".into(), latest: "5.6.1".into() }]
);
assert_eq!(tools("5.4.0").updates(&cancel).unwrap(), vec![]);
}
+1 -1
View File
@@ -51,7 +51,7 @@ RSpec has its own configuration type (**RSpec**) and a detected `rspec` configur
A test run opens a tab in the **Run** tool window with the test tree next to the console. Failed suites expand automatically.
- Click a test to see its own output in the console.
- Click a test to see its own output in the console, or a class or suite to see its setup and teardown output followed by its tests'. **Show all output** above the console goes back to the whole run.
- Double-click, `Enter` or `F4` jumps to the test's source (**Jump to Source**).
- **Show Passed** and **Show Ignored** filter the tree. Passed tests are hidden by default; the filters are remembered.
- Right-click a test or a selection for **Run '…'**, **Debug '…'** and **Jump to Source**.
+1 -1
View File
@@ -47,7 +47,7 @@ RSpec 有自己的配置类型(**RSpec 测试**)和检测到的 `rspec` 配
测试运行会在 **运行** 工具窗口中打开一个标签页,控制台旁边是测试树。失败的测试套件会自动展开。
- 单击某个测试,控制台只显示它自己的输出。
- 单击某个测试,控制台只显示它自己的输出;单击类或套件,先显示它自己的准备和清理输出,再显示其下各个测试的输出。控制台上方的 **显示所有输出** 可回到整个运行的输出。
- 双击、按 `Enter` 或 `F4` 跳到测试源码(**跳转到源代码**)。
- **显示已通过** 和 **显示已忽略** 用于筛选测试树。默认隐藏已通过的测试,筛选状态会被记住。
- 右键单个测试或多选后,可以 **运行 '…'**、**调试 '…'** 或 **跳转到源代码**。
+84 -75
View File
@@ -430,6 +430,11 @@ matching SDK. Released 1.7 plugins keep working. New in 1.8:
annotations follow the file instead of the window that published them.
`set_decorations` accepts files outside the open project for the same reason,
and the `typo` / `weak` styles are no longer dropped.
- `decorations.set-file-icon` / `set-file-icons` (`host::set_file_icon`,
`set_file_icons`): give a file or plain folder one of the plugin's
`[[contributes.fileIcons]]` icons by path, for icons that depend on where a
file is or what it holds (a Helm chart folder, a Kustomize overlay). Such
icons may omit `patterns`. Requires `project.read`.
### API 1.7
@@ -595,80 +600,81 @@ then run `openidea-plugin check`.
### The API
| Rust SDK | WIT | Notes |
| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Plugin::run_command(id, ctx, arg)` | export `commands.run` | `ctx` is the window: `ctx.project()`, `ctx.editor()`. An error shows _"<title> failed: …"_; the plugin keeps running. `"msg".into()` is `Error::Failed`; `Cancelled` and `Denied` don't count as the plugin failing. |
| `Plugin::handle_events(events, ctx)` | export `event-handler.handle` | Batches of subscribed events, oldest first, per window. |
| `Plugin::activate` / `deactivate` | export `lifecycle` | Per instance: after a crash or idle reclaim a new one is activated. |
| `TextEditor::document()` → `{version, text}` | `editor.text-editor.document` | `selections()` are those when the command ran (0-based lines, UTF-16 columns). |
| `Project::document`, `apply_document_edits` | `editor.{document,apply-edits}-at` | API 1.6: versioned access to an explicitly named open project document from background events. Requires `project.read` and `editor.read`/`editor.write`; stale edits change nothing. |
| `WebSocket::{connect,send,drain,close}` | `net.websocket-*` | API 1.6: project-bound, bounded host-owned WSS transport with secret-filled headers, close state and event-driven wakeups. |
| `Project::{upsert_data_source,remove_data_source,data_sources,set_data_source_credential,connect_data_source}` | `database` | API 1.4: owned project sources; requires `database.sources` and trust. See _Database sources_. |
| `host::mcp_stdio_command()` | `mcp.stdio-command` | API 1.4: invoking project bridge argv, `None` while off. No URL/token. |
| `McpProvider::call(tool, arguments, ctx)` | export `mcp-provider.call` | API 1.4: declared tools under plugin grants and command budget. See _Plugin MCP tools_. |
| `host::open_editors()` | `editor.open-editors` | API 1.4: invoking window's path/language/dirty/active snapshots; requires `editor.read`. Commands/events of a window only. |
| `host::open_editor(path, selection)` | `editor.open` | API 1.4: open/select a project file (`project.read`) or plugin-dir file; same path rules as `views.open-location`. Commands/events of a window only. |
| `TextEditor::set_selections(ranges)`, `reveal(range)` | `editor.text-editor` | API 1.4: selections or scrolling in this window's already open editor; no capability. Commands/events only, UTF-16 coordinates. |
| `host::set_decorations`, `clear_decorations` | `decorations.{set,clear}` | API 1.4: project-scoped editor annotations cleared on plugin stop. Requires `project.read`. |
| `host::set_file_decoration`, `clear_file_decorations` | `decorations.{set-file,clear-file}` | API 1.4: project-tree badges/styles. Requires `project.read`. |
| `host::set_file_note` | `decorations.set-file-note` | API 1.7: a note after the name in the project tree (one line, at most 200 characters). Requires `project.read`. |
| `Project::search_text(query, limit)` | `workspace.project.search-text` | API 1.4: cancellable Find in Files with unsaved documents and project excludes, at most 10,000 matches. Requires `project.read`. |
| `host::problems(path)` | `diagnostics.problems` | API 1.4: project Problems reports grouped by file/source, optional file filter. Requires `project.read` and an invoking window. Empty for remote projects. |
| `Project::vcs_log(path, limit)` | `vcs.log` | API 1.4: newest first, at most 1000 commits, optional file history. Requires `vcs.read` and trust. |
| `Project::vcs_blame(path)` | `vcs.blame` | API 1.4: attribution including unsaved editor text. Requires `vcs.read` and trust. |
| `Project::vcs_diff(path, against)` | `vcs.diff` | API 1.4: disk working tree against HEAD/revision, optionally one file. None outside Git; fails above 4 MiB. Requires `vcs.read` and trust. |
| `TextEditor::apply_edits(version, edits)` | `editor.text-editor.apply-edits` | One undoable change, only if the document is still at `version` — else `Error::Stale` and nothing changes. `transform_selections` retries once. |
| `Project::root()`, `read_file(path)` | `workspace.project` | Absolute or project-relative; `..`/symlinks escaping the root are rejected; UTF-8 only, ≤ 16 MiB; unsaved editor contents win; a missing path is `Error::NotFound`. |
| `Project::read_bytes(path)` | `workspace.project.read-bytes` | API 1.5: the exact bytes of a file inside the project (binary content is the point), at most 16 MiB, the file on disk. Same paths and `Error::NotFound` as `read_file`. |
| `Project::exists(path)`, `stat(path)` | `workspace.project` | `exists` covers files and directories, hidden and ignored included, without reading content; `stat` gives kind, size and modified time (`Error::NotFound` for a missing path). |
| `host::projects()` | `workspace.projects` | Every open project (for scheduled work without a window). |
| `Project::vcs_status()`, `vcs_remotes()`, `vcs_fetch()` | `vcs` | `vcs_remotes` returns `(name, url)` pairs; fetch runs git, so only in trusted projects (call it inside a task). |
| `host::pending_commit()`, `host::set_commit_message(revision, text)` | `commit` | The commit being prepared in the calling window and its message; see _Commits_. |
| `host::subscribe(kind)` | `events.subscribe` | For the session, across idle reclaim (an event activates the plugin). Or declare `on-event:<kind>`. |
| `host::begin_task(title, cancellable)` → `Task` | `tasks.begin` | Shown with the background tasks; `report`, `is_cancelled`; the call may run 60 s instead of 5 s while it is open; dropping it ends it. |
| `host::storage::{get, set, delete, keys}` | `storage` | Survives restarts; the instance's memory does not. Deleted on uninstall. |
| `host::set_status_item(id, item)`, `remove_status_item` | `ui` | `text`, `tooltip`, `command` (one of yours, run on click); at most 8; kept while the plugin is idle. |
| `host::set_window_status_item(id, item)`, `remove_window_status_item(id)` | `ui.{set,remove}-window-status-item` | API 1.4: window-only override; removed on window close or plugin stop; removing falls back to the global item. Click runs its command in that window. |
| `host::execute_command(id, arg)` → `Option<String>` | `command-runner.execute` | API 1.4: requires high-risk `ide.commands` for IDE actions and other plugins; this plugin's own commands are exempt. Stable IDE action or `plugin.<plugin-id>.<command-id>` in the invoking window. IDE actions return none; plugin commands return their result. Unknown ids are not-found; at most 8 nested calls, no re-entry into a waiting instance. |
| `host::ipc_connect(name)` → `Connection` | `ipc.connect` | API 1.4: `write(bytes)`, `read(max_bytes, timeout_ms)`, `close()`; requires `ipc:<path>`, at most 8 connections, closed on drop or plugin stop. Runs on the project server in remote projects. |
| `host::secret_set(name, value)`, `secret_delete(name)` | `secrets.{set,delete}` | API 1.4: requires `secrets` and a declared name. Never returns the value. |
| `host::begin_loopback(path)` → `Loopback` | `auth.begin-loopback` | API 1.4: `redirect_uri()`, `wait(timeout_ms)` → raw query or none on timeout. Desktop-only, even for remote projects; no capability. At most 2 listeners; one-shot, closed on timeout, drop or stop. Waiting excludes the call budget. |
| `host::output_append(channel, text)`, `output_clear`, `output_reveal` | `output.{append,clear,reveal}` | API 1.4: read-only Output tabs; 32 named channels per plugin, each a 1 MiB UTF-8 ring buffer. Reveal requires the invoking window. |
| `host::set_context(key, value)`, `remove_context` | `ui.{set,remove}-context` | Sets `plugin.<id>.<key>` for `when` clauses in the window the call came from (`true`, `3.5`, `"text"`; `ContextValue`). Key 1–64 of `A-Za-z0-9_-`, 100 per plugin; gone when the plugin stops. |
| `host::notify(level, title, body)` | `ui.show-notification` | Balloon in the window the command or events came from (every window otherwise). |
| `Project::notify(level, title, body, actions)` | `ui.notify-project` | API 1.5: balloon only in a window of that project (the most recently focused one), with or without an invoking window; see _Notifications with commands_. |
| `host::copy_to_clipboard(text)` | `ui.copy-to-clipboard` | Copies `text` to the system clipboard. Needs `clipboard`. |
| `host::open_url(url)` | `ui.open-url` | Opens an `http://` or `https://` URL in the user's default browser. |
| `host::pick(title, items)`, `pick_many` | `ui.pick`, `ui.pick-many` | Picks key(s) from `PickItem`s, or `None` when dismissed. From a command/event or code-action resolution in its editor window (else `invalid`); waiting is excluded from the budget. |
| `host::input(prompt, value)` | `ui.input` | A one-line text box pre-filled with `value`; `None` when dismissed. Same rules as `pick`. |
| `host::input_multiline(prompt, value)` | `ui.input-multiline` | A multi-line text box pre-filled with `value`; `None` when dismissed; Cmd/Ctrl+Enter confirms. Only from a command or event of a window. |
| `host::confirm(title, body)` | `ui.confirm` | OK / Cancel; `true` on OK. Same rules as `pick`. |
| `host::locale()` | `ui.locale` | The IDE's UI language as a BCP 47 tag (`en`, `fr`, `zh-CN`); no capability. See _Translations_. |
| `host::log(level, msg)` | `log` | IDE log, tagged with the plugin id. `eprintln!` output is logged too and shown when the plugin crashes. |
| `Project::find_files(glob, limit)` | `workspace.project.find-files` | Project-relative glob (`**/*Mapper.xml`); honours `.gitignore`; at most `limit` paths. |
| `Project::symbols(query, limit)` | `workspace.symbols` | New in 1.3; `project.read`. Fuzzy in-memory search (no LSP): name, camelCase kind, container and UTF-16 location. Queries ≤ 256 UTF-8 bytes; limits capped at 1000. Snapshot notes below. |
| `host::publish_diagnostics(path, items)` | `diagnostics.publish` | Replaces this plugin's problems for the file (editor squiggles and the Problems view); an empty list clears them. |
| `host::fetch(request)` → `{status, headers, body}` | `net.fetch` | See _Network_. |
| `Project::run_process(program, args, stdin)` → `{code, stdout, stderr}` | `process.run` | Only the pinned program of a `process:` grant; runs in the project root of a trusted project; up to 16 MiB of stdout (fails if exceeded); killed when the call is cancelled or times out. |
| `host::run_command(cmd, project, stdin)` → `{code, stdout, stderr}` | `process.run-command` | API 1.3: same limits as `run_process`, with append-only environment variables; needs the pinned `process:<program>` grant. |
| `host::start(cmd, project)` → `Child` | `process.start` | API 1.3: `poll(max_bytes)`, `write(text)`, `close_stdin()`, `wait(timeout_ms)`, `kill()`; at most 8 handles; dropping one kills its process. |
| `host::refresh_view(view)` | `views.refresh` | The view (or custom editors) render again; call it when the content changed. |
| `host::reveal_view(view)` | `views.reveal` | Brings one of your declared `[[contributes.views]]` to the front in the calling window. |
| `host::post_view_message(view, text)` | `views.post-message` | To the script of the view's `html` body. |
| `host::open_terminal(title, program, args)` | `views.open-terminal` | A terminal tab in the invoking window running the approved `process:<program>` in the project root: output that keeps coming, interactive sessions. |
| `host::register_run_target(project, target)` | `run-targets.register` | API 1.4: project-scoped, read-only argv wrapper; requires its process grant, executable pin and project trust; remote projects run on the server. |
| `host::unregister_run_target(project, id)` | `run-targets.unregister` | Withdraw this plugin's target; remembered prompt answers remain. |
| `host::start_run(config, args, debug)` | `run.start` | API 1.4: start an owned configuration in this window's Run tool window; returns an id, followed by `run-exited`. Approved `process:` program; commands/events only. |
| `host::open_terminal_session(key, title, cmd)` | `views.open-terminal-session` | API 1.4: keyed, pinned terminal in this window, append-only env; returns its operation id and later `terminal-exited`. |
| `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. |
| `host::open_location(path, line, column)` | `views.open-location` | Opens a file inside the project in an editor of the invoking window, the caret at `line`/`column` (0-based, UTF-16): a view's links to code. Needs `project.read`. |
| `host::setting(key)`, `setting_bool`, `setting_string`, `setting_number` | `settings.get` | The user's value (project over global) or the schema's default. |
| Rust SDK | WIT | Notes |
| -------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Plugin::run_command(id, ctx, arg)` | export `commands.run` | `ctx` is the window: `ctx.project()`, `ctx.editor()`. An error shows _"<title> failed: …"_; the plugin keeps running. `"msg".into()` is `Error::Failed`; `Cancelled` and `Denied` don't count as the plugin failing. |
| `Plugin::handle_events(events, ctx)` | export `event-handler.handle` | Batches of subscribed events, oldest first, per window. |
| `Plugin::activate` / `deactivate` | export `lifecycle` | Per instance: after a crash or idle reclaim a new one is activated. |
| `TextEditor::document()` → `{version, text}` | `editor.text-editor.document` | `selections()` are those when the command ran (0-based lines, UTF-16 columns). |
| `Project::document`, `apply_document_edits` | `editor.{document,apply-edits}-at` | API 1.6: versioned access to an explicitly named open project document from background events. Requires `project.read` and `editor.read`/`editor.write`; stale edits change nothing. |
| `WebSocket::{connect,send,drain,close}` | `net.websocket-*` | API 1.6: project-bound, bounded host-owned WSS transport with secret-filled headers, close state and event-driven wakeups. |
| `Project::{upsert_data_source,remove_data_source,data_sources,set_data_source_credential,connect_data_source}` | `database` | API 1.4: owned project sources; requires `database.sources` and trust. See _Database sources_. |
| `host::mcp_stdio_command()` | `mcp.stdio-command` | API 1.4: invoking project bridge argv, `None` while off. No URL/token. |
| `McpProvider::call(tool, arguments, ctx)` | export `mcp-provider.call` | API 1.4: declared tools under plugin grants and command budget. See _Plugin MCP tools_. |
| `host::open_editors()` | `editor.open-editors` | API 1.4: invoking window's path/language/dirty/active snapshots; requires `editor.read`. Commands/events of a window only. |
| `host::open_editor(path, selection)` | `editor.open` | API 1.4: open/select a project file (`project.read`) or plugin-dir file; same path rules as `views.open-location`. Commands/events of a window only. |
| `TextEditor::set_selections(ranges)`, `reveal(range)` | `editor.text-editor` | API 1.4: selections or scrolling in this window's already open editor; no capability. Commands/events only, UTF-16 coordinates. |
| `host::set_decorations`, `clear_decorations` | `decorations.{set,clear}` | API 1.4: project-scoped editor annotations cleared on plugin stop. Requires `project.read`. |
| `host::set_file_decoration`, `clear_file_decorations` | `decorations.{set-file,clear-file}` | API 1.4: project-tree badges/styles. Requires `project.read`. |
| `host::set_file_note` | `decorations.set-file-note` | API 1.7: a note after the name in the project tree (one line, at most 200 characters). Requires `project.read`. |
| `host::set_file_icon`, `set_file_icons` | `decorations.{set-file-icon,set-file-icons}` | API 1.8: assign one of the plugin's declared `[[contributes.fileIcons]]` icons to a file or plain folder by path (individually or up to 10,000 entries per set). Requires `project.read`. |
| `Project::search_text(query, limit)` | `workspace.project.search-text` | API 1.4: cancellable Find in Files with unsaved documents and project excludes, at most 10,000 matches. Requires `project.read`. |
| `host::problems(path)` | `diagnostics.problems` | API 1.4: project Problems reports grouped by file/source, optional file filter. Requires `project.read` and an invoking window. Empty for remote projects. |
| `Project::vcs_log(path, limit)` | `vcs.log` | API 1.4: newest first, at most 1000 commits, optional file history. Requires `vcs.read` and trust. |
| `Project::vcs_blame(path)` | `vcs.blame` | API 1.4: attribution including unsaved editor text. Requires `vcs.read` and trust. |
| `Project::vcs_diff(path, against)` | `vcs.diff` | API 1.4: disk working tree against HEAD/revision, optionally one file. None outside Git; fails above 4 MiB. Requires `vcs.read` and trust. |
| `TextEditor::apply_edits(version, edits)` | `editor.text-editor.apply-edits` | One undoable change, only if the document is still at `version` — else `Error::Stale` and nothing changes. `transform_selections` retries once. |
| `Project::root()`, `read_file(path)` | `workspace.project` | Absolute or project-relative; `..`/symlinks escaping the root are rejected; UTF-8 only, ≤ 16 MiB; unsaved editor contents win; a missing path is `Error::NotFound`. |
| `Project::read_bytes(path)` | `workspace.project.read-bytes` | API 1.5: the exact bytes of a file inside the project (binary content is the point), at most 16 MiB, the file on disk. Same paths and `Error::NotFound` as `read_file`. |
| `Project::exists(path)`, `stat(path)` | `workspace.project` | `exists` covers files and directories, hidden and ignored included, without reading content; `stat` gives kind, size and modified time (`Error::NotFound` for a missing path). |
| `host::projects()` | `workspace.projects` | Every open project (for scheduled work without a window). |
| `Project::vcs_status()`, `vcs_remotes()`, `vcs_fetch()` | `vcs` | `vcs_remotes` returns `(name, url)` pairs; fetch runs git, so only in trusted projects (call it inside a task). |
| `host::pending_commit()`, `host::set_commit_message(revision, text)` | `commit` | The commit being prepared in the calling window and its message; see _Commits_. |
| `host::subscribe(kind)` | `events.subscribe` | For the session, across idle reclaim (an event activates the plugin). Or declare `on-event:<kind>`. |
| `host::begin_task(title, cancellable)` → `Task` | `tasks.begin` | Shown with the background tasks; `report`, `is_cancelled`; the call may run 60 s instead of 5 s while it is open; dropping it ends it. |
| `host::storage::{get, set, delete, keys}` | `storage` | Survives restarts; the instance's memory does not. Deleted on uninstall. |
| `host::set_status_item(id, item)`, `remove_status_item` | `ui` | `text`, `tooltip`, `command` (one of yours, run on click); at most 8; kept while the plugin is idle. |
| `host::set_window_status_item(id, item)`, `remove_window_status_item(id)` | `ui.{set,remove}-window-status-item` | API 1.4: window-only override; removed on window close or plugin stop; removing falls back to the global item. Click runs its command in that window. |
| `host::execute_command(id, arg)` → `Option<String>` | `command-runner.execute` | API 1.4: requires high-risk `ide.commands` for IDE actions and other plugins; this plugin's own commands are exempt. Stable IDE action or `plugin.<plugin-id>.<command-id>` in the invoking window. IDE actions return none; plugin commands return their result. Unknown ids are not-found; at most 8 nested calls, no re-entry into a waiting instance. |
| `host::ipc_connect(name)` → `Connection` | `ipc.connect` | API 1.4: `write(bytes)`, `read(max_bytes, timeout_ms)`, `close()`; requires `ipc:<path>`, at most 8 connections, closed on drop or plugin stop. Runs on the project server in remote projects. |
| `host::secret_set(name, value)`, `secret_delete(name)` | `secrets.{set,delete}` | API 1.4: requires `secrets` and a declared name. Never returns the value. |
| `host::begin_loopback(path)` → `Loopback` | `auth.begin-loopback` | API 1.4: `redirect_uri()`, `wait(timeout_ms)` → raw query or none on timeout. Desktop-only, even for remote projects; no capability. At most 2 listeners; one-shot, closed on timeout, drop or stop. Waiting excludes the call budget. |
| `host::output_append(channel, text)`, `output_clear`, `output_reveal` | `output.{append,clear,reveal}` | API 1.4: read-only Output tabs; 32 named channels per plugin, each a 1 MiB UTF-8 ring buffer. Reveal requires the invoking window. |
| `host::set_context(key, value)`, `remove_context` | `ui.{set,remove}-context` | Sets `plugin.<id>.<key>` for `when` clauses in the window the call came from (`true`, `3.5`, `"text"`; `ContextValue`). Key 1–64 of `A-Za-z0-9_-`, 100 per plugin; gone when the plugin stops. |
| `host::notify(level, title, body)` | `ui.show-notification` | Balloon in the window the command or events came from (every window otherwise). |
| `Project::notify(level, title, body, actions)` | `ui.notify-project` | API 1.5: balloon only in a window of that project (the most recently focused one), with or without an invoking window; see _Notifications with commands_. |
| `host::copy_to_clipboard(text)` | `ui.copy-to-clipboard` | Copies `text` to the system clipboard. Needs `clipboard`. |
| `host::open_url(url)` | `ui.open-url` | Opens an `http://` or `https://` URL in the user's default browser. |
| `host::pick(title, items)`, `pick_many` | `ui.pick`, `ui.pick-many` | Picks key(s) from `PickItem`s, or `None` when dismissed. From a command/event or code-action resolution in its editor window (else `invalid`); waiting is excluded from the budget. |
| `host::input(prompt, value)` | `ui.input` | A one-line text box pre-filled with `value`; `None` when dismissed. Same rules as `pick`. |
| `host::input_multiline(prompt, value)` | `ui.input-multiline` | A multi-line text box pre-filled with `value`; `None` when dismissed; Cmd/Ctrl+Enter confirms. Only from a command or event of a window. |
| `host::confirm(title, body)` | `ui.confirm` | OK / Cancel; `true` on OK. Same rules as `pick`. |
| `host::locale()` | `ui.locale` | The IDE's UI language as a BCP 47 tag (`en`, `fr`, `zh-CN`); no capability. See _Translations_. |
| `host::log(level, msg)` | `log` | IDE log, tagged with the plugin id. `eprintln!` output is logged too and shown when the plugin crashes. |
| `Project::find_files(glob, limit)` | `workspace.project.find-files` | Project-relative glob (`**/*Mapper.xml`); honours `.gitignore`; at most `limit` paths. |
| `Project::symbols(query, limit)` | `workspace.symbols` | New in 1.3; `project.read`. Fuzzy in-memory search (no LSP): name, camelCase kind, container and UTF-16 location. Queries ≤ 256 UTF-8 bytes; limits capped at 1000. Snapshot notes below. |
| `host::publish_diagnostics(path, items)` | `diagnostics.publish` | Replaces this plugin's problems for the file (editor squiggles and the Problems view); an empty list clears them. |
| `host::fetch(request)` → `{status, headers, body}` | `net.fetch` | See _Network_. |
| `Project::run_process(program, args, stdin)` → `{code, stdout, stderr}` | `process.run` | Only the pinned program of a `process:` grant; runs in the project root of a trusted project; up to 16 MiB of stdout (fails if exceeded); killed when the call is cancelled or times out. |
| `host::run_command(cmd, project, stdin)` → `{code, stdout, stderr}` | `process.run-command` | API 1.3: same limits as `run_process`, with append-only environment variables; needs the pinned `process:<program>` grant. |
| `host::start(cmd, project)` → `Child` | `process.start` | API 1.3: `poll(max_bytes)`, `write(text)`, `close_stdin()`, `wait(timeout_ms)`, `kill()`; at most 8 handles; dropping one kills its process. |
| `host::refresh_view(view)` | `views.refresh` | The view (or custom editors) render again; call it when the content changed. |
| `host::reveal_view(view)` | `views.reveal` | Brings one of your declared `[[contributes.views]]` to the front in the calling window. |
| `host::post_view_message(view, text)` | `views.post-message` | To the script of the view's `html` body. |
| `host::open_terminal(title, program, args)` | `views.open-terminal` | A terminal tab in the invoking window running the approved `process:<program>` in the project root: output that keeps coming, interactive sessions. |
| `host::register_run_target(project, target)` | `run-targets.register` | API 1.4: project-scoped, read-only argv wrapper; requires its process grant, executable pin and project trust; remote projects run on the server. |
| `host::unregister_run_target(project, id)` | `run-targets.unregister` | Withdraw this plugin's target; remembered prompt answers remain. |
| `host::start_run(config, args, debug)` | `run.start` | API 1.4: start an owned configuration in this window's Run tool window; returns an id, followed by `run-exited`. Approved `process:` program; commands/events only. |
| `host::open_terminal_session(key, title, cmd)` | `views.open-terminal-session` | API 1.4: keyed, pinned terminal in this window, append-only env; returns its operation id and later `terminal-exited`. |
| `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. |
| `host::open_location(path, line, column)` | `views.open-location` | Opens a file inside the project in an editor of the invoking window, the caret at `line`/`column` (0-based, UTF-16): a view's links to code. Needs `project.read`. |
| `host::setting(key)`, `setting_bool`, `setting_string`, `setting_number` | `settings.get` | The user's value (project over global) or the schema's default. |
`Project::symbols` reads the current in-memory index: results may be partial
while indexing, and locations can lag unsaved edits. Normal saves and filesystem
@@ -867,7 +873,10 @@ Decorations are plain text, with fixed styles mapped to theme tokens:
have `{ range, style, hover, after }`; tree items have `{ badge, style, tooltip }`,
where a badge has at most two Unicode characters. A tree item can instead be a note
(`set-file-note`, API 1.7): muted text after the file or folder name, one line of at
most 200 characters, with an optional tooltip; it replaces that set's item for the file.
most 200 characters, with an optional tooltip; or a dynamic icon (`set-file-icon` /
`set-file-icons`, API 1.8): one of the plugin's own `[[contributes.fileIcons]]` ids
(`patterns` may be omitted under `engines.openidea = "^1.8"`), applied to a file or
plain folder by exact path; each replaces that set's item for the path.
Paths must resolve inside an open project; absolute paths select that project, relative paths use the invoking
project. Sets are isolated by plugin and project and shared by all windows of
that project. Setting replaces a set's items for one file; clearing removes that
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
ec93db9b37437986167b4040714f5cda5cb7a2fc138e6c2dc2a9447faaf5a802
56008cb5a36b35b8975b95da971212c95c165de378b0c8b7f7b428d9d9332ec1
Binary file not shown.
@@ -1 +1 @@
78465a96e1e34ef3d4cf658c3b80f8388e43604dfe5abbf105f1149d040c5994
d87e571ebe16283bbffcc7525f69755d5faffb4693ff4a5183de67418976086b
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
1bd90bca250bb06ab3b0f27b045110c722685d894ce79d23648cb7c795d2c033
254da92cdb20bdf432b5bb4e90ef9bdfaff92eca7195fbff38127f47fa231e6e
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
173a8b66a3d5969bc51b2c97fcb435e2313555608aecbe1752d4586e3508d74f
50ea437aa2b99dc28afd51286fbac7e07d8386f826f222c517a8bd6e4f647a1a
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
c4c86cef9512ac03193e7d89d39b6ee41665c2d4721a78f9415d406289e9ece1
28584de67687b8e34376cd391a020d4c246fd98a3b904e15f413b56a650b5f06
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
6ddc2f9166452e4473cdaa9974943cb85ad4575457d2b3ca3d35abea65cb8b30
335fa8a14e0d517ef1f735b02c49d8d902a57895c862ca8240854bb758796928
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
e97394224db64bcefb5ac43eba55eaba38f9d33122241313fae311ef8f0957db
d5ddd6d4b5dc7983945163188c31a844cbcbbdfda3fb893afc5a3acca2e012bb
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
ca7266484d34f16de46b97e6317135984fdc4141099e50bdd52294617c2af9c0
cdb81586fc7bd1043659a70b73241fa3111b9c86742029f018ca3a362cc3e9c2
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
0d7731de73514b950d8579762029d98c46136f55bfdb2fa44e256cc9a9955a0f
de843867603a4d271af1b7159c3ce927236d1ab7575c5e8f12278b97910c8611
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
959cb2ab750211bd1cc894a4d804acb2dd16e95c1e65b267e7eccbb64cbdd8da
9a79d41299571be7165a264b6733ae9974ef76af928d51097ac85238940cfd9c
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
d5ec377869ded4ec0bf533d07d51610aec7a6813d43ead98a261b3f54e7d0655
bb6539e78a31a830edd6c59286c0f01d9b95418a20c8f492c962739e9b5f414a
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
ebbd24e08c19953895464567cbaf1335b9847e98e8a8da8a4e0b8c2886c1abd6
84fce12270a15e21be616d264bd9dd6d515bffd38ec37e992232843c80e7a439
Loaded 100 of 147 files, more files were not shown because too many files have changed in this diff. Show more