diff --git a/crates/ide-host/src/plugins/api.rs b/crates/ide-host/src/plugins/api.rs
index 375ab182..a09e61a1 100644
--- a/crates/ide-host/src/plugins/api.rs
+++ b/crates/ide-host/src/plugins/api.rs
@@ -2,6 +2,7 @@
//! Capabilities were checked by the runtime before any of this runs.
use super::{PluginEvent, PluginEventSink};
+use ide_core::ipc_path::to_ipc;
use ide_core::{CancellationToken, TextEdit};
use ide_extension::Package;
use ide_language::ProgramPin;
@@ -419,7 +420,7 @@ impl HostApi for PluginApi {
let info = repo.branch_info().map_err(failed)?;
let changed = repo.status().map_err(failed)?.len();
Ok(Some(RepositoryStatus {
- root: repo.root().to_string_lossy().into_owned(),
+ root: to_ipc(repo.root()),
branch: info.head,
commit: info.head_commit,
upstream: info.upstream,
@@ -467,7 +468,7 @@ impl HostApi for PluginApi {
}
let Ok(rel) = entry.path().strip_prefix(root) else { continue };
if matcher.is_match(rel) {
- out.push(entry.path().to_string_lossy().into_owned());
+ out.push(to_ipc(entry.path()));
}
}
out.sort();
@@ -806,6 +807,19 @@ mod tests {
PluginApi { pkg: Package::new(manifest, dir.to_path_buf()), shared }
}
+ #[test]
+ fn repository_and_search_paths_match_ipc_events() {
+ let tmp = tempfile::tempdir().unwrap();
+ assert!(std::process::Command::new("git").args(["init", "-q"]).arg(tmp.path()).status().unwrap().success());
+ let root = tmp.path().canonicalize().unwrap();
+ std::fs::create_dir(root.join("src")).unwrap();
+ let file = root.join("src/Example.java");
+ std::fs::write(&file, "class Example {}\n").unwrap();
+ let api = api(&root, Arc::default());
+ assert_eq!(api.find_files(&root, "**/*.java", 8).unwrap(), [to_ipc(&file)]);
+ assert_eq!(api.vcs_status(&root).unwrap().unwrap().root, to_ipc(&root));
+ }
+
#[test]
fn programs_run_only_as_approved() {
let tmp = tempfile::tempdir().unwrap();
diff --git a/crates/ide-host/tests/plugins.rs b/crates/ide-host/tests/plugins.rs
index 575584f0..ee5db393 100644
--- a/crates/ide-host/tests/plugins.rs
+++ b/crates/ide-host/tests/plugins.rs
@@ -6,6 +6,7 @@
//! `plugins/build.sh
` after changing their source or `wit/`.
use ide_core::error::ErrorKind;
+use ide_core::ipc_path::to_ipc;
use ide_host::plugins::*;
use ide_plugin::{EditorContext, HostError, Invocation, Limits};
use parking_lot::Mutex;
@@ -214,7 +215,7 @@ fn fixture(limits: Limits) -> Fixture {
std::fs::create_dir_all(project.join("src")).unwrap();
std::fs::write(project.join("src/notes.txt"), "one two three\nfour five\n").unwrap();
std::fs::write(tmp.path().join("secret.txt"), "top secret").unwrap();
- let project = project.canonicalize().unwrap();
+ let project = dunce::canonicalize(project).unwrap();
let events: Events = Arc::default();
let sink = {
let events = events.clone();
@@ -359,7 +360,7 @@ fn read_file_is_confined_to_the_project() {
let inv = Invocation { client: None, workspace_root: None, editor: None };
let err = f.host.run_command("misbehave", "read", Some("src/notes.txt"), inv).unwrap_err().message;
assert!(err.contains("no project is open"), "{err}");
- assert_eq!(f.run("misbehave", "root", None).unwrap().unwrap(), f.project.to_string_lossy());
+ assert_eq!(f.run("misbehave", "root", None).unwrap().unwrap(), to_ipc(&f.project));
assert_eq!(f.status("misbehave"), PluginStatus::Active);
}
@@ -1740,7 +1741,7 @@ fn official_checkstyle_checks_on_save() {
let java = f.project.join("src/A.java");
std::fs::write(&java, "class A {\n\tint x;\n}\n").unwrap();
let saved = |f: &Fixture| {
- f.host.post_events(Some("main"), vec![WorkbenchEvent::FileSaved(java.to_string_lossy().into_owned())]);
+ f.host.post_events(Some("main"), vec![WorkbenchEvent::FileSaved(to_ipc(&java))]);
f.host.flush_events_at(Instant::now() + Duration::from_secs(3));
};
saved(&f);
@@ -1793,7 +1794,7 @@ fn official_license_header_reports_fixes_and_scans() {
f.workspace.diagnostics.lock().get(&("openidea.license-header".into(), path.to_path_buf())).cloned()
};
- f.host.post_events(Some("main"), vec![WorkbenchEvent::FileOpened(a.to_string_lossy().into_owned())]);
+ f.host.post_events(Some("main"), vec![WorkbenchEvent::FileOpened(to_ipc(&a))]);
f.host.flush_events_at(Instant::now() + Duration::from_secs(3));
assert!(f.host.wait_events(Duration::from_secs(20)));
let d = diagnostics(&a).expect("published on open");
@@ -1818,7 +1819,7 @@ fn official_license_header_reports_fixes_and_scans() {
f.host.run_command("openidea.license-header", "scan", None, f.inv()).unwrap();
assert_eq!(diagnostics(&b).map(|d| d.len()), Some(1), "found by the scan");
std::fs::write(&a, "// Copyright (c) 2026 X\nfn a() {}\n").unwrap();
- f.host.post_events(Some("main"), vec![WorkbenchEvent::FileSaved(a.to_string_lossy().into_owned())]);
+ f.host.post_events(Some("main"), vec![WorkbenchEvent::FileSaved(to_ipc(&a))]);
f.host.flush_events_at(Instant::now() + Duration::from_secs(3));
assert!(f.host.wait_events(Duration::from_secs(20)));
assert_eq!(diagnostics(&a).map(|d| d.len()), Some(0), "cleared once the header is there");
diff --git a/crates/ide-plugin/src/runtime.rs b/crates/ide-plugin/src/runtime.rs
index 19d97abb..e7fe2555 100644
--- a/crates/ide-plugin/src/runtime.rs
+++ b/crates/ide-plugin/src/runtime.rs
@@ -13,6 +13,7 @@ use crate::types::{
};
use crate::wit_host::{bindings, providers_bindings, syntax_bindings, views_bindings};
use ide_core::CancellationToken;
+use ide_core::ipc_path::to_ipc;
use ide_extension::Capability;
use parking_lot::Mutex;
use std::path::Path;
@@ -935,12 +936,7 @@ fn location_from_wit(l: wit_types::Location) -> Location {
}
fn doc_to_wit(d: &ProviderDocument) -> wit_prov::Document {
- wit_prov::Document {
- path: d.path.to_string_lossy().into_owned(),
- language: d.language.clone(),
- version: d.version,
- text: d.text.clone(),
- }
+ wit_prov::Document { path: to_ipc(&d.path), language: d.language.clone(), version: d.version, text: d.text.clone() }
}
/// Resets the budgets for the next call: fuel, deadline, cancellation.
@@ -1012,7 +1008,7 @@ fn code_action_to_wit(a: &CodeAction) -> wit_prov::CodeAction {
.edits
.iter()
.map(|(path, edits)| wit_prov::FileEdit {
- path: path.to_string_lossy().into_owned(),
+ path: to_ipc(path),
edits: edits
.iter()
.map(|e| wit_types::TextEdit { range: range_to_wit(e.range), text: e.new_text.clone() })
@@ -1131,6 +1127,27 @@ fn event_to_wit_view(e: ViewEvent) -> wit_vt::ViewEvent {
mod tests {
use super::*;
+ #[cfg(windows)]
+ #[test]
+ fn provider_and_resolved_action_paths_match_ipc_events() {
+ for (path, expected) in [
+ (r"C:\project\Example.java", "C:/project/Example.java"),
+ (r"\\?\C:\project\Example.java", "C:/project/Example.java"),
+ (r"\\server\share\Example.java", "//server/share/Example.java"),
+ ] {
+ let doc = ProviderDocument { path: path.into(), ..Default::default() };
+ assert_eq!(doc_to_wit(&doc).path, expected);
+ let action = CodeAction {
+ title: "Fix".into(),
+ kind: None,
+ preferred: false,
+ edits: vec![(path.into(), Vec::new())],
+ data: None,
+ };
+ assert_eq!(code_action_to_wit(&action).edits[0].path, expected);
+ }
+ }
+
fn ext(kind: &str) -> wit_types::Extension {
wit_types::Extension { kind: kind.into(), data: "{}".into() }
}
diff --git a/crates/ide-plugin/src/wit_host.rs b/crates/ide-plugin/src/wit_host.rs
index b274a088..a1f7b753 100644
--- a/crates/ide-plugin/src/wit_host.rs
+++ b/crates/ide-plugin/src/wit_host.rs
@@ -5,6 +5,7 @@
use crate::runtime::{StoreData, clip};
use crate::sandbox;
use crate::types::{EditorContext, EventKind, HostError, HttpRequest, NotificationLevel, StatusItem, net_host};
+use ide_core::ipc_path::to_ipc;
use ide_extension::Capability;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
@@ -310,7 +311,8 @@ impl workspace::HostProject for StoreData {
if let Err(e) = self.require(Capability::ProjectRead) {
return Ok(Err(e));
}
- Ok(Ok(self.table.get(&this)?.root.to_string_lossy().into_owned()))
+ // Paths reach plugins in IPC form (`/` on Windows), as the UI's event paths do, so they compare as text.
+ Ok(Ok(to_ipc(&self.table.get(&this)?.root)))
}
fn read_file(&mut self, this: Resource, path: String) -> Fallible {
@@ -346,7 +348,7 @@ impl editor::Host for StoreData {}
impl editor::HostTextEditor for StoreData {
fn path(&mut self, this: Resource) -> wasmtime::Result {
- Ok(self.table.get(&this)?.ctx.path.to_string_lossy().into_owned())
+ Ok(to_ipc(&self.table.get(&this)?.ctx.path))
}
fn language(&mut self, this: Resource) -> wasmtime::Result {
diff --git a/plugins/official/checkstyle/Cargo.lock b/plugins/official/checkstyle/Cargo.lock
index 5c3d4f38..bc79d791 100644
--- a/plugins/official/checkstyle/Cargo.lock
+++ b/plugins/official/checkstyle/Cargo.lock
@@ -103,7 +103,7 @@ dependencies = [
[[package]]
name = "openidea-plugin-openidea-checkstyle"
-version = "0.1.0"
+version = "0.1.1"
dependencies = [
"openidea-plugin",
]
diff --git a/plugins/official/checkstyle/Cargo.toml b/plugins/official/checkstyle/Cargo.toml
index 2a04ad51..0ffe0337 100644
--- a/plugins/official/checkstyle/Cargo.toml
+++ b/plugins/official/checkstyle/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "openidea-plugin-openidea-checkstyle"
-version = "0.1.0"
+version = "0.1.1"
edition = "2024"
publish = false
diff --git a/plugins/official/checkstyle/dist/plugin.wasm b/plugins/official/checkstyle/dist/plugin.wasm
index e6c41ba8..0c5b399d 100644
Binary files a/plugins/official/checkstyle/dist/plugin.wasm and b/plugins/official/checkstyle/dist/plugin.wasm differ
diff --git a/plugins/official/checkstyle/dist/plugin.wasm.sources b/plugins/official/checkstyle/dist/plugin.wasm.sources
index 9d7525f7..d07b95aa 100644
--- a/plugins/official/checkstyle/dist/plugin.wasm.sources
+++ b/plugins/official/checkstyle/dist/plugin.wasm.sources
@@ -1 +1 @@
-2f4608630c5e41453458ecee5bc4e2f1994a680d4307a5f002f9dd5ccc04d457
+69273e1ddfd2fff9f49c11d4fbdc60a771f8e3befe440e1623c2ab97469aa250
diff --git a/plugins/official/checkstyle/plugin.toml b/plugins/official/checkstyle/plugin.toml
index 1732ad30..9c83c2b1 100644
--- a/plugins/official/checkstyle/plugin.toml
+++ b/plugins/official/checkstyle/plugin.toml
@@ -1,6 +1,6 @@
id = "openidea.checkstyle"
name = "Checkstyle"
-version = "0.1.0"
+version = "0.1.1"
description = "Checks Java files with Checkstyle when you save them: problems in the editor and the Problems view, quick fixes for tabs, trailing spaces and missing final newlines."
authors = ["OpenIDEA contributors"]
license = "Apache-2.0"
diff --git a/plugins/official/checkstyle/src/lib.rs b/plugins/official/checkstyle/src/lib.rs
index aebf998e..6159e7ce 100644
--- a/plugins/official/checkstyle/src/lib.rs
+++ b/plugins/official/checkstyle/src/lib.rs
@@ -110,6 +110,26 @@ fn project_of(path: &str) -> Option {
.max_by_key(|p| p.root().map(|r| r.len()).unwrap_or(0))
}
+fn windows_path(path: &str) -> String {
+ let path = path.replace('\\', "/");
+ if let Some(rest) = path.strip_prefix("//?/UNC/") {
+ format!("//{rest}")
+ } else {
+ path.strip_prefix("//?/").unwrap_or(&path).to_owned()
+ }
+}
+
+fn report_matches_file(reported: &str, path: &str) -> bool {
+ // WASI plugins run on every host, so interpret the host's path rather than cfg(windows).
+ let drive = path.as_bytes().get(1) == Some(&b':') && path.as_bytes()[0].is_ascii_alphabetic();
+ if drive || path.starts_with("//") || path.starts_with(r"\\") {
+ let (reported, path) = (windows_path(reported), windows_path(path));
+ reported == path || path.ends_with(&reported)
+ } else {
+ reported == path || path.ends_with(reported)
+ }
+}
+
/// The configuration for files of `project`.
fn config_for(project: &Project) -> String {
PROJECT_CONFIGS
@@ -138,11 +158,8 @@ impl Checkstyle {
return Err(Error::Failed(format!("checkstyle: {}", out.stderr.lines().next().unwrap_or("no report"))));
}
let report = parse_report(&out.stdout);
- let problems = report
- .into_iter()
- .find(|(f, _)| f == path || path.ends_with(f.as_str()))
- .map(|(_, p)| p)
- .unwrap_or_default();
+ let problems =
+ report.into_iter().find(|(f, _)| report_matches_file(f, path)).map(|(_, p)| p).unwrap_or_default();
let text = project.read_file(path).unwrap_or_default();
let diagnostics: Vec = problems.iter().map(|p| diagnostic(p, &text)).collect();
host::publish_diagnostics(path, &diagnostics)?;
@@ -233,7 +250,7 @@ impl Plugin for Checkstyle {
register_plugin!(Checkstyle, providers);
-#[cfg(test)]
+#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
use openidea_plugin::ProcessOutput;
@@ -288,6 +305,45 @@ mod tests {
assert_eq!(actions[1].edits[0].1[0].text, " void f() {}");
}
+ #[test]
+ fn unix_report_paths_preserve_backslashes() {
+ assert!(report_matches_file("/project/A\\B.java", "/project/A\\B.java"));
+ assert!(!report_matches_file("/project/A\\B.java", "/project/A/B.java"));
+ }
+
+ #[test]
+ fn windows_reports_match_the_saved_file_and_keep_quick_fixes() {
+ for (root, reported) in [
+ ("C:/project", r"C:\project\src\A.java"),
+ ("C:/project", r"\\?\C:\project\src\A.java"),
+ ("C:/project", r"src\A.java"),
+ ("//server/share/project", r"\\server\share\project\src\A.java"),
+ ("//server/share/project", r"\\?\UNC\server\share\project\src\A.java"),
+ ] {
+ let host =
+ MockHost::install().with_capabilities(&["project.read", "events", "storage", "process:checkstyle"]);
+ host.set_project_root(Some(root));
+ host.add_file("src/A.java", "class A {\n\tint x;\n void f() {} \n}");
+ let xml = REPORT.replace("/project/src/A.java", reported);
+ host.on_process("checkstyle", move |_, _| {
+ Ok(ProcessOutput { status: Some(1), stdout: xml.clone(), stderr: String::new() })
+ });
+ let path = format!("{root}/src/A.java");
+ let mut plugin = Checkstyle::default();
+ plugin.handle_events(&[Event::FileSaved(path.clone())], &host.invocation());
+ assert_eq!(host.diagnostics()[&path].len(), 2, "{reported}");
+ let doc = Document {
+ path: path.clone(),
+ language: "java".into(),
+ version: 1,
+ text: "class A {\n\tint x;\n void f() {} \n}".into(),
+ };
+ let actions = plugin.code_actions(&doc, Range::new(Position::new(1, 0), Position::new(2, 0)));
+ assert_eq!(actions.len(), 2, "{reported}");
+ assert_eq!(actions[0].edits[0].0, path);
+ }
+ }
+
#[test]
fn the_projects_configuration_wins() {
let host = MockHost::install().with_capabilities(&["project.read", "events", "storage", "process:checkstyle"]);