Compare commits

..
Author SHA1 Message Date
l0ng-ai 6f01d74860 test(plugins): let the first call in the deadline test start its instance
The first highlights call of edit_path_calls_have_soft_and_hard_deadlines
starts the plugin instance, which can take longer than the 50 ms soft
deadline on a slow runner (Windows CI). Wait for it as the later calls do.
2026-11-04 19:58:07 +08:00
61 changed files with 215 additions and 761 deletions

No files matched your search

+11 -67
View File
@@ -76,17 +76,9 @@ async fn plugin_call<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static)
tokio::task::spawn_blocking(f).await.ok()
}
/// Items from plugins carry no server: commands don't apply, and a code
/// action is resolved by the plugin named in its `data` ([`PluginActionRef`]).
/// Items from plugins carry no server (resolve and commands don't apply).
const PLUGIN_SERVER: ServerId = ServerId(0);
/// `data` of a plugin's code action whose edits it computes when picked.
#[derive(serde::Serialize, serde::Deserialize)]
struct PluginActionRef {
plugin: String,
data: String,
}
/// A plugin's `completion-item.kind` (wit/providers.wit lists them).
fn plugin_completion_kind(kind: &str) -> Option<CompletionKind> {
Some(match kind {
@@ -128,14 +120,14 @@ fn plugin_completion(item: ide_plugin::CompletionItem) -> CompletionItem {
kind,
documentation: item.documentation.map(|value| Markup { kind: "markdown".into(), value }),
sort_text: item.sort_text,
filter_text: item.filter_text,
filter_text: None,
preselect: false,
deprecated: false,
insert_text: item.insert_text,
is_snippet: item.snippet,
is_snippet: false,
replace_range: item.replace,
replace_range_tab: None,
additional_edits: item.additional_edits,
additional_edits: Vec::new(),
commit_characters: Vec::new(),
command: None,
data: None,
@@ -322,7 +314,7 @@ pub async fn lsp_code_actions(
};
let plugin_actions: Vec<CodeAction> = plugin_actions
.into_iter()
.map(|(plugin, a)| plugin_code_action(&plugin, a))
.map(|(_, a)| plugin_code_action(a))
.filter(|a| {
only.is_empty()
|| a.kind.as_ref().is_some_and(|k| only.iter().any(|o| k == o || k.starts_with(&format!("{o}."))))
@@ -338,53 +330,24 @@ pub async fn lsp_code_actions(
prepare_actions(project, doc, version, actions).await
}
fn plugin_code_action(plugin: &str, a: ide_plugin::CodeAction) -> CodeAction {
// With `data` and no edits, the plugin computes them when the action is picked.
let lazy = a.edits.is_empty() && a.data.is_some();
let files: Vec<_> = a
fn plugin_code_action(a: ide_plugin::CodeAction) -> CodeAction {
let files = a
.edits
.into_iter()
.map(|(path, edits)| ide_core::FileEdit::Edit { path: path.into(), edits, version: None })
.collect();
let data = a
.data
.filter(|_| lazy)
.and_then(|data| serde_json::to_value(PluginActionRef { plugin: plugin.to_owned(), data }).ok());
CodeAction {
title: a.title,
kind: a.kind,
is_preferred: a.preferred,
disabled_reason: None,
edit: (!lazy).then(|| WorkspaceEdit { files, preconditions: Vec::new() }),
edit: Some(WorkspaceEdit { files, preconditions: Vec::new() }),
command: None,
data,
data: None,
server: PLUGIN_SERVER,
}
}
/// A code action resolved by whoever offered it: its language server, or
/// the plugin its `data` names.
async fn resolve_action(host: &Host, window: &str, doc: DocId, action: CodeAction) -> IdeResult<CodeAction> {
if action.server != PLUGIN_SERVER {
return host.project(window)?.lsp.resolve_code_action(doc, action).await;
}
let Some(r) = action.data.clone().and_then(|d| serde_json::from_value::<PluginActionRef>(d).ok()) else {
return Ok(action);
};
let (plugins, d) = plugin_providers(host, window, doc, ProviderKind::CodeActions)
.ok_or_else(|| ide_core::IdeError::unavailable("the plugin that offered this action is gone"))?;
let pending = ide_plugin::CodeAction {
title: action.title.clone(),
kind: action.kind.clone(),
preferred: action.is_preferred,
edits: Vec::new(),
data: Some(r.data),
};
let plugin = r.plugin.clone();
let resolved = blocking(move || plugins.resolve_code_action(&plugin, &d, pending)).await?;
Ok(plugin_code_action(&r.plugin, ide_plugin::CodeAction { data: None, ..resolved }))
}
/// Cancellable code actions with the kinds the servers advertise (Refactor This, refactoring availability).
pub async fn lsp_code_action_query(
host: &Host,
@@ -418,7 +381,7 @@ pub async fn lsp_resolve_code_action(
let project = host.project(window)?;
let documents = project.documents.clone();
let version = blocking(move || documents.version(doc)).await?;
let action = resolve_action(host, window, doc, action).await?;
let action = project.lsp.resolve_code_action(doc, action).await?;
Ok(prepare_actions(project, doc, version, vec![action]).await?.remove(0))
}
@@ -428,7 +391,7 @@ pub async fn lsp_apply_code_action(host: &Host, window: &str, doc: DocId, action
let action = if action.edit.is_none() && action.command.is_none() {
let documents = project.documents.clone();
let version = blocking(move || documents.version(doc)).await?;
let action = resolve_action(host, window, doc, action).await?;
let action = project.lsp.resolve_code_action(doc, action).await?;
prepare_actions(project.clone(), doc, version, vec![action]).await?.remove(0)
} else {
action
@@ -838,25 +801,6 @@ pub(crate) fn register(d: &mut Dispatcher) {
mod tests {
use super::*;
#[test]
fn a_plugin_action_with_data_and_no_edits_is_resolved_by_its_plugin() {
let action = |edits: Vec<(std::path::PathBuf, Vec<TextEdit>)>, data: Option<&str>| ide_plugin::CodeAction {
title: "Fix".into(),
kind: None,
preferred: false,
edits,
data: data.map(str::to_owned),
};
let lazy = plugin_code_action("acme.x", action(Vec::new(), Some("ref")));
assert!(lazy.edit.is_none());
let r: PluginActionRef = serde_json::from_value(lazy.data.unwrap()).unwrap();
assert_eq!((r.plugin.as_str(), r.data.as_str()), ("acme.x", "ref"));
// With edits, `data` is the plugin's business: applied as is.
let edit = TextEdit { range: Range::default(), new_text: "x".into() };
let eager = plugin_code_action("acme.x", action(vec![("/a".into(), vec![edit])], Some("ref")));
assert!(eager.edit.is_some() && eager.data.is_none());
}
#[test]
fn every_completion_kind_the_plugin_api_documents_is_understood() {
let wit = include_str!("../../../../wit/providers.wit");
-15
View File
@@ -292,21 +292,6 @@ impl PluginHost {
.collect()
}
/// The edits of an action `plugin_id` returned with `data`, now that the
/// user picked it: a call with the command budget, not the edit path's.
pub fn resolve_code_action(
&self,
plugin_id: &str,
doc: &ProviderDocument,
action: CodeAction,
) -> ide_core::IdeResult<CodeAction> {
let d = Arc::new(doc.clone());
let timeout = self.limits.timeout;
self.code_call(plugin_id, ide_plugin::Invocation::default(), "resolving the code action", move |rt, _| {
rt.resolve_code_action(&d, &action, timeout)
})
}
pub fn provide_definition(&self, doc: &ProviderDocument, at: ide_core::Pos) -> Vec<Location> {
let d = Arc::new(doc.clone());
self.edit_calls(ProviderKind::Definition, &doc.language, move |rt, timeout| {
+1 -29
View File
@@ -274,15 +274,6 @@ pub fn restricted_command_allowed(cmd: &str) -> bool {
| "plugin_set_theme_frame"
| "plugin_catalog"
| "plugin_assets"
| "plugin_market"
| "plugin_market_url"
| "plugin_set_market_url"
| "plugin_market_download"
| "plugin_unpack_package"
| "plugin_stage_package"
| "plugin_preview_install"
| "plugin_read_intellij_theme"
| "plugin_installed_intellij_themes"
| "notebook_open"
| "notebook_get"
| "notebook_edit"
@@ -328,31 +319,12 @@ pub fn restricted_command_allowed(cmd: &str) -> bool {
) || cmd.starts_with("plugin:")
}
/// Commands that change which plugin code runs: allowed where no project is open (the
/// welcome screen), otherwise only in a trusted project.
pub fn projectless_command_allowed(cmd: &str) -> bool {
matches!(
cmd,
"plugin_install_dir"
| "plugin_uninstall"
| "plugin_set_enabled"
| "plugin_grant"
| "plugin_set_capability"
| "plugin_roll_back"
| "plugin_reload"
)
}
impl crate::Host {
pub fn authorize_command(&self, window: &str, command: &str) -> IdeResult<()> {
if restricted_command_allowed(command) {
return Ok(());
}
match self.project(window) {
Ok(project) => project.require_trust(),
Err(_) if projectless_command_allowed(command) => Ok(()),
Err(error) => Err(error),
}
self.project(window)?.require_trust()
}
}
+1 -54
View File
@@ -635,7 +635,7 @@ fn idle_instances_are_reclaimed_and_schedules_reactivate() {
assert_eq!(f.host.instances(), 0);
let info = f.host.info_of("misbehave").unwrap();
assert_eq!((info.status, info.stats.reclaims), (PluginStatus::Inactive, 1));
assert_eq!(f.command_titles("misbehave").len(), 10, "contributions stay");
assert_eq!(f.command_titles("misbehave").len(), 8, "contributions stay");
assert_eq!(f.run("misbehave", "echo", Some("y")).unwrap().as_deref(), Some("y"));
assert_eq!(f.host.info_of("misbehave").unwrap().stats.activations, 2);
@@ -2410,56 +2410,3 @@ fn uninstalling_deletes_the_plugins_secrets() {
f.host.uninstall("openidea.ai-completion").unwrap();
assert!(keychain.0.lock().is_empty(), "{:?}", keychain.0.lock().keys().collect::<Vec<_>>());
}
/// Plugin API 1.0 leaves room to grow: event kinds are names, and the editor
/// providers carry snippets, imports, lazily computed quick fixes and tagged
/// problems.
#[test]
fn completion_quick_fixes_problems_and_event_kinds_of_api_1() {
let f = fixture(Limits::default());
let pkg = f._tmp.path().join("misbehave-providers");
copy_dir(&misbehave_pkg(), &pkg);
let manifest = std::fs::read_to_string(pkg.join("plugin.toml")).unwrap();
std::fs::write(
pkg.join("plugin.toml"),
format!("{manifest}\n[[contributes.providers]]\nkinds = [\"completion\", \"code-actions\"]\nlanguages = [\"text\"]\n"),
)
.unwrap();
f.install(&pkg);
let doc = |text: &str| ide_plugin::ProviderDocument {
path: f.project.join("a.txt"),
language: "text".into(),
version: 1,
text: text.into(),
};
let at = ide_core::Pos::new(0, 0);
let items = f.host.provide_completion(&doc("rich"), at, None);
let item = &items[0].1;
assert_eq!(
(item.insert_text.as_str(), item.snippet, item.filter_text.as_deref()),
("f(${1:x})$0", true, Some("fun"))
);
assert_eq!(item.additional_edits[0].new_text, "use f;\n");
// Offered without edits; computed when picked.
let actions = f.host.provide_code_actions(&doc("lazy"), ide_core::Range::default());
let (plugin, action) = actions.into_iter().next().expect("offered");
assert!(action.edits.is_empty() && action.data.as_deref() == Some("fix"));
let resolved = f.host.resolve_code_action(&plugin, &doc("lazy"), action.clone()).unwrap();
assert_eq!(resolved.edits[0].1[0].new_text, "fixed ");
let foreign = ide_plugin::CodeAction { data: Some("other".into()), ..action };
assert!(f.host.resolve_code_action(&plugin, &doc("lazy"), foreign).is_err());
// A kind this IDE doesn't know is refused, not a trap.
let err = f.host.run_command("misbehave", "subscribe", Some("file-deleted"), f.inv()).unwrap_err();
assert!(err.message.contains("no event kind"), "{}", err.message);
let err = f.host.run_command("misbehave", "subscribe", Some("file-saved"), f.inv()).unwrap_err();
assert!(err.message.contains("events"), "known, but not granted: {}", err.message);
// Tags the IDE knows, the tool that found it.
let path = f.project.join("a.txt");
f.host.run_command("misbehave", "diagnose", Some(&path.to_string_lossy()), f.inv()).unwrap();
let d = f.workspace.diagnostics.lock().get(&("misbehave".into(), path)).cloned().unwrap();
assert_eq!((d[0].unnecessary, d[0].deprecated, d[0].source.as_deref()), (true, false, Some("lint")));
}
+2 -11
View File
@@ -58,11 +58,6 @@ async fn application_commands_work_in_restricted_projects_and_without_a_project(
"vcs_write_patch",
"remote_bootstrap_list",
"deploy_test_connection",
"plugin_market",
"plugin_market_url",
"plugin_set_market_url",
"plugin_market_download",
"plugin_preview_install",
] {
assert!(ide_host::trust::restricted_command_allowed(command), "{command}");
for window in ["main", "welcome"] {
@@ -77,6 +72,8 @@ async fn application_commands_work_in_restricted_projects_and_without_a_project(
"tools_list",
"tools_install",
"tools_uninstall",
"plugin_install_dir",
"plugin_reload",
"vcs_git_version",
"docker_compose_op",
"db_execute",
@@ -96,12 +93,6 @@ async fn application_commands_work_in_restricted_projects_and_without_a_project(
assert!(host.authorize_command(window, command).is_err(), "{window}: {command}");
}
}
// Plugin installs run plugin code: from the welcome screen, but not in an untrusted project.
for command in ["plugin_install_dir", "plugin_uninstall", "plugin_set_enabled", "plugin_reload"] {
assert!(!ide_host::trust::restricted_command_allowed(command), "{command}");
assert!(host.authorize_command("main", command).is_err(), "{command}");
assert!(host.authorize_command("welcome", command).is_ok(), "{command}");
}
// These requests validate the vendor before any network access, independently of project trust.
for window in ["main", "welcome"] {
let error = ide_host::ops::tools::jdk_download_releases(
+35 -96
View File
@@ -467,6 +467,7 @@ const MAX_SYNTAX_ITEMS: usize = 200_000;
const MAX_BATCH: usize = 1000;
fn event_to_wit(e: WorkbenchEvent) -> wit_events::Event {
use crate::types::ActionSource as S;
match e {
WorkbenchEvent::FileOpened(p) => wit_events::Event::FileOpened(p),
WorkbenchEvent::FileSaved(p) => wit_events::Event::FileSaved(p),
@@ -476,7 +477,14 @@ fn event_to_wit(e: WorkbenchEvent) -> wit_events::Event {
WorkbenchEvent::ActionPerformed(a) => wit_events::Event::ActionPerformed(wit_events::ActionPerformed {
action: a.action,
title: a.title,
source: a.source.name().to_owned(),
source: match a.source {
S::Keyboard => wit_events::ActionSource::Keyboard,
S::Menu => wit_events::ActionSource::Menu,
S::Toolbar => wit_events::ActionSource::Toolbar,
S::ContextMenu => wit_events::ActionSource::ContextMenu,
S::Search => wit_events::ActionSource::Search,
S::Other => wit_events::ActionSource::Other,
},
shortcut: a.shortcut,
}),
}
@@ -662,17 +670,9 @@ impl Runtime {
detail: i.detail.map(|d| clip(d, MAX_TITLE)),
kind: i.kind.map(|k| clip(k, 32)),
insert_text: clip(i.insert_text, 64 << 10),
snippet: i.snippet,
replace: i.replace.map(range_from_wit),
sort_text: i.sort_text.map(|t| clip(t, 64)),
filter_text: i.filter_text.map(|t| clip(t, MAX_TITLE)),
documentation: i.documentation.map(|d| clip(d, 16 << 10)),
additional_edits: i
.additional_edits
.into_iter()
.take(MAX_ADDITIONAL_EDITS)
.map(edit_from_wit)
.collect(),
})
.collect())
}
@@ -699,20 +699,27 @@ impl Runtime {
) -> Result<Vec<CodeAction>, CallError> {
let d = doc_to_wit(doc);
let actions = self.provider_call(timeout, |p, s| p.call_code_actions(s, &d, range_to_wit(selection)))?;
Ok(actions.into_iter().take(MAX_PROVIDER_ITEMS).map(code_action_from_wit).collect())
}
/// The edits of an action `provide_code_actions` returned with `data`.
pub fn resolve_code_action(
&mut self,
doc: &ProviderDocument,
action: &CodeAction,
timeout: Duration,
) -> Result<CodeAction, CallError> {
let d = doc_to_wit(doc);
let a = code_action_to_wit(action);
let resolved = self.provider_call(timeout, |p, s| p.call_resolve_code_action(s, &d, &a))?;
resolved.map(code_action_from_wit).map_err(|e| CallError::Guest(e.into()))
Ok(actions
.into_iter()
.take(MAX_PROVIDER_ITEMS)
.map(|a| CodeAction {
title: clip(a.title, MAX_TITLE),
kind: a.kind.map(|k| clip(k, 64)),
preferred: a.preferred,
edits: a
.edits
.into_iter()
.map(|f| {
let edits = f
.edits
.into_iter()
.map(|e| ide_core::TextEdit { range: range_from_wit(e.range), new_text: e.text })
.collect();
(std::path::PathBuf::from(f.path), edits)
})
.collect(),
})
.collect())
}
pub fn provide_definition(
@@ -961,47 +968,6 @@ const MAX_VIEW_ITEMS: usize = 20_000;
const MAX_CELL: usize = 64 << 10;
const MAX_HTML: usize = 4 << 20;
/// Most additional edits of a completion item.
const MAX_ADDITIONAL_EDITS: usize = 100;
fn edit_from_wit(e: wit_types::TextEdit) -> ide_core::TextEdit {
ide_core::TextEdit { range: range_from_wit(e.range), new_text: e.text }
}
fn code_action_from_wit(a: wit_prov::CodeAction) -> CodeAction {
CodeAction {
title: clip(a.title, MAX_TITLE),
kind: a.kind.map(|k| clip(k, 64)),
preferred: a.preferred,
edits: a
.edits
.into_iter()
.map(|f| (std::path::PathBuf::from(f.path), f.edits.into_iter().map(edit_from_wit).collect()))
.collect(),
data: a.data.map(|d| clip(d, 64 << 10)),
}
}
fn code_action_to_wit(a: &CodeAction) -> wit_prov::CodeAction {
wit_prov::CodeAction {
title: a.title.clone(),
kind: a.kind.clone(),
preferred: a.preferred,
edits: a
.edits
.iter()
.map(|(path, edits)| wit_prov::FileEdit {
path: path.to_string_lossy().into_owned(),
edits: edits
.iter()
.map(|e| wit_types::TextEdit { range: range_to_wit(e.range), text: e.new_text.clone() })
.collect(),
})
.collect(),
data: a.data.clone(),
}
}
fn opt_clip(s: Option<String>, max: usize) -> Option<String> {
s.map(|s| clip(s, max))
}
@@ -1009,9 +975,6 @@ fn opt_clip(s: Option<String>, max: usize) -> Option<String> {
fn view_from_wit(v: wit_vt::View) -> View {
let body = match v.body {
wit_vt::Body::Message(m) => ViewBody::Message(clip(m, 16 << 10)),
wit_vt::Body::Other(e) => {
ViewBody::Message(format!("This view needs a newer version of OpenIDEA ({}).", clip(e.kind, 64)))
}
wit_vt::Body::Tree(items) => ViewBody::Tree(
items
.into_iter()
@@ -1043,8 +1006,10 @@ fn view_from_wit(v: wit_vt::View) -> View {
fields
.into_iter()
.take(MAX_VIEW_ITEMS)
.filter_map(|f| {
let value = match f.value {
.map(|f| ViewField {
key: clip(f.key, 128),
label: clip(f.label, MAX_TITLE),
value: match f.value {
wit_vt::FieldValue::Text(t) => FieldValue::Text(clip(t, MAX_CELL)),
wit_vt::FieldValue::Multiline(t) => FieldValue::Multiline(clip(t, 1 << 20)),
wit_vt::FieldValue::Checkbox(b) => FieldValue::Checkbox(b),
@@ -1052,10 +1017,7 @@ fn view_from_wit(v: wit_vt::View) -> View {
options: options.into_iter().take(1000).map(|o| clip(o, MAX_TITLE)).collect(),
selected: clip(selected, MAX_TITLE),
},
// A field type of a newer API: left out.
wit_vt::FieldValue::Other(_) => return None,
};
Some(ViewField { key: clip(f.key, 128), label: clip(f.label, MAX_TITLE), value })
},
})
.collect(),
),
@@ -1105,26 +1067,3 @@ fn event_to_wit_view(e: ViewEvent) -> wit_vt::ViewEvent {
ViewEvent::Message { text } => W::Message(text),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ext(kind: &str) -> wit_types::Extension {
wit_types::Extension { kind: kind.into(), data: "{}".into() }
}
#[test]
fn bodies_and_fields_of_a_newer_api_degrade() {
let view = |body| wit_vt::View { toolbar: Vec::new(), body, status: None };
let shown = view_from_wit(view(wit_vt::Body::Other(ext("markdown"))));
assert_eq!(shown.body, ViewBody::Message("This view needs a newer version of OpenIDEA (markdown).".into()));
let field = |key: &str, value| wit_vt::Field { key: key.into(), label: key.into(), value };
let form = view_from_wit(view(wit_vt::Body::Form(vec![
field("color", wit_vt::FieldValue::Other(ext("color"))),
field("name", wit_vt::FieldValue::Text("n".into())),
])));
let ViewBody::Form(fields) = form.body else { panic!("a form") };
assert_eq!(fields.iter().map(|f| f.key.as_str()).collect::<Vec<_>>(), ["name"], "unknown fields are left out");
}
}
-21
View File
@@ -126,20 +126,6 @@ pub enum ActionSource {
Other,
}
impl ActionSource {
/// As plugins see it.
pub fn name(self) -> &'static str {
match self {
Self::Keyboard => "keyboard",
Self::Menu => "menu",
Self::Toolbar => "toolbar",
Self::ContextMenu => "context-menu",
Self::Search => "search",
Self::Other => "other",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct ActionPerformed {
@@ -362,14 +348,9 @@ pub struct CompletionItem {
/// WIT `completion-kind` name (`method`, `keyword`, …).
pub kind: Option<String>,
pub insert_text: String,
/// `insert_text` has tab stops and placeholders.
pub snippet: bool,
pub replace: Option<ide_core::Range>,
pub sort_text: Option<String>,
pub filter_text: Option<String>,
pub documentation: Option<String>,
/// Applied with the item elsewhere in the document (an import).
pub additional_edits: Vec<ide_core::TextEdit>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -384,8 +365,6 @@ pub struct CodeAction {
pub kind: Option<String>,
pub preferred: bool,
pub edits: Vec<(PathBuf, Vec<ide_core::TextEdit>)>,
/// The plugin's reference: the edits come from `resolve_code_action`.
pub data: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
+18 -11
View File
@@ -440,11 +440,20 @@ impl vcs::Host for StoreData {
}
}
fn kind_from_wit(k: events::EventKind) -> EventKind {
match k {
events::EventKind::FileOpened => EventKind::FileOpened,
events::EventKind::FileSaved => EventKind::FileSaved,
events::EventKind::EditorFocused => EventKind::EditorFocused,
events::EventKind::ActionPerformed => EventKind::ActionPerformed,
events::EventKind::VcsChanged => EventKind::VcsChanged,
events::EventKind::Scheduled => EventKind::Scheduled,
}
}
impl events::Host for StoreData {
fn subscribe(&mut self, kind: String) -> Fallible<()> {
let Some(kind) = EventKind::parse(&kind) else {
return Ok(Err(wit::Error::Invalid(format!("no event kind {kind:?} in this OpenIDEA"))));
};
fn subscribe(&mut self, kind: events::EventKind) -> Fallible<()> {
let kind = kind_from_wit(kind);
if let Some(cap) = kind.capability()
&& let Err(e) = self.require(cap)
{
@@ -454,10 +463,8 @@ impl events::Host for StoreData {
Ok(Ok(()))
}
fn unsubscribe(&mut self, kind: String) -> wasmtime::Result<()> {
if let Some(kind) = EventKind::parse(&kind) {
self.host.subscribe(kind, false);
}
fn unsubscribe(&mut self, kind: events::EventKind) -> wasmtime::Result<()> {
self.host.subscribe(kind_from_wit(kind), false);
Ok(())
}
}
@@ -535,10 +542,10 @@ impl diagnostics::Host for StoreData {
diagnostics::Severity::Hint => ide_core::Severity::Hint,
},
message: clip(d.message, 4096),
source: Some(d.source.map_or_else(|| source.clone(), |s| clip(s, 128))),
source: Some(source.clone()),
code: d.code.map(|c| clip(c, 128)),
unnecessary: d.tags.iter().any(|t| t == "unnecessary"),
deprecated: d.tags.iter().any(|t| t == "deprecated"),
unnecessary: false,
deprecated: false,
server: Some(source.clone()),
})
.collect();
Binary file not shown.
@@ -1 +1 @@
c9acbc021f3840649823cb7680e13907d429c6fda8271fdb0ae6e5ea89ed15ee
86dfe70a30ab1f7955a16e96c1b32c8be663456b871cc4d3bed7dc6052c062da
Binary file not shown.
@@ -1 +1 @@
2fb6aa879562733bf5b7f11dabe8fc6a415c9ef695868fe7845417a4245d2dc8
0dacb82ac35c1f132dc66e85dbf0663b729f818279d3b32cd06403e502fbf880
Binary file not shown.
@@ -1 +1 @@
25b0f419a68a3b1651d3c2774d834b05e40ede52bc5ed8c0eba6d27a204efe82
0ad2e4c6977a576b7eb27fc1e535fd056dd9da64bdfe6cfda9ca9f57537d0384
@@ -40,11 +40,3 @@ title = "insert"
[[contributes.commands]]
id = "cancel"
title = "cancel"
[[contributes.commands]]
id = "subscribe"
title = "subscribe"
[[contributes.commands]]
id = "diagnose"
title = "diagnose"
+4 -52
View File
@@ -14,9 +14,7 @@ wit_bindgen::generate!({
generate_all,
});
use exports::openidea::plugin::providers::{
self, CodeAction, CompletionItem, Document, FileEdit, InlineSuggestion, LineMarker,
};
use exports::openidea::plugin::providers::{self, CodeAction, CompletionItem, Document, InlineSuggestion, LineMarker};
use exports::openidea::plugin::syntax_provider::{self, Fold, Symbol, Token};
use exports::openidea::plugin::{commands, event_handler, lifecycle};
use openidea::plugin::{invocation::Invocation, types};
@@ -69,23 +67,6 @@ impl commands::Guest for Misbehave {
}
// The user gave up: not the plugin failing.
"cancel" => Err(types::Error::Cancelled),
// Event kinds are names: one this IDE doesn't know is refused.
"subscribe" => openidea::plugin::events::subscribe(&arg.unwrap_or_default()).map(|()| None),
// Tagged problems from another tool on the line 0 of `arg`.
"diagnose" => {
use openidea::plugin::diagnostics::{self, Diagnostic, Severity};
let start = types::Position { line: 0, character: 0 };
let d = Diagnostic {
range: types::Range { start, end: start },
severity: Severity::Warning,
message: "unused".into(),
code: None,
source: Some("lint".into()),
tags: vec!["unnecessary".into(), "glowing".into()],
related: Vec::new(),
};
diagnostics::publish(&arg.unwrap_or_default(), &[d]).map(|()| None)
}
other => Err(types::Error::NotFound(format!("command {other}"))),
}
}
@@ -132,24 +113,14 @@ impl syntax_provider::Guest for Misbehave {
impl providers::Guest for Misbehave {
fn completion(doc: Document, _at: types::Position, _trigger: Option<String>) -> Vec<CompletionItem> {
behave(&doc.text);
// `rich`: a snippet that also adds an import.
let rich = doc.text == "rich";
let start = types::Position { line: 0, character: 0 };
vec![CompletionItem {
label: doc.path.clone(),
detail: None,
kind: Some("keyword".into()),
insert_text: if rich { "f(${1:x})$0".into() } else { doc.path },
snippet: rich,
insert_text: doc.path,
replace: None,
sort_text: None,
filter_text: rich.then(|| "fun".into()),
documentation: None,
additional_edits: if rich {
vec![types::TextEdit { range: types::Range { start, end: start }, text: "use f;\n".into() }]
} else {
Vec::new()
},
}]
}
@@ -157,27 +128,8 @@ impl providers::Guest for Misbehave {
None
}
/// `lazy`: one action whose edits come from `resolve-code-action`.
fn code_actions(doc: Document, _selection: types::Range) -> Vec<CodeAction> {
if doc.text != "lazy" {
return Vec::new();
}
vec![CodeAction {
title: "Fix later".into(),
kind: Some("quickfix".into()),
preferred: false,
edits: Vec::new(),
data: Some("fix".into()),
}]
}
fn resolve_code_action(doc: Document, action: CodeAction) -> Result<CodeAction, types::Error> {
if action.data.as_deref() != Some("fix") {
return Err(types::Error::Invalid("not one of mine".into()));
}
let start = types::Position { line: 0, character: 0 };
let edit = types::TextEdit { range: types::Range { start, end: start }, text: "fixed ".into() };
Ok(CodeAction { edits: vec![FileEdit { path: doc.path, edits: vec![edit] }], ..action })
fn code_actions(_doc: Document, _selection: types::Range) -> Vec<CodeAction> {
Vec::new()
}
fn definition(_doc: Document, _at: types::Position) -> Vec<types::Location> {
+2 -25
View File
@@ -321,11 +321,6 @@ Events are batched: a running plugin gets them within a fraction of a second,
an idle one collects them for 2 s before it is woken, so frequent events
(Key Promoter's actions) don't keep it alive.
Kinds are names (`subscribe` takes a string), so later IDEs add kinds without
a new API: an IDE refuses a kind it doesn't know with `invalid`, and a kind
newer than the plugin's SDK arrives as `Event::Other { kind, data }` (JSON).
The SDK's event enums are `#[non_exhaustive]`; match them with a `_` arm.
### Providers
Declare what the plugin provides in the editor; only plugins whose
@@ -343,14 +338,7 @@ Then `register_plugin!(MyPlugin, providers)` and implement
the document (path, language, version, text). Results join the language
servers' (completion lists, ⌥⏎ quick fixes, ⌘B, gutter icons; a code action
carries edits for any files, applied as one change). A completion item's
`kind` (`CompletionKind` in the SDK, a string in the WIT) picks its icon; it
can be a `snippet` (`$1`, `${1:name}`, `$0`), match on `filter_text` instead
of its label, and bring `additional_edits` elsewhere in the document (an
import). A code action can leave `edits` empty and set `data`: its edits are
then computed by `resolve_code_action` when the user picks it, under the
command budget instead of the 50 ms. Problems (`publish_diagnostics`) can name
the tool that found them (`source`) and be `unnecessary` (faded) or
`deprecated` (struck through).
`kind` (`CompletionKind` in the SDK, a string in the WIT) picks its icon.
Editor providers run under the edit-path budget: 50 ms, after which the
result is dropped, and an interruption at 500 ms. Every plugin providing a
kind is asked at the same time, so each has the whole budget.
@@ -405,9 +393,6 @@ collapse of items, form fields, table cells, inserted and deleted rows, and
`html` messages. A tree is a flat list whose items name their `parent`; an
`expandable` item gets its children when it is expanded. A table gets only
the rows it shows (`viewport`): report `total_rows` and return those.
Bodies, field types and view events added by later APIs travel as `Other {
kind, data }`: an IDE that doesn't know a body says it needs updating, and
leaves out a field type it doesn't know.
Showing a view activates the plugin (`on-view:<id>`). `views` become tool
windows; a `customEditors` file (`priority = "default"`) opens in the
@@ -905,10 +890,7 @@ only grows:
Extend with new types and new functions (or interfaces). `types.error` is
used everywhere and is no exception; new situations use `failed(string)`
or `invalid(string)`. Sets that keep growing (completion, token, symbol and
fold kinds, icons, event kinds, action sources, diagnostic tags) are
strings for this reason, and variants that grow (`event`, view `body`,
`view-event`, `field-value`) have an `other(extension)` case, a kind and
JSON data, that carries later cases to older plugins and IDEs.
fold kinds, icons) are strings for this reason.
2. **Host interfaces (imports) may gain functions and interfaces**: older
components don't import them.
3. **Plugin interfaces (exports) never gain functions** once released: the
@@ -920,11 +902,6 @@ only grows:
minor with a message that says to update the IDE or rebuild with the SDK.
5. **One version number**: the `wit/` package version, `ENGINE_VERSION` and
`API_VERSION` agree, and the SDK's `wit/` copy is identical to `wit/`.
6. **1.x calls are synchronous and pass documents whole.** Host calls block
the plugin's instance, and providers get the document's text with each
call. WASI 0.3 async components come with 2.0, behind an adapter in the
host that keeps 1.x plugins running; incremental document sync, if
needed, is a new interface.
Checked automatically:
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
28e4bf10a41bdda011111b0e150ebe96205c57f410bad98c19eea3f1286349d6
aa09a92c0ba9d85a01c56bbf4455e40ae44f005b443ce1002bbe8011408ff975
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
7e015486cc916f5edbac542863373fd7204f5bcd534f6c408d5f9d2b72913862
908c2fd177ad2bb9a8a8492505151c3b81579191156c5ec53f705127f50a8ee9
+6 -3
View File
@@ -97,8 +97,12 @@ fn diagnostic(p: &Problem, text: &str) -> Diagnostic {
let line_len = text.split('\n').nth(p.line as usize).map_or(0, |l| l.encode_utf16().count() as u32);
let start = p.column.unwrap_or(0).min(line_len);
let end = if p.column.is_some() && start < line_len { start + 1 } else { line_len };
let range = Range::new(Position::new(p.line, start), Position::new(p.line, end.max(start)));
Diagnostic::new(range, p.severity, p.message.clone()).code(p.check.clone())
Diagnostic {
range: Range::new(Position::new(p.line, start), Position::new(p.line, end.max(start))),
severity: p.severity,
message: p.message.clone(),
code: Some(p.check.clone()),
}
}
/// The project containing `path`.
@@ -184,7 +188,6 @@ impl Providers for Checkstyle {
kind: Some("quickfix".into()),
preferred: true,
edits: vec![(doc.path.clone(), vec![edit])],
data: None,
});
}
}
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
a6cf59e74595e13908e2cbf1752c58665c6963de258413c9d7332d2a7dd9cbd0
64270fa0f5b3084c78764f09b8ce161742fdbff9c28fb90590da39594e558db1
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
b8dde206a74b222b93e61056d4cbe403617ae240d142e01e91420a2aacdb2da4
22876f842f727a2b9aa292986df3393f5b5f30c5c34319f5665f7d2701c8accf
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
a681c2abd4a65d6aeb62efd514e9b170f4fca52b0c14d2d380a3b04be0a3fbc6
14c60782eca79a25cf2c38d58c05be0eb97c4afc607d4cc83302d1445d094aab
+6 -3
View File
@@ -52,9 +52,12 @@ pub fn diagnostics(text: &str, reports: &[Report]) -> Vec<Diagnostic> {
.map(|r| {
let line = r.line.map_or(0, |l| l.saturating_sub(1)).min(lines.len().saturating_sub(1) as u32);
let len = lines.get(line as usize).map_or(0, |l| l.encode_utf16().count() as u32);
let range = Range::new(Position::new(line, 0), Position::new(line, len));
let severity = if r.error { Severity::Error } else { Severity::Warning };
Diagnostic::new(range, severity, r.message.clone()).code("graphviz")
Diagnostic {
range: Range::new(Position::new(line, 0), Position::new(line, len)),
severity: if r.error { Severity::Error } else { Severity::Warning },
message: r.message.clone(),
code: Some("graphviz".into()),
}
})
.collect()
}
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
3c83bc9559df54457e19e650cdd21bab9d609e22daaa1fc4f596910b70834a34
7f5aacde2b8a8bcee5365c8cbda91045195688899c6537dc2159037cca74a672
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
63f1bbdf6eca5a4642cd3453ae384ff3f876de0373c12008b757a63852164b71
45a7ee993b5c234b0d25e5b05ef60e714fa3edf63d6bd0d6a29b13587c8d09ce
+6 -3
View File
@@ -129,8 +129,12 @@ pub fn header_edit(path: &str, text: &str, year: i64, owner: &str, license: &str
fn missing(text: &str) -> Diagnostic {
let line = insert_line(text);
let len = text.lines().nth(line as usize).map_or(0, |l| l.encode_utf16().count() as u32);
Diagnostic::new(Range::new(Position::new(line, 0), Position::new(line, len)), Severity::Info, "No license header")
.code(CODE)
Diagnostic {
range: Range::new(Position::new(line, 0), Position::new(line, len)),
severity: Severity::Info,
message: "No license header".into(),
code: Some(CODE.into()),
}
}
/// Reports `path` when it takes a header and has none, clears it otherwise.
@@ -270,7 +274,6 @@ impl Providers for LicenseHeader {
kind: Some("quickfix".into()),
preferred: true,
edits: vec![(doc.path.clone(), vec![edit])],
data: None,
}]
}
}
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
0db5cb24c358651d0e728ab3efb52286290c1a38ec379435225c935d4165ad6a
d5cf81455ae8b2b0fd6d1402d8775913bd7999db62b9bdf87675890ce52b1e80
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
257478b050ea360c55e904f2a89c6069293139ff0d97ba860de3f29dac927787
227c8150c6ad7a5a20236764f149c876cbbc3b082763bf2e7fc05de99726b740
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
21e7510d6a3712bf0f21d53a5c8d652e3c2787d89a1fc25998cde96efe124207
bbadcaa7a86a3e0a3f8be6d86aed2229d5cdfd128fda3435b4d64c7e19ad1df2
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
04844bfbfbcb1770f4f4742658944c592922b037b2387e9340de58ef0d9baacc
09cc92e8e731f8f59f3857a71f9a30cc3d9e04a1c32092920f573b0f26592855
+24 -23
View File
@@ -152,12 +152,24 @@ mod imp {
wit::vcs::fetch(p).map_err(error)
}
fn kind(k: EventKind) -> wit::events::EventKind {
use wit::events::EventKind as K;
match k {
EventKind::FileOpened => K::FileOpened,
EventKind::FileSaved => K::FileSaved,
EventKind::EditorFocused => K::EditorFocused,
EventKind::ActionPerformed => K::ActionPerformed,
EventKind::VcsChanged => K::VcsChanged,
EventKind::Scheduled => K::Scheduled,
}
}
pub fn subscribe(k: EventKind) -> Result<(), Error> {
wit::events::subscribe(k.name()).map_err(error)
wit::events::subscribe(kind(k)).map_err(error)
}
pub fn unsubscribe(k: EventKind) {
wit::events::unsubscribe(k.name())
wit::events::unsubscribe(kind(k))
}
pub fn begin_task(title: &str, cancellable: bool) -> TaskRes {
@@ -177,7 +189,7 @@ mod imp {
}
pub fn publish_diagnostics(path: &str, items: &[Diagnostic]) -> Result<(), Error> {
use wit::diagnostics::{Diagnostic as D, RelatedInformation as R, Severity as S};
use wit::diagnostics::{Diagnostic as D, Severity as S};
let items: Vec<D> = items
.iter()
.map(|d| D {
@@ -190,23 +202,6 @@ mod imp {
},
message: d.message.clone(),
code: d.code.clone(),
source: d.source.clone(),
tags: [(d.unnecessary, "unnecessary"), (d.deprecated, "deprecated")]
.into_iter()
.filter(|(on, _)| *on)
.map(|(_, tag)| tag.to_owned())
.collect(),
related: d
.related
.iter()
.map(|r| R {
location: wit::types::Location {
path: r.location.path.clone(),
range: range_out(r.location.range),
},
message: r.message.clone(),
})
.collect(),
})
.collect();
wit::diagnostics::publish(path, &items).map_err(error)
@@ -269,7 +264,7 @@ mod imp {
}
pub fn event(e: wit::events::Event) -> Event {
use wit::events::Event as E;
use wit::events::{ActionSource as S, Event as E};
match e {
E::FileOpened(p) => Event::FileOpened(p),
E::FileSaved(p) => Event::FileSaved(p),
@@ -279,10 +274,16 @@ mod imp {
E::ActionPerformed(a) => Event::ActionPerformed(ActionPerformed {
action: a.action,
title: a.title,
source: ActionSource::parse(&a.source),
source: match a.source {
S::Keyboard => ActionSource::Keyboard,
S::Menu => ActionSource::Menu,
S::Toolbar => ActionSource::Toolbar,
S::ContextMenu => ActionSource::ContextMenu,
S::Search => ActionSource::Search,
S::Other => ActionSource::Other,
},
shortcut: a.shortcut,
}),
E::Other(x) => Event::Other { kind: x.kind, data: x.data },
}
}
}
+19 -66
View File
@@ -255,16 +255,6 @@ macro_rules! __providers_exports {
$crate::__rt::code_actions::<$plugin>(doc, selection)
}
fn resolve_code_action(
doc: $crate::providers_bindings::exports::openidea::plugin::providers::Document,
action: $crate::providers_bindings::exports::openidea::plugin::providers::CodeAction,
) -> ::core::result::Result<
$crate::providers_bindings::exports::openidea::plugin::providers::CodeAction,
$crate::bindings::openidea::plugin::types::Error,
> {
$crate::__rt::resolve_code_action::<$plugin>(doc, action)
}
fn definition(
doc: $crate::providers_bindings::exports::openidea::plugin::providers::Document,
at: $crate::bindings::openidea::plugin::types::Position,
@@ -430,12 +420,9 @@ pub mod __rt {
detail: i.detail,
kind: i.kind.map(|k| k.as_str().to_owned()),
insert_text: i.insert_text,
snippet: i.snippet,
replace: i.replace.map(crate::host::__range_to),
sort_text: i.sort_text,
filter_text: i.filter_text,
documentation: i.documentation,
additional_edits: i.additional_edits.into_iter().map(edit_out).collect(),
})
.collect()
}
@@ -456,54 +443,25 @@ pub mod __rt {
sel: b::types::Range,
) -> Vec<w::CodeAction> {
let d = doc(d);
with::<T, _>(|x| x.code_actions(&d, crate::host::__range_from(sel))).into_iter().map(action_out).collect()
}
pub fn resolve_code_action<T: Plugin + p::Providers + Default>(
d: w::Document,
action: w::CodeAction,
) -> Result<w::CodeAction, b::types::Error> {
let d = doc(d);
with::<T, _>(|x| x.resolve_code_action(&d, action_in(action))).map(action_out).map_err(error_out)
}
fn edit_out(e: crate::TextEdit) -> b::types::TextEdit {
b::types::TextEdit { range: crate::host::__range_to(e.range), text: e.text }
}
fn action_out(a: p::CodeAction) -> w::CodeAction {
w::CodeAction {
title: a.title,
kind: a.kind,
preferred: a.preferred,
edits: a
.edits
.into_iter()
.map(|(path, edits)| w::FileEdit { path, edits: edits.into_iter().map(edit_out).collect() })
.collect(),
data: a.data,
}
}
fn action_in(a: w::CodeAction) -> p::CodeAction {
p::CodeAction {
title: a.title,
kind: a.kind,
preferred: a.preferred,
edits: a
.edits
.into_iter()
.map(|f| {
let edits = f
.edits
.into_iter()
.map(|e| crate::TextEdit { range: crate::host::__range_from(e.range), text: e.text })
.collect();
(f.path, edits)
})
.collect(),
data: a.data,
}
with::<T, _>(|x| x.code_actions(&d, crate::host::__range_from(sel)))
.into_iter()
.map(|a| w::CodeAction {
title: a.title,
kind: a.kind,
preferred: a.preferred,
edits: a
.edits
.into_iter()
.map(|(path, edits)| w::FileEdit {
path,
edits: edits
.into_iter()
.map(|e| b::types::TextEdit { range: crate::host::__range_to(e.range), text: e.text })
.collect(),
})
.collect(),
})
.collect()
}
pub fn definition<T: Plugin + p::Providers + Default>(
@@ -573,9 +531,6 @@ pub mod __rt {
v::FieldValue::Multiline(t) => vt::FieldValue::Multiline(t),
v::FieldValue::Checkbox(c) => vt::FieldValue::Checkbox(c),
v::FieldValue::Choice(o, s) => vt::FieldValue::Choice((o, s)),
v::FieldValue::Other { kind, data } => {
vt::FieldValue::Other(b::types::Extension { kind, data })
}
},
})
.collect(),
@@ -588,7 +543,6 @@ pub mod __rt {
editable: t.editable,
}),
v::Body::Html(h) => vt::Body::Html(h),
v::Body::Other { kind, data } => vt::Body::Other(b::types::Extension { kind, data }),
};
vt::View {
toolbar: view
@@ -628,7 +582,6 @@ pub mod __rt {
vt::ViewEvent::InsertRow(r) => v::ViewEvent::InsertRow(r),
vt::ViewEvent::DeleteRows(r) => v::ViewEvent::DeleteRows(r),
vt::ViewEvent::Message(m) => v::ViewEvent::Message(m),
vt::ViewEvent::Other(x) => v::ViewEvent::Other { kind: x.kind, data: x.data },
};
with::<T, _>(|p| p.handle(&view, event, &ctx)).map_err(error_out)
}
+5 -2
View File
@@ -722,8 +722,11 @@ mod tests {
let out = mock.invocation().project().unwrap().run_process("java", &["-version".into()], None).unwrap();
assert_eq!(out.stdout, "-version");
assert_eq!(mock.process_runs()[0].cwd.as_deref(), Some("/project"));
host::publish_diagnostics("/project/a.java", &[Diagnostic::new(Range::default(), Severity::Warning, "m")])
.unwrap();
host::publish_diagnostics(
"/project/a.java",
&[Diagnostic { range: Range::default(), severity: Severity::Warning, message: "m".into(), code: None }],
)
.unwrap();
assert_eq!(mock.diagnostics()["/project/a.java"].len(), 1);
}
}
-18
View File
@@ -92,18 +92,12 @@ pub struct CompletionItem {
pub label: String,
pub detail: Option<String>,
pub kind: Option<CompletionKind>,
/// With `snippet`, tab stops and placeholders (`$1`, `${1:name}`, `$0`).
pub insert_text: String,
pub snippet: bool,
/// Replaced on accept; `None`: the word at the caret.
pub replace: Option<Range>,
pub sort_text: Option<String>,
/// Matched against what was typed instead of `label`.
pub filter_text: Option<String>,
/// Markdown.
pub documentation: Option<String>,
/// Applied with the item elsewhere in the document (an import).
pub additional_edits: Vec<TextEdit>,
}
impl CompletionItem {
@@ -113,12 +107,9 @@ impl CompletionItem {
detail: None,
kind: Some(kind),
insert_text: label.into(),
snippet: false,
replace: None,
sort_text: None,
filter_text: None,
documentation: None,
additional_edits: Vec::new(),
}
}
}
@@ -138,9 +129,6 @@ pub struct CodeAction {
pub preferred: bool,
/// (absolute path, edits) per file.
pub edits: Vec<(String, Vec<TextEdit>)>,
/// The plugin's own reference: with it and no `edits`, the edits come
/// from [`Providers::resolve_code_action`] once the user picks the action.
pub data: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -169,12 +157,6 @@ pub trait Providers {
Vec::new()
}
/// The edits of an action [`Providers::code_actions`] returned with
/// `data`, when the user picks it (the command budget applies, not 50 ms).
fn resolve_code_action(&mut self, _doc: &Document, action: CodeAction) -> Result<CodeAction, crate::Error> {
Ok(action)
}
fn definition(&mut self, _doc: &Document, _at: Position) -> Vec<Location> {
Vec::new()
}
+1 -87
View File
@@ -143,9 +143,7 @@ pub struct StatusItem {
pub command: Option<String>,
}
/// Kinds of workbench events; later SDKs add more.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum EventKind {
FileOpened,
FileSaved,
@@ -156,46 +154,17 @@ pub enum EventKind {
Scheduled,
}
impl EventKind {
/// The name the IDE knows it by.
pub fn name(self) -> &'static str {
match self {
Self::FileOpened => "file-opened",
Self::FileSaved => "file-saved",
Self::EditorFocused => "editor-focused",
Self::ActionPerformed => "action-performed",
Self::VcsChanged => "vcs-changed",
Self::Scheduled => "scheduled",
}
}
}
/// How the user started an action; later IDEs may report more.
/// How the user started an action.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ActionSource {
Keyboard,
Menu,
Toolbar,
ContextMenu,
Search,
/// Anything else, including sources this SDK doesn't know.
Other,
}
impl ActionSource {
pub fn parse(name: &str) -> Self {
match name {
"keyboard" => Self::Keyboard,
"menu" => Self::Menu,
"toolbar" => Self::Toolbar,
"context-menu" => Self::ContextMenu,
"search" => Self::Search,
_ => Self::Other,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ActionPerformed {
/// Action id (`editor.duplicate-line`).
@@ -208,7 +177,6 @@ pub struct ActionPerformed {
/// A workbench event (see [`crate::host::subscribe`]).
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Event {
FileOpened(String),
FileSaved(String),
@@ -218,11 +186,6 @@ pub enum Event {
VcsChanged(String),
/// The `on-schedule` period elapsed.
Scheduled,
/// A kind this SDK doesn't know: its name and JSON data.
Other {
kind: String,
data: String,
},
}
/// The repository containing a project.
@@ -264,41 +227,6 @@ pub struct Diagnostic {
pub message: String,
/// Rule or check id.
pub code: Option<String>,
/// The tool that found it, when not the plugin itself.
pub source: Option<String>,
/// Code that does nothing (shown faded).
pub unnecessary: bool,
/// Use of something deprecated (struck through).
pub deprecated: bool,
/// Other places that explain it.
pub related: Vec<RelatedInformation>,
}
impl Diagnostic {
pub fn new(range: Range, severity: Severity, message: impl Into<String>) -> Self {
Self {
range,
severity,
message: message.into(),
code: None,
source: None,
unnecessary: false,
deprecated: false,
related: Vec::new(),
}
}
pub fn code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
}
/// Another place that explains a diagnostic.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RelatedInformation {
pub location: Location,
pub message: String,
}
/// An HTTP request (see [`crate::host::fetch`]).
@@ -348,17 +276,3 @@ pub struct ProcessOutput {
pub stdout: String,
pub stderr: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn names_the_ide_uses() {
assert_eq!(EventKind::ActionPerformed.name(), "action-performed");
assert_eq!(ActionSource::parse("context-menu"), ActionSource::ContextMenu);
assert_eq!(ActionSource::parse("pen"), ActionSource::Other, "a source of a newer IDE");
let d = Diagnostic::new(Range::default(), Severity::Hint, "m").code("c");
assert_eq!((d.code.as_deref(), d.unnecessary, d.related.len()), (Some("c"), false, 0));
}
}
-20
View File
@@ -110,19 +110,12 @@ impl ListItem {
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum FieldValue {
Text(String),
Multiline(String),
Checkbox(bool),
/// Options and the chosen one.
Choice(Vec<String>, String),
/// A field type of a newer plugin API: its kind and JSON data (IDEs that
/// don't know it leave the field out).
Other {
kind: String,
data: String,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -149,7 +142,6 @@ pub struct Table {
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Body {
Message(String),
Tree(Vec<TreeItem>),
@@ -159,12 +151,6 @@ pub enum Body {
/// Shown in a sandboxed frame; its script talks to the plugin with
/// `openidea.postMessage(text)` and `openidea.onMessage(fn)`.
Html(String),
/// A body of a newer plugin API: its kind and JSON data (IDEs that don't
/// know it say they need updating).
Other {
kind: String,
data: String,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -211,7 +197,6 @@ impl Viewport {
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ViewEvent {
Button(String),
Activate(String),
@@ -228,11 +213,6 @@ pub enum ViewEvent {
InsertRow(u32),
DeleteRows(Vec<u32>),
Message(String),
/// An event this SDK doesn't know: its kind and JSON data.
Other {
kind: String,
data: String,
},
}
/// Content of the plugin's declared views and custom editors.
+27 -13
View File
@@ -2,15 +2,36 @@ package openidea:[email protected];
/// Workbench events, delivered in batches to `event-handler.handle`.
interface events {
use types.{error, extension};
use types.{error};
enum event-kind {
file-opened,
file-saved,
/// The focused editor changed.
editor-focused,
/// An IDE action ran. Requires `events.input`.
action-performed,
/// A repository's state changed (commit, checkout, fetch, …).
vcs-changed,
/// The plugin's `on-schedule` period elapsed.
scheduled,
}
/// How the user started an action.
enum action-source {
keyboard,
menu,
toolbar,
context-menu,
search,
other,
}
record action-performed {
/// Action id (`editor.duplicate-line`).
action: string,
title: string,
/// How the user started it: `keyboard`, `menu`, `toolbar`,
/// `context-menu`, `search` or `other` (more may be added).
source: string,
source: action-source,
/// Its keyboard shortcut, when it has one (`Ctrl+D`).
shortcut: option<string>,
}
@@ -22,21 +43,14 @@ interface events {
action-performed(action-performed),
vcs-changed(string),
scheduled,
/// A kind added after 1.0 (subscribed to by its name).
other(extension),
}
/// Delivers `kind` events from now on, for the rest of the session (also
/// across idle reclaim: an event activates the plugin again). Declare
/// `activation = ["on-event:<kind>"]` to get them before first use.
/// Kinds: `file-opened`, `file-saved`, `editor-focused` (the focused editor
/// changed), `vcs-changed` (a repository's state changed: commit,
/// checkout, fetch…), `action-performed` (an IDE action ran) and
/// `scheduled` (the `on-schedule` period elapsed); later IDEs add more,
/// and an IDE that doesn't know one fails with `invalid`.
/// Requires `events` (`events.input` for `action-performed`).
subscribe: func(kind: string) -> result<_, error>;
unsubscribe: func(kind: string);
subscribe: func(kind: event-kind) -> result<_, error>;
unsubscribe: func(kind: event-kind);
}
/// Long-running work with a progress indicator. While a task is open, the
+1 -12
View File
@@ -2,7 +2,7 @@ package openidea:[email protected];
/// Problems in files, shown in the editor and the Problems view.
interface diagnostics {
use types.{error, range, location};
use types.{error, range};
enum severity {
error,
@@ -11,23 +11,12 @@ interface diagnostics {
hint,
}
/// Another place that explains a diagnostic (the earlier declaration, say).
record related-information {
location: location,
message: string,
}
record diagnostic {
range: range,
severity: severity,
message: string,
/// Rule or check id (`LineLength`).
code: option<string>,
/// The tool that found it, when not the plugin itself (`checkstyle`).
source: option<string>,
/// `unnecessary` (shown faded) and `deprecated` (struck through); unknown ones are ignored.
tags: list<string>,
related: list<related-information>,
}
/// Replaces the plugin's diagnostics for the file at `path` (absolute, in
+1 -13
View File
@@ -5,7 +5,7 @@ package openidea:[email protected];
/// on the editing path: a result later than ~50 ms is dropped, a call still
/// running at 500 ms is interrupted.
interface providers {
use types.{error, position, range, text-edit, location};
use types.{position, range, text-edit, location};
/// The document a request is about.
record document {
@@ -24,17 +24,11 @@ interface providers {
/// constant, struct, event, operator or type-parameter. Unknown kinds
/// show no icon.
kind: option<string>,
/// With `snippet`, tab stops and placeholders (`$1`, `${1:name}`, `$0`).
insert-text: string,
snippet: bool,
/// Replaced on accept; `none`: the word at the caret.
replace: option<range>,
sort-text: option<string>,
/// Matched against what was typed instead of `label`.
filter-text: option<string>,
documentation: option<string>,
/// Applied with the item elsewhere in the document (an import).
additional-edits: list<text-edit>,
}
record inline-suggestion {
@@ -54,9 +48,6 @@ interface providers {
kind: option<string>,
preferred: bool,
edits: list<file-edit>,
/// The plugin's own reference: with it and no `edits`, the edits are
/// computed by `resolve-code-action` once the user picks the action.
data: option<string>,
}
record line-marker {
@@ -73,9 +64,6 @@ interface providers {
inline-completion: func(doc: document, at: position) -> option<inline-suggestion>;
/// Actions for `selection` (quick fixes for the plugin's own diagnostics).
code-actions: func(doc: document, selection: range) -> list<code-action>;
/// The edits of an action `code-actions` returned with `data`, when the
/// user picks it (the command budget applies, not the editor's 50 ms).
resolve-code-action: func(doc: document, action: code-action) -> result<code-action, error>;
definition: func(doc: document, at: position) -> list<location>;
line-markers: func(doc: document) -> list<line-marker>;
}
-9
View File
@@ -52,15 +52,6 @@ interface types {
range: range,
}
/// A case added after 1.0, carried by the `other` case of a variant that
/// grows (events, view bodies, view events, form fields): its kind and its
/// data as JSON. Whoever doesn't know the kind ignores it or shows a
/// fallback; the SDKs decode the kinds they know.
record extension {
kind: string,
data: string,
}
/// Passed to `lifecycle.activate`.
record context {
plugin-id: string,
-8
View File
@@ -8,8 +8,6 @@ package openidea:[email protected];
/// runs in the WebView, except a `html` body, which is shown in a sandboxed
/// frame that can only exchange messages with the plugin.
interface view-types {
use types.{extension};
record button {
id: string,
label: string,
@@ -45,8 +43,6 @@ interface view-types {
checkbox(bool),
/// The options and the chosen one.
choice(tuple<list<string>, string>),
/// A field type added after 1.0 (an IDE that doesn't know it leaves it out).
other(extension),
}
record field {
@@ -76,8 +72,6 @@ interface view-types {
/// network, no IDE API. Its script talks to the plugin with
/// `openidea.postMessage(text)` / `openidea.onMessage(fn)`.
html(string),
/// A body added after 1.0; an IDE that doesn't know it says it needs updating.
other(extension),
}
record view {
@@ -115,8 +109,6 @@ interface view-types {
delete-rows(list<u32>),
/// From the `html` body's script.
message(string),
/// An event added after 1.0, from a body or field added after 1.0.
other(extension),
}
}
+1 -1
View File
@@ -924,7 +924,7 @@ export function PluginsPage() {
<CategoryFilter market={market ?? []} value={category} onChange={setCategory} />
</li>
)}
{!marketUrl && !marketError && (
{!marketUrl && (
<li className={styles.notice} role="note">
<Icon name="info" size={14} />
<span>{tx`${NO_REPOSITORY}. Showing plugin packages available on this machine.`}</span>
@@ -570,21 +570,6 @@ describe("PluginsPage", () => {
expect(within(list).getAllByText("Installed").length).toBeGreaterThan(0);
expect(notes.at(-1)).toMatchObject({ title: "Installed Toy Language 0.1.0" });
});
it("shows why the marketplace couldn't be read instead of claiming there is none", async () => {
// Commands without a result type reject with the backend's error object, not an Error.
setPluginsBackend({
...createMemoryPluginsBackend([], sampleCatalog()),
marketUrl: () => Promise.reject({ kind: "unavailable", message: "Index unreachable", data: null }),
});
const user = userEvent.setup();
render(<PluginsPage />);
await user.click(screen.getByRole("tab", { name: /Available/ }));
const list = await screen.findByRole("list", { name: "Available plugins" });
expect(await within(list).findByText("Index unreachable")).toBeInTheDocument();
expect(within(list).queryByText("[object Object]")).not.toBeInTheDocument();
expect(within(list).queryByText(new RegExp(NO_REPOSITORY))).not.toBeInTheDocument();
});
});
describe("secrets", () => {
+1 -7
View File
@@ -54,13 +54,7 @@ export const usePlugins = create<PluginsState>(() => ({
marketError: null,
}));
/** A rejection's message: an `Error`, or an `IdeError` object from a command without a result type. */
export const errorText = (e: unknown): string =>
e instanceof Error
? e.message
: typeof e === "object" && e !== null && "message" in e && typeof e.message === "string"
? e.message
: String(e);
export const errorText = (e: unknown) => (e instanceof Error ? e.message : String(e));
/** Statuses whose contributions are in effect. */
export const isLive = (status: PluginStatus) => status === "active" || status === "inactive" || status === "static";
+27 -13
View File
@@ -2,15 +2,36 @@ package openidea:[email protected];
/// Workbench events, delivered in batches to `event-handler.handle`.
interface events {
use types.{error, extension};
use types.{error};
enum event-kind {
file-opened,
file-saved,
/// The focused editor changed.
editor-focused,
/// An IDE action ran. Requires `events.input`.
action-performed,
/// A repository's state changed (commit, checkout, fetch, …).
vcs-changed,
/// The plugin's `on-schedule` period elapsed.
scheduled,
}
/// How the user started an action.
enum action-source {
keyboard,
menu,
toolbar,
context-menu,
search,
other,
}
record action-performed {
/// Action id (`editor.duplicate-line`).
action: string,
title: string,
/// How the user started it: `keyboard`, `menu`, `toolbar`,
/// `context-menu`, `search` or `other` (more may be added).
source: string,
source: action-source,
/// Its keyboard shortcut, when it has one (`Ctrl+D`).
shortcut: option<string>,
}
@@ -22,21 +43,14 @@ interface events {
action-performed(action-performed),
vcs-changed(string),
scheduled,
/// A kind added after 1.0 (subscribed to by its name).
other(extension),
}
/// Delivers `kind` events from now on, for the rest of the session (also
/// across idle reclaim: an event activates the plugin again). Declare
/// `activation = ["on-event:<kind>"]` to get them before first use.
/// Kinds: `file-opened`, `file-saved`, `editor-focused` (the focused editor
/// changed), `vcs-changed` (a repository's state changed: commit,
/// checkout, fetch…), `action-performed` (an IDE action ran) and
/// `scheduled` (the `on-schedule` period elapsed); later IDEs add more,
/// and an IDE that doesn't know one fails with `invalid`.
/// Requires `events` (`events.input` for `action-performed`).
subscribe: func(kind: string) -> result<_, error>;
unsubscribe: func(kind: string);
subscribe: func(kind: event-kind) -> result<_, error>;
unsubscribe: func(kind: event-kind);
}
/// Long-running work with a progress indicator. While a task is open, the
+1 -12
View File
@@ -2,7 +2,7 @@ package openidea:[email protected];
/// Problems in files, shown in the editor and the Problems view.
interface diagnostics {
use types.{error, range, location};
use types.{error, range};
enum severity {
error,
@@ -11,23 +11,12 @@ interface diagnostics {
hint,
}
/// Another place that explains a diagnostic (the earlier declaration, say).
record related-information {
location: location,
message: string,
}
record diagnostic {
range: range,
severity: severity,
message: string,
/// Rule or check id (`LineLength`).
code: option<string>,
/// The tool that found it, when not the plugin itself (`checkstyle`).
source: option<string>,
/// `unnecessary` (shown faded) and `deprecated` (struck through); unknown ones are ignored.
tags: list<string>,
related: list<related-information>,
}
/// Replaces the plugin's diagnostics for the file at `path` (absolute, in
+1 -13
View File
@@ -5,7 +5,7 @@ package openidea:[email protected];
/// on the editing path: a result later than ~50 ms is dropped, a call still
/// running at 500 ms is interrupted.
interface providers {
use types.{error, position, range, text-edit, location};
use types.{position, range, text-edit, location};
/// The document a request is about.
record document {
@@ -24,17 +24,11 @@ interface providers {
/// constant, struct, event, operator or type-parameter. Unknown kinds
/// show no icon.
kind: option<string>,
/// With `snippet`, tab stops and placeholders (`$1`, `${1:name}`, `$0`).
insert-text: string,
snippet: bool,
/// Replaced on accept; `none`: the word at the caret.
replace: option<range>,
sort-text: option<string>,
/// Matched against what was typed instead of `label`.
filter-text: option<string>,
documentation: option<string>,
/// Applied with the item elsewhere in the document (an import).
additional-edits: list<text-edit>,
}
record inline-suggestion {
@@ -54,9 +48,6 @@ interface providers {
kind: option<string>,
preferred: bool,
edits: list<file-edit>,
/// The plugin's own reference: with it and no `edits`, the edits are
/// computed by `resolve-code-action` once the user picks the action.
data: option<string>,
}
record line-marker {
@@ -73,9 +64,6 @@ interface providers {
inline-completion: func(doc: document, at: position) -> option<inline-suggestion>;
/// Actions for `selection` (quick fixes for the plugin's own diagnostics).
code-actions: func(doc: document, selection: range) -> list<code-action>;
/// The edits of an action `code-actions` returned with `data`, when the
/// user picks it (the command budget applies, not the editor's 50 ms).
resolve-code-action: func(doc: document, action: code-action) -> result<code-action, error>;
definition: func(doc: document, at: position) -> list<location>;
line-markers: func(doc: document) -> list<line-marker>;
}
-9
View File
@@ -52,15 +52,6 @@ interface types {
range: range,
}
/// A case added after 1.0, carried by the `other` case of a variant that
/// grows (events, view bodies, view events, form fields): its kind and its
/// data as JSON. Whoever doesn't know the kind ignores it or shows a
/// fallback; the SDKs decode the kinds they know.
record extension {
kind: string,
data: string,
}
/// Passed to `lifecycle.activate`.
record context {
plugin-id: string,
-8
View File
@@ -8,8 +8,6 @@ package openidea:[email protected];
/// runs in the WebView, except a `html` body, which is shown in a sandboxed
/// frame that can only exchange messages with the plugin.
interface view-types {
use types.{extension};
record button {
id: string,
label: string,
@@ -45,8 +43,6 @@ interface view-types {
checkbox(bool),
/// The options and the chosen one.
choice(tuple<list<string>, string>),
/// A field type added after 1.0 (an IDE that doesn't know it leaves it out).
other(extension),
}
record field {
@@ -76,8 +72,6 @@ interface view-types {
/// network, no IDE API. Its script talks to the plugin with
/// `openidea.postMessage(text)` / `openidea.onMessage(fn)`.
html(string),
/// A body added after 1.0; an IDE that doesn't know it says it needs updating.
other(extension),
}
record view {
@@ -115,8 +109,6 @@ interface view-types {
delete-rows(list<u32>),
/// From the `html` body's script.
message(string),
/// An event added after 1.0, from a body or field added after 1.0.
other(extension),
}
}