mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
feat(plugins): let plugin views open a project file at a line (#1362)
Plugin tool windows that list code locations (routes, failed tests, queries) had no way to take the user to file:line. A host import `views.open-location(path, line, column)` opens a file inside the project in an editor of the window the call came from, caret at the given line and column (0-based, UTF-16). It needs `project.read` and refuses paths outside the project; the event goes only to the calling window, as `open-terminal` does. The Rust SDK exposes it as `host::open_location` (with the mock), the API table lists it, and the shipped components are rebuilt for the new WIT. Closes #1334 Co-authored-by: yzPeedro <54516747+yzPeedro@users.noreply.github.com>
This commit is contained in:
@@ -667,6 +667,17 @@ impl HostApi for PluginApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn open_location(&self, client: Option<&str>, path: &Path, line: u32, column: u32) -> Result<(), HostError> {
|
||||
let event = PluginEvent::OpenLocation {
|
||||
plugin_id: self.id().to_owned(),
|
||||
path: path.to_path_buf().into(),
|
||||
line,
|
||||
column,
|
||||
};
|
||||
self.shared.sink.emit(client, event);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setting(&self, root: Option<&Path>, key: &str) -> Option<String> {
|
||||
let schema: Vec<super::ui_points::SettingSchema> = self
|
||||
.shared
|
||||
|
||||
@@ -114,6 +114,7 @@ pub fn forwarded(event: &PluginEvent) -> bool {
|
||||
| PluginEvent::ViewChanged { .. }
|
||||
| PluginEvent::ViewMessage { .. }
|
||||
| PluginEvent::OpenTerminal { .. }
|
||||
| PluginEvent::OpenLocation { .. }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -192,6 +192,8 @@ pub enum PluginEvent {
|
||||
args: Vec<String>,
|
||||
cwd: ide_core::IpcPath,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
OpenLocation { plugin_id: String, path: ide_core::IpcPath, line: u32, column: u32 },
|
||||
/// Plugin languages, or the servers they may start, changed: re-fetch
|
||||
/// `languages` (the host re-detects open documents: [`crate::Host::plugin_languages_changed`]).
|
||||
LanguagesChanged,
|
||||
|
||||
@@ -2045,6 +2045,38 @@ fn pinned_programs_fill_views_and_open_terminals() {
|
||||
assert_eq!(args, ["follow", "build", "--lines=5"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn views_open_project_files_at_a_line() {
|
||||
let f = fixture(Limits::default());
|
||||
let info = f.install(&sample("jobs"));
|
||||
assert_eq!(info.status, PluginStatus::Inactive, "{:?}", info.error);
|
||||
std::fs::write(f.project.join("jobs.log"), "build\ndeploy\nfailed\n").unwrap();
|
||||
let press = |button: &str| {
|
||||
f.take_events();
|
||||
let result = f.host.view_event("jobs", "jobs", ide_plugin::ViewEvent::Button { id: button.into() }, f.inv());
|
||||
let opened = f.take_events().into_iter().find_map(|(client, e)| match e {
|
||||
PluginEvent::OpenLocation { path, line, column, .. } => Some((client, path.0, line, column)),
|
||||
_ => None,
|
||||
});
|
||||
(result, opened)
|
||||
};
|
||||
|
||||
let (result, opened) = press("log");
|
||||
assert!(result.is_ok(), "{result:?}");
|
||||
let (client, path, line, column) = opened.expect("a location");
|
||||
assert_eq!((client.as_deref(), line, column), (Some("main"), 2, 4));
|
||||
assert_eq!(path.canonicalize().unwrap(), f.project.join("jobs.log").canonicalize().unwrap());
|
||||
|
||||
let (result, opened) = press("outside");
|
||||
assert!(result.is_err(), "a path outside the project is refused");
|
||||
assert!(opened.is_none());
|
||||
|
||||
f.host.set_capability("jobs", "project.read", false).unwrap();
|
||||
let (result, opened) = press("log");
|
||||
assert!(result.unwrap_err().message.contains("project.read"), "needs project.read");
|
||||
assert!(opened.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn views_can_use_an_svg_icon_from_the_package() {
|
||||
let f = fixture(Limits::default());
|
||||
|
||||
@@ -105,6 +105,7 @@ pub trait HostApi: Send + Sync {
|
||||
args: Vec<String>,
|
||||
cwd: Option<&Path>,
|
||||
) -> Result<(), HostError>;
|
||||
fn open_location(&self, client: Option<&str>, path: &Path, line: u32, column: u32) -> Result<(), HostError>;
|
||||
/// A setting of the plugin as JSON (project over global over the schema default).
|
||||
fn setting(&self, root: Option<&Path>, key: &str) -> Option<String>;
|
||||
}
|
||||
|
||||
@@ -640,6 +640,21 @@ impl views::Host for StoreData {
|
||||
let title = clip(title, crate::runtime::MAX_TITLE);
|
||||
Ok(self.host.open_terminal(client.as_deref(), title, &program, args, cwd.as_deref()).map_err(to_wit))
|
||||
}
|
||||
|
||||
fn open_location(&mut self, path: String, line: u32, column: u32) -> Fallible<()> {
|
||||
if let Err(e) = self.require(Capability::ProjectRead) {
|
||||
return Ok(Err(e));
|
||||
}
|
||||
let Some(root) = self.invocation.workspace_root.clone() else {
|
||||
return Ok(Err(wit::Error::Invalid("no project is open in this window".into())));
|
||||
};
|
||||
let real = match sandbox::resolve_in_root(&root, &path) {
|
||||
Ok(real) => real,
|
||||
Err(e) => return Ok(Err(wit::Error::Invalid(e))),
|
||||
};
|
||||
let client = self.invocation.client.clone();
|
||||
Ok(self.host.open_location(client.as_deref(), &real, line, column).map_err(to_wit))
|
||||
}
|
||||
}
|
||||
|
||||
impl settings::Host for StoreData {
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
a50d1eb41ab8dd0c9c8d6bae8266906cd6eb5de8e6f6ca6f681990389472bb5b
|
||||
1f7402e2b1b4602e4073eb61837c3dc8840f9a4f7275d800dbd6fcf6ce5e40c3
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
2fb6aa879562733bf5b7f11dabe8fc6a415c9ef695868fe7845417a4245d2dc8
|
||||
4f6cd5b5e0c12e7dcbccf66a6ad97276bf968bc3ad26f0fd08e1f7765cbfee14
|
||||
|
||||
@@ -12,6 +12,7 @@ activation = ["on-view:jobs", "on-command:*"]
|
||||
|
||||
[capabilities]
|
||||
required = ["process:jobs"]
|
||||
optional = ["project.read"]
|
||||
|
||||
[[contributes.views]]
|
||||
id = "jobs"
|
||||
|
||||
@@ -64,6 +64,8 @@ impl Views for Jobs {
|
||||
return self.follow();
|
||||
}
|
||||
ViewEvent::Button(b) if b == "follow" => return self.follow(),
|
||||
ViewEvent::Button(b) if b == "log" => return host::open_location("jobs.log", 2, 4),
|
||||
ViewEvent::Button(b) if b == "outside" => return host::open_location("../outside.log", 0, 0),
|
||||
_ => return Ok(()),
|
||||
}
|
||||
host::refresh_view(VIEW);
|
||||
|
||||
@@ -1 +1 @@
|
||||
25b0f419a68a3b1651d3c2774d834b05e40ede52bc5ed8c0eba6d27a204efe82
|
||||
3d4dd1b6365c7fbafdd3c1f99f4405e5ee40fc5e56c97da960adfe7af2e7edb8
|
||||
|
||||
@@ -313,6 +313,7 @@ then run `openidea-plugin check`.
|
||||
| `host::refresh_view(view)` | `views.refresh` | The view (or custom editors) render again; call it when the content changed. |
|
||||
| `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::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. |
|
||||
|
||||
`run-command` receives an optional string argument: `executeAction("plugin.<plugin id>.<command id>", "arg")`
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
28e4bf10a41bdda011111b0e150ebe96205c57f410bad98c19eea3f1286349d6
|
||||
9ad412cb7b853d40467c67305a1126bb87910d179ecfa1c88e13d4a900367c50
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
7e015486cc916f5edbac542863373fd7204f5bcd534f6c408d5f9d2b72913862
|
||||
49a3c6bf2511ba137457397203244bdfca3d6e10f62dbc7fde9473dab07f6457
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
a6cf59e74595e13908e2cbf1752c58665c6963de258413c9d7332d2a7dd9cbd0
|
||||
c199dc52e00d3943882d532e7e6095f7b2f8d7d6026cf1ccaa685dd6300f2319
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
b8dde206a74b222b93e61056d4cbe403617ae240d142e01e91420a2aacdb2da4
|
||||
6047be5eeaa1330d7aa1f182e88ae2f352f244829ad9a01fdc3a2a081bdca95d
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
a681c2abd4a65d6aeb62efd514e9b170f4fca52b0c14d2d380a3b04be0a3fbc6
|
||||
7a3ee410d228d1e39155aa042237d222d23d7f5a45e261086cffc45c6ee1ad32
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
3c83bc9559df54457e19e650cdd21bab9d609e22daaa1fc4f596910b70834a34
|
||||
7658a95eb9fc27468cae4b1d20e3b5ab299fde478e07f69ab0fdb4cc10caf11d
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
63f1bbdf6eca5a4642cd3453ae384ff3f876de0373c12008b757a63852164b71
|
||||
227a30ef0028f167a20825bdd775b15392161e641f6c68030e01d0c79e263bb5
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
0db5cb24c358651d0e728ab3efb52286290c1a38ec379435225c935d4165ad6a
|
||||
046f769822102de0d4ecec99ad41027cb792941065226716f38a6e64c50ff364
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
257478b050ea360c55e904f2a89c6069293139ff0d97ba860de3f29dac927787
|
||||
0b9c3572390a3caab7d47f9b975439f8dde3feff6b9044579d99b8334ac2a692
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
21e7510d6a3712bf0f21d53a5c8d652e3c2787d89a1fc25998cde96efe124207
|
||||
0af62bcd4583d126a616cafbf2ddc80f039a3ead2b5dcb7144222b0ebc673432
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
04844bfbfbcb1770f4f4742658944c592922b037b2387e9340de58ef0d9baacc
|
||||
24ef150beefb05622212228e1c44cb487fffe6526776762d713f3ebdf4c02ac0
|
||||
|
||||
@@ -245,6 +245,10 @@ mod imp {
|
||||
wit::views::open_terminal(title, program, args).map_err(error)
|
||||
}
|
||||
|
||||
pub fn open_location(path: &str, line: u32, column: u32) -> Result<(), Error> {
|
||||
wit::views::open_location(path, line, column).map_err(error)
|
||||
}
|
||||
|
||||
pub fn setting(key: &str) -> Option<String> {
|
||||
wit::settings::get(key)
|
||||
}
|
||||
@@ -387,6 +391,10 @@ pub fn open_terminal(title: &str, program: &str, args: &[String]) -> Result<(),
|
||||
imp::open_terminal(title, program, args)
|
||||
}
|
||||
|
||||
pub fn open_location(path: &str, line: u32, column: u32) -> Result<(), Error> {
|
||||
imp::open_location(path, line, column)
|
||||
}
|
||||
|
||||
/// One of the plugin's settings (`[contributes.settings]`) as JSON: the
|
||||
/// user's value (project over global) or the schema's default.
|
||||
pub fn setting(key: &str) -> Option<String> {
|
||||
|
||||
@@ -74,6 +74,7 @@ struct State {
|
||||
refreshed: Vec<String>,
|
||||
view_messages: Vec<(String, String)>,
|
||||
terminals: Vec<MockTerminal>,
|
||||
locations: Vec<(String, u32, u32)>,
|
||||
settings: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
@@ -220,6 +221,10 @@ impl MockHost {
|
||||
self.state.borrow().view_messages.clone()
|
||||
}
|
||||
|
||||
pub fn opened_locations(&self) -> Vec<(String, u32, u32)> {
|
||||
self.state.borrow().locations.clone()
|
||||
}
|
||||
|
||||
/// Terminals the plugin opened.
|
||||
pub fn terminals(&self) -> Vec<MockTerminal> {
|
||||
self.state.borrow().terminals.clone()
|
||||
@@ -595,6 +600,14 @@ pub mod current {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_location(path: &str, line: u32, column: u32) -> Result<(), Error> {
|
||||
with(|s| {
|
||||
require(s, "project.read")?;
|
||||
s.locations.push((path.into(), line, column));
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn setting(key: &str) -> Option<String> {
|
||||
with(|s| s.settings.get(key).cloned())
|
||||
}
|
||||
|
||||
@@ -133,6 +133,10 @@ interface views {
|
||||
/// `program` (pinned by a `process:<program>` grant) with `args` in the
|
||||
/// project root: long-running output (logs) and interactive sessions.
|
||||
open-terminal: func(title: string, program: string, args: list<string>) -> result<_, error>;
|
||||
/// Opens `path` (project-relative or absolute, inside the project) in an
|
||||
/// editor of the window the call came from, the caret at `line` and
|
||||
/// `column` (0-based; UTF-16 column): a view's links to code.
|
||||
open-location: func(path: string, line: u32, column: u32) -> result<_, error>;
|
||||
}
|
||||
|
||||
/// The plugin's own settings (`[contributes.settings]`), as the user set them
|
||||
|
||||
+1
-1
@@ -5480,7 +5480,7 @@ export type PluginEvent =
|
||||
/** For the script of a view's `html` body. */
|
||||
{ type: "viewMessage"; pluginId: string; view: string; message: string } |
|
||||
/** Open a terminal running `program` (already approved and verified). */
|
||||
{ type: "openTerminal"; pluginId: string; title: string; program: IpcPath; args: string[]; cwd: IpcPath } |
|
||||
{ type: "openTerminal"; pluginId: string; title: string; program: IpcPath; args: string[]; cwd: IpcPath } | { type: "openLocation"; pluginId: string; path: IpcPath; line: number; column: number } |
|
||||
/**
|
||||
* Plugin languages, or the servers they may start, changed: re-fetch
|
||||
* `languages` (the host re-detects open documents: [`crate::Host::plugin_languages_changed`]).
|
||||
|
||||
@@ -12,7 +12,7 @@ import { onIdeEvent } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { shortcutLabel } from "@/platform/keymap";
|
||||
import { BarButton } from "@/ui/BarButton";
|
||||
import { registerStatusItem } from "@/workbench/contributions";
|
||||
import { maybeEditorService, registerStatusItem } from "@/workbench/contributions";
|
||||
import { removeTask, upsertTask, useTasks } from "@/workbench/tasks";
|
||||
import { pluginsBackend } from "./backend";
|
||||
import { bumpView, deliverViewMessage } from "./views/viewStore";
|
||||
@@ -259,6 +259,9 @@ export function handleRuntimeEvent(e: PluginEvent): boolean {
|
||||
// The program was approved (`process:`) and verified by the host.
|
||||
void executeAction("terminal.runCommand", { program: e.program, args: e.args, name: e.title, cwd: e.cwd });
|
||||
return true;
|
||||
case "openLocation":
|
||||
void maybeEditorService()?.openFile(e.path, { selection: { line: e.line, col: e.column }, focus: true });
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -133,6 +133,10 @@ interface views {
|
||||
/// `program` (pinned by a `process:<program>` grant) with `args` in the
|
||||
/// project root: long-running output (logs) and interactive sessions.
|
||||
open-terminal: func(title: string, program: string, args: list<string>) -> result<_, error>;
|
||||
/// Opens `path` (project-relative or absolute, inside the project) in an
|
||||
/// editor of the window the call came from, the caret at `line` and
|
||||
/// `column` (0-based; UTF-16 column): a view's links to code.
|
||||
open-location: func(path: string, line: u32, column: u32) -> result<_, error>;
|
||||
}
|
||||
|
||||
/// The plugin's own settings (`[contributes.settings]`), as the user set them
|
||||
|
||||
Reference in New Issue
Block a user