mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
feat(plugins): leave room in plugin API 1.0 for what will grow
Released 1.x types never change, so the sets and records that will have to grow are opened up before the API is frozen: - Event kinds and action sources are strings; `event`, view `body`, `view-event` and `field-value` get an `other(extension)` case (a kind and JSON data) that carries later cases to older plugins and IDEs. An IDE refuses an event kind it doesn't know, says a view body needs a newer IDE, and leaves out unknown form fields. - Completion items can be snippets, match on `filter-text` and bring `additional-edits` (imports). Code actions can carry `data` instead of edits; the new `resolve-code-action` export computes them when the user picks the action, under the command budget, through the existing resolve path. - Diagnostics name their `source`, carry `tags` (`unnecessary`, `deprecated`) and `related` locations. - The SDK's enums are `#[non_exhaustive]` with `Other` variants; `Diagnostic::new`, `Providers::resolve_code_action`. - README: 1.x calls stay synchronous and pass documents whole; async and incremental sync come later behind the host adapter or a new interface. Official plugins and fixtures rebuilt.
This commit is contained in:
@@ -76,9 +76,17 @@ 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 (resolve and commands don't apply).
|
||||
/// Items from plugins carry no server: commands don't apply, and a code
|
||||
/// action is resolved by the plugin named in its `data` ([`PluginActionRef`]).
|
||||
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 {
|
||||
@@ -120,14 +128,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: None,
|
||||
filter_text: item.filter_text,
|
||||
preselect: false,
|
||||
deprecated: false,
|
||||
insert_text: item.insert_text,
|
||||
is_snippet: false,
|
||||
is_snippet: item.snippet,
|
||||
replace_range: item.replace,
|
||||
replace_range_tab: None,
|
||||
additional_edits: Vec::new(),
|
||||
additional_edits: item.additional_edits,
|
||||
commit_characters: Vec::new(),
|
||||
command: None,
|
||||
data: None,
|
||||
@@ -314,7 +322,7 @@ pub async fn lsp_code_actions(
|
||||
};
|
||||
let plugin_actions: Vec<CodeAction> = plugin_actions
|
||||
.into_iter()
|
||||
.map(|(_, a)| plugin_code_action(a))
|
||||
.map(|(plugin, a)| plugin_code_action(&plugin, 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}."))))
|
||||
@@ -330,24 +338,53 @@ pub async fn lsp_code_actions(
|
||||
prepare_actions(project, doc, version, actions).await
|
||||
}
|
||||
|
||||
fn plugin_code_action(a: ide_plugin::CodeAction) -> CodeAction {
|
||||
let files = a
|
||||
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
|
||||
.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: Some(WorkspaceEdit { files, preconditions: Vec::new() }),
|
||||
edit: (!lazy).then(|| WorkspaceEdit { files, preconditions: Vec::new() }),
|
||||
command: None,
|
||||
data: None,
|
||||
data,
|
||||
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,
|
||||
@@ -381,7 +418,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 = project.lsp.resolve_code_action(doc, action).await?;
|
||||
let action = resolve_action(host, window, doc, action).await?;
|
||||
Ok(prepare_actions(project, doc, version, vec![action]).await?.remove(0))
|
||||
}
|
||||
|
||||
@@ -391,7 +428,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 = project.lsp.resolve_code_action(doc, action).await?;
|
||||
let action = resolve_action(host, window, doc, action).await?;
|
||||
prepare_actions(project.clone(), doc, version, vec![action]).await?.remove(0)
|
||||
} else {
|
||||
action
|
||||
@@ -801,6 +838,25 @@ 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");
|
||||
|
||||
@@ -292,6 +292,21 @@ 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| {
|
||||
|
||||
@@ -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(), 8, "contributions stay");
|
||||
assert_eq!(f.command_titles("misbehave").len(), 10, "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,3 +2410,56 @@ 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")));
|
||||
}
|
||||
|
||||
@@ -467,7 +467,6 @@ 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),
|
||||
@@ -477,14 +476,7 @@ 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: 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,
|
||||
},
|
||||
source: a.source.name().to_owned(),
|
||||
shortcut: a.shortcut,
|
||||
}),
|
||||
}
|
||||
@@ -670,9 +662,17 @@ 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,27 +699,20 @@ 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(|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())
|
||||
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()))
|
||||
}
|
||||
|
||||
pub fn provide_definition(
|
||||
@@ -968,6 +961,47 @@ 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))
|
||||
}
|
||||
@@ -975,6 +1009,9 @@ 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()
|
||||
@@ -1006,10 +1043,8 @@ fn view_from_wit(v: wit_vt::View) -> View {
|
||||
fields
|
||||
.into_iter()
|
||||
.take(MAX_VIEW_ITEMS)
|
||||
.map(|f| ViewField {
|
||||
key: clip(f.key, 128),
|
||||
label: clip(f.label, MAX_TITLE),
|
||||
value: match f.value {
|
||||
.filter_map(|f| {
|
||||
let 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),
|
||||
@@ -1017,7 +1052,10 @@ 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(),
|
||||
),
|
||||
@@ -1067,3 +1105,26 @@ 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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,20 @@ 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 {
|
||||
@@ -348,9 +362,14 @@ 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)]
|
||||
@@ -365,6 +384,8 @@ 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)]
|
||||
|
||||
@@ -440,20 +440,11 @@ 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: events::EventKind) -> Fallible<()> {
|
||||
let kind = kind_from_wit(kind);
|
||||
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"))));
|
||||
};
|
||||
if let Some(cap) = kind.capability()
|
||||
&& let Err(e) = self.require(cap)
|
||||
{
|
||||
@@ -463,8 +454,10 @@ impl events::Host for StoreData {
|
||||
Ok(Ok(()))
|
||||
}
|
||||
|
||||
fn unsubscribe(&mut self, kind: events::EventKind) -> wasmtime::Result<()> {
|
||||
self.host.subscribe(kind_from_wit(kind), false);
|
||||
fn unsubscribe(&mut self, kind: String) -> wasmtime::Result<()> {
|
||||
if let Some(kind) = EventKind::parse(&kind) {
|
||||
self.host.subscribe(kind, false);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -542,10 +535,10 @@ impl diagnostics::Host for StoreData {
|
||||
diagnostics::Severity::Hint => ide_core::Severity::Hint,
|
||||
},
|
||||
message: clip(d.message, 4096),
|
||||
source: Some(source.clone()),
|
||||
source: Some(d.source.map_or_else(|| source.clone(), |s| clip(s, 128))),
|
||||
code: d.code.map(|c| clip(c, 128)),
|
||||
unnecessary: false,
|
||||
deprecated: false,
|
||||
unnecessary: d.tags.iter().any(|t| t == "unnecessary"),
|
||||
deprecated: d.tags.iter().any(|t| t == "deprecated"),
|
||||
server: Some(source.clone()),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
86dfe70a30ab1f7955a16e96c1b32c8be663456b871cc4d3bed7dc6052c062da
|
||||
c9acbc021f3840649823cb7680e13907d429c6fda8271fdb0ae6e5ea89ed15ee
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
0dacb82ac35c1f132dc66e85dbf0663b729f818279d3b32cd06403e502fbf880
|
||||
2fb6aa879562733bf5b7f11dabe8fc6a415c9ef695868fe7845417a4245d2dc8
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
0ad2e4c6977a576b7eb27fc1e535fd056dd9da64bdfe6cfda9ca9f57537d0384
|
||||
25b0f419a68a3b1651d3c2774d834b05e40ede52bc5ed8c0eba6d27a204efe82
|
||||
|
||||
@@ -40,3 +40,11 @@ title = "insert"
|
||||
[[contributes.commands]]
|
||||
id = "cancel"
|
||||
title = "cancel"
|
||||
|
||||
[[contributes.commands]]
|
||||
id = "subscribe"
|
||||
title = "subscribe"
|
||||
|
||||
[[contributes.commands]]
|
||||
id = "diagnose"
|
||||
title = "diagnose"
|
||||
|
||||
+52
-4
@@ -14,7 +14,9 @@ wit_bindgen::generate!({
|
||||
generate_all,
|
||||
});
|
||||
|
||||
use exports::openidea::plugin::providers::{self, CodeAction, CompletionItem, Document, InlineSuggestion, LineMarker};
|
||||
use exports::openidea::plugin::providers::{
|
||||
self, CodeAction, CompletionItem, Document, FileEdit, 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};
|
||||
@@ -67,6 +69,23 @@ 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}"))),
|
||||
}
|
||||
}
|
||||
@@ -113,14 +132,24 @@ 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: doc.path,
|
||||
insert_text: if rich { "f(${1:x})$0".into() } else { doc.path },
|
||||
snippet: rich,
|
||||
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()
|
||||
},
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -128,8 +157,27 @@ impl providers::Guest for Misbehave {
|
||||
None
|
||||
}
|
||||
|
||||
fn code_actions(_doc: Document, _selection: types::Range) -> Vec<CodeAction> {
|
||||
Vec::new()
|
||||
/// `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 definition(_doc: Document, _at: types::Position) -> Vec<types::Location> {
|
||||
|
||||
+25
-2
@@ -321,6 +321,11 @@ 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
|
||||
@@ -338,7 +343,14 @@ 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.
|
||||
`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).
|
||||
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.
|
||||
@@ -393,6 +405,9 @@ 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
|
||||
@@ -890,7 +905,10 @@ 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) are strings for this reason.
|
||||
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.
|
||||
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
|
||||
@@ -902,6 +920,11 @@ 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:
|
||||
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
aa09a92c0ba9d85a01c56bbf4455e40ae44f005b443ce1002bbe8011408ff975
|
||||
28e4bf10a41bdda011111b0e150ebe96205c57f410bad98c19eea3f1286349d6
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
908c2fd177ad2bb9a8a8492505151c3b81579191156c5ec53f705127f50a8ee9
|
||||
7e015486cc916f5edbac542863373fd7204f5bcd534f6c408d5f9d2b72913862
|
||||
|
||||
@@ -97,12 +97,8 @@ 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 };
|
||||
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()),
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
/// The project containing `path`.
|
||||
@@ -188,6 +184,7 @@ impl Providers for Checkstyle {
|
||||
kind: Some("quickfix".into()),
|
||||
preferred: true,
|
||||
edits: vec![(doc.path.clone(), vec![edit])],
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
64270fa0f5b3084c78764f09b8ce161742fdbff9c28fb90590da39594e558db1
|
||||
a6cf59e74595e13908e2cbf1752c58665c6963de258413c9d7332d2a7dd9cbd0
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
22876f842f727a2b9aa292986df3393f5b5f30c5c34319f5665f7d2701c8accf
|
||||
b8dde206a74b222b93e61056d4cbe403617ae240d142e01e91420a2aacdb2da4
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
14c60782eca79a25cf2c38d58c05be0eb97c4afc607d4cc83302d1445d094aab
|
||||
a681c2abd4a65d6aeb62efd514e9b170f4fca52b0c14d2d380a3b04be0a3fbc6
|
||||
|
||||
@@ -52,12 +52,9 @@ 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);
|
||||
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()),
|
||||
}
|
||||
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")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
7f5aacde2b8a8bcee5365c8cbda91045195688899c6537dc2159037cca74a672
|
||||
3c83bc9559df54457e19e650cdd21bab9d609e22daaa1fc4f596910b70834a34
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
45a7ee993b5c234b0d25e5b05ef60e714fa3edf63d6bd0d6a29b13587c8d09ce
|
||||
63f1bbdf6eca5a4642cd3453ae384ff3f876de0373c12008b757a63852164b71
|
||||
|
||||
@@ -129,12 +129,8 @@ 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 {
|
||||
range: Range::new(Position::new(line, 0), Position::new(line, len)),
|
||||
severity: Severity::Info,
|
||||
message: "No license header".into(),
|
||||
code: Some(CODE.into()),
|
||||
}
|
||||
Diagnostic::new(Range::new(Position::new(line, 0), Position::new(line, len)), Severity::Info, "No license header")
|
||||
.code(CODE)
|
||||
}
|
||||
|
||||
/// Reports `path` when it takes a header and has none, clears it otherwise.
|
||||
@@ -274,6 +270,7 @@ impl Providers for LicenseHeader {
|
||||
kind: Some("quickfix".into()),
|
||||
preferred: true,
|
||||
edits: vec![(doc.path.clone(), vec![edit])],
|
||||
data: None,
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
d5cf81455ae8b2b0fd6d1402d8775913bd7999db62b9bdf87675890ce52b1e80
|
||||
0db5cb24c358651d0e728ab3efb52286290c1a38ec379435225c935d4165ad6a
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
227c8150c6ad7a5a20236764f149c876cbbc3b082763bf2e7fc05de99726b740
|
||||
257478b050ea360c55e904f2a89c6069293139ff0d97ba860de3f29dac927787
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
bbadcaa7a86a3e0a3f8be6d86aed2229d5cdfd128fda3435b4d64c7e19ad1df2
|
||||
21e7510d6a3712bf0f21d53a5c8d652e3c2787d89a1fc25998cde96efe124207
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
09cc92e8e731f8f59f3857a71f9a30cc3d9e04a1c32092920f573b0f26592855
|
||||
04844bfbfbcb1770f4f4742658944c592922b037b2387e9340de58ef0d9baacc
|
||||
|
||||
@@ -152,24 +152,12 @@ 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(kind(k)).map_err(error)
|
||||
wit::events::subscribe(k.name()).map_err(error)
|
||||
}
|
||||
|
||||
pub fn unsubscribe(k: EventKind) {
|
||||
wit::events::unsubscribe(kind(k))
|
||||
wit::events::unsubscribe(k.name())
|
||||
}
|
||||
|
||||
pub fn begin_task(title: &str, cancellable: bool) -> TaskRes {
|
||||
@@ -189,7 +177,7 @@ mod imp {
|
||||
}
|
||||
|
||||
pub fn publish_diagnostics(path: &str, items: &[Diagnostic]) -> Result<(), Error> {
|
||||
use wit::diagnostics::{Diagnostic as D, Severity as S};
|
||||
use wit::diagnostics::{Diagnostic as D, RelatedInformation as R, Severity as S};
|
||||
let items: Vec<D> = items
|
||||
.iter()
|
||||
.map(|d| D {
|
||||
@@ -202,6 +190,23 @@ 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)
|
||||
@@ -264,7 +269,7 @@ mod imp {
|
||||
}
|
||||
|
||||
pub fn event(e: wit::events::Event) -> Event {
|
||||
use wit::events::{ActionSource as S, Event as E};
|
||||
use wit::events::Event as E;
|
||||
match e {
|
||||
E::FileOpened(p) => Event::FileOpened(p),
|
||||
E::FileSaved(p) => Event::FileSaved(p),
|
||||
@@ -274,16 +279,10 @@ mod imp {
|
||||
E::ActionPerformed(a) => Event::ActionPerformed(ActionPerformed {
|
||||
action: a.action,
|
||||
title: a.title,
|
||||
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,
|
||||
},
|
||||
source: ActionSource::parse(&a.source),
|
||||
shortcut: a.shortcut,
|
||||
}),
|
||||
E::Other(x) => Event::Other { kind: x.kind, data: x.data },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,6 +255,16 @@ 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,
|
||||
@@ -420,9 +430,12 @@ 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()
|
||||
}
|
||||
@@ -443,25 +456,54 @@ 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(|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()
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn definition<T: Plugin + p::Providers + Default>(
|
||||
@@ -531,6 +573,9 @@ 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(),
|
||||
@@ -543,6 +588,7 @@ 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
|
||||
@@ -582,6 +628,7 @@ 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)
|
||||
}
|
||||
|
||||
@@ -722,11 +722,8 @@ 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 { range: Range::default(), severity: Severity::Warning, message: "m".into(), code: None }],
|
||||
)
|
||||
.unwrap();
|
||||
host::publish_diagnostics("/project/a.java", &[Diagnostic::new(Range::default(), Severity::Warning, "m")])
|
||||
.unwrap();
|
||||
assert_eq!(mock.diagnostics()["/project/a.java"].len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,12 +92,18 @@ 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 {
|
||||
@@ -107,9 +113,12 @@ 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,6 +138,9 @@ 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)]
|
||||
@@ -157,6 +169,12 @@ 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()
|
||||
}
|
||||
|
||||
@@ -143,7 +143,9 @@ 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,
|
||||
@@ -154,17 +156,46 @@ pub enum EventKind {
|
||||
Scheduled,
|
||||
}
|
||||
|
||||
/// How the user started an action.
|
||||
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.
|
||||
#[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`).
|
||||
@@ -177,6 +208,7 @@ pub struct ActionPerformed {
|
||||
|
||||
/// A workbench event (see [`crate::host::subscribe`]).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Event {
|
||||
FileOpened(String),
|
||||
FileSaved(String),
|
||||
@@ -186,6 +218,11 @@ 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.
|
||||
@@ -227,6 +264,41 @@ 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`]).
|
||||
@@ -276,3 +348,17 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,12 +110,19 @@ 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)]
|
||||
@@ -142,6 +149,7 @@ pub struct Table {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Body {
|
||||
Message(String),
|
||||
Tree(Vec<TreeItem>),
|
||||
@@ -151,6 +159,12 @@ 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)]
|
||||
@@ -197,6 +211,7 @@ impl Viewport {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum ViewEvent {
|
||||
Button(String),
|
||||
Activate(String),
|
||||
@@ -213,6 +228,11 @@ 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.
|
||||
|
||||
@@ -2,36 +2,15 @@ package openidea:plugin@1.0.0;
|
||||
|
||||
/// Workbench events, delivered in batches to `event-handler.handle`.
|
||||
interface events {
|
||||
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,
|
||||
}
|
||||
use types.{error, extension};
|
||||
|
||||
record action-performed {
|
||||
/// Action id (`editor.duplicate-line`).
|
||||
action: string,
|
||||
title: string,
|
||||
source: action-source,
|
||||
/// How the user started it: `keyboard`, `menu`, `toolbar`,
|
||||
/// `context-menu`, `search` or `other` (more may be added).
|
||||
source: string,
|
||||
/// Its keyboard shortcut, when it has one (`Ctrl+D`).
|
||||
shortcut: option<string>,
|
||||
}
|
||||
@@ -43,14 +22,21 @@ 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: event-kind) -> result<_, error>;
|
||||
unsubscribe: func(kind: event-kind);
|
||||
subscribe: func(kind: string) -> result<_, error>;
|
||||
unsubscribe: func(kind: string);
|
||||
}
|
||||
|
||||
/// Long-running work with a progress indicator. While a task is open, the
|
||||
|
||||
@@ -2,7 +2,7 @@ package openidea:plugin@1.0.0;
|
||||
|
||||
/// Problems in files, shown in the editor and the Problems view.
|
||||
interface diagnostics {
|
||||
use types.{error, range};
|
||||
use types.{error, range, location};
|
||||
|
||||
enum severity {
|
||||
error,
|
||||
@@ -11,12 +11,23 @@ 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
|
||||
|
||||
@@ -5,7 +5,7 @@ package openidea:plugin@1.0.0;
|
||||
/// 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.{position, range, text-edit, location};
|
||||
use types.{error, position, range, text-edit, location};
|
||||
|
||||
/// The document a request is about.
|
||||
record document {
|
||||
@@ -24,11 +24,17 @@ 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 {
|
||||
@@ -48,6 +54,9 @@ 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 {
|
||||
@@ -64,6 +73,9 @@ 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>;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,15 @@ 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,6 +8,8 @@ package openidea:plugin@1.0.0;
|
||||
/// 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,
|
||||
@@ -43,6 +45,8 @@ 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 {
|
||||
@@ -72,6 +76,8 @@ 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 {
|
||||
@@ -109,6 +115,8 @@ 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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-27
@@ -2,36 +2,15 @@ package openidea:plugin@1.0.0;
|
||||
|
||||
/// Workbench events, delivered in batches to `event-handler.handle`.
|
||||
interface events {
|
||||
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,
|
||||
}
|
||||
use types.{error, extension};
|
||||
|
||||
record action-performed {
|
||||
/// Action id (`editor.duplicate-line`).
|
||||
action: string,
|
||||
title: string,
|
||||
source: action-source,
|
||||
/// How the user started it: `keyboard`, `menu`, `toolbar`,
|
||||
/// `context-menu`, `search` or `other` (more may be added).
|
||||
source: string,
|
||||
/// Its keyboard shortcut, when it has one (`Ctrl+D`).
|
||||
shortcut: option<string>,
|
||||
}
|
||||
@@ -43,14 +22,21 @@ 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: event-kind) -> result<_, error>;
|
||||
unsubscribe: func(kind: event-kind);
|
||||
subscribe: func(kind: string) -> result<_, error>;
|
||||
unsubscribe: func(kind: string);
|
||||
}
|
||||
|
||||
/// Long-running work with a progress indicator. While a task is open, the
|
||||
|
||||
+12
-1
@@ -2,7 +2,7 @@ package openidea:plugin@1.0.0;
|
||||
|
||||
/// Problems in files, shown in the editor and the Problems view.
|
||||
interface diagnostics {
|
||||
use types.{error, range};
|
||||
use types.{error, range, location};
|
||||
|
||||
enum severity {
|
||||
error,
|
||||
@@ -11,12 +11,23 @@ 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
|
||||
|
||||
+13
-1
@@ -5,7 +5,7 @@ package openidea:plugin@1.0.0;
|
||||
/// 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.{position, range, text-edit, location};
|
||||
use types.{error, position, range, text-edit, location};
|
||||
|
||||
/// The document a request is about.
|
||||
record document {
|
||||
@@ -24,11 +24,17 @@ 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 {
|
||||
@@ -48,6 +54,9 @@ 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 {
|
||||
@@ -64,6 +73,9 @@ 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>;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,15 @@ 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,6 +8,8 @@ package openidea:plugin@1.0.0;
|
||||
/// 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,
|
||||
@@ -43,6 +45,8 @@ 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 {
|
||||
@@ -72,6 +76,8 @@ 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 {
|
||||
@@ -109,6 +115,8 @@ 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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user