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 (#1245)
Closes #1237
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