mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 16:08:05 +00:00
fix(plugins): hand plugins paths in one form on Windows (#1500)
* fix(plugins): hand plugins paths in one form on Windows Plugins got the project root, editor path, provider documents, code action edits, vcs-status roots and find-files results as D:\… while event paths from the UI use /, so Checkstyle could not find the project of a saved file. The host now converts every path it hands to plugins with ipc_path::to_ipc. * fix(checkstyle): match native Windows report paths * test(plugins): use workbench IPC paths in host fixtures
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//! `plugins/build.sh <dir>` 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");
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
@@ -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<ProjectRes>, path: String) -> Fallible<String> {
|
||||
@@ -346,7 +348,7 @@ impl editor::Host for StoreData {}
|
||||
|
||||
impl editor::HostTextEditor for StoreData {
|
||||
fn path(&mut self, this: Resource<EditorRes>) -> wasmtime::Result<String> {
|
||||
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<EditorRes>) -> wasmtime::Result<String> {
|
||||
|
||||
Generated
+1
-1
@@ -103,7 +103,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openidea-plugin-openidea-checkstyle"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"openidea-plugin",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "openidea-plugin-openidea-checkstyle"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
2f4608630c5e41453458ecee5bc4e2f1994a680d4307a5f002f9dd5ccc04d457
|
||||
69273e1ddfd2fff9f49c11d4fbdc60a771f8e3befe440e1623c2ab97469aa250
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -110,6 +110,26 @@ fn project_of(path: &str) -> Option<Project> {
|
||||
.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<Diagnostic> = 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"]);
|
||||
|
||||
Reference in New Issue
Block a user