289 lines
8.3 KiB
Rust
289 lines
8.3 KiB
Rust
// code_action.rs --- T M4.5 L3 LSP code actions / executeCommand.
|
|
|
|
//! `textDocument/codeAction` response state.
|
|
//!
|
|
//! The response is `(Command | CodeAction)[] | null`. The two shapes
|
|
//! are normalised into one [`CodeActionItem`]:
|
|
//!
|
|
//! * a bare `Command` becomes an item with no `edit` and its
|
|
//! `command` populated;
|
|
//! * a `CodeAction` keeps its `title`/`kind`, an optional inline
|
|
//! [`WorkspaceEditResponse`] (`edit`), and an optional `command`
|
|
//! to run after (or instead of) the edit.
|
|
//!
|
|
//! Applying the chosen item is Lua policy (same division as
|
|
//! [`crate::rename`] / [`crate::formatting`]): an inline `edit` is fed
|
|
//! to the shared `WorkspaceEdit` applier; a `command` is dispatched via
|
|
//! `workspace/executeCommand`, after which the server typically drives
|
|
//! the real change with a server→client `workspace/applyEdit` request
|
|
//! (handled by the Lua event pump). Nothing here mutates the editor.
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use serde_json::Value;
|
|
|
|
use crate::rename::WorkspaceEditResponse;
|
|
|
|
/// A server command to run via `workspace/executeCommand`.
|
|
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
|
pub struct CommandRef {
|
|
/// Human-readable title.
|
|
pub title: String,
|
|
/// The command identifier the server registered.
|
|
pub command: String,
|
|
/// Opaque arguments, passed through verbatim.
|
|
pub arguments: Vec<Value>,
|
|
}
|
|
|
|
/// One normalised code action.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct CodeActionItem {
|
|
/// Display title.
|
|
pub title: String,
|
|
/// LSP `CodeActionKind` (e.g. `quickfix`, `refactor`), if any.
|
|
pub kind: Option<String>,
|
|
/// Inline workspace edit, if the action carries one. Empty when
|
|
/// the action is command-only.
|
|
pub edit: WorkspaceEditResponse,
|
|
/// Command to dispatch, if any.
|
|
pub command: Option<CommandRef>,
|
|
}
|
|
|
|
impl CodeActionItem {
|
|
/// True iff the action carries an inline edit.
|
|
#[must_use]
|
|
pub fn has_edit(&self) -> bool {
|
|
!self.edit.is_empty()
|
|
}
|
|
}
|
|
|
|
/// Parsed `textDocument/codeAction` response.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct CodeActionResponse {
|
|
/// Actions in server order.
|
|
pub actions: Vec<CodeActionItem>,
|
|
}
|
|
|
|
impl CodeActionResponse {
|
|
/// Parse `(Command | CodeAction)[] | null`.
|
|
#[must_use]
|
|
pub fn from_lsp_value(v: &Value) -> Self {
|
|
let Some(arr) = v.as_array() else {
|
|
return Self::default();
|
|
};
|
|
let mut actions = Vec::with_capacity(arr.len());
|
|
for item in arr {
|
|
if let Some(a) = parse_item(item) {
|
|
actions.push(a);
|
|
}
|
|
}
|
|
Self { actions }
|
|
}
|
|
|
|
/// True iff the server offered no actions.
|
|
#[must_use]
|
|
pub fn is_empty(&self) -> bool {
|
|
self.actions.is_empty()
|
|
}
|
|
}
|
|
|
|
fn parse_command(v: &Value) -> Option<CommandRef> {
|
|
// A `Command` always has a string `command`. `arguments` is
|
|
// optional; `title` defaults to the command id when absent.
|
|
let command = v.get("command")?.as_str()?.to_owned();
|
|
let title = v
|
|
.get("title")
|
|
.and_then(Value::as_str)
|
|
.unwrap_or(&command)
|
|
.to_owned();
|
|
let arguments = v
|
|
.get("arguments")
|
|
.and_then(Value::as_array)
|
|
.cloned()
|
|
.unwrap_or_default();
|
|
Some(CommandRef {
|
|
title,
|
|
command,
|
|
arguments,
|
|
})
|
|
}
|
|
|
|
fn parse_item(v: &Value) -> Option<CodeActionItem> {
|
|
// Disambiguate `Command` from `CodeAction`: a `Command`'s
|
|
// `command` is a string; a `CodeAction`'s `command` (when present)
|
|
// is a nested `Command` object. So a top-level *string* `command`
|
|
// means the whole entry is a bare Command.
|
|
if v.get("command").and_then(Value::as_str).is_some() {
|
|
let cmd = parse_command(v)?;
|
|
return Some(CodeActionItem {
|
|
title: cmd.title.clone(),
|
|
kind: None,
|
|
edit: WorkspaceEditResponse::default(),
|
|
command: Some(cmd),
|
|
});
|
|
}
|
|
// Otherwise a CodeAction. `title` is required by spec; tolerate a
|
|
// missing one rather than dropping the action.
|
|
let title = v
|
|
.get("title")
|
|
.and_then(Value::as_str)
|
|
.unwrap_or("")
|
|
.to_owned();
|
|
let kind = v.get("kind").and_then(Value::as_str).map(str::to_owned);
|
|
let edit = v
|
|
.get("edit")
|
|
.map(WorkspaceEditResponse::from_lsp_value)
|
|
.unwrap_or_default();
|
|
let command = v.get("command").and_then(parse_command);
|
|
Some(CodeActionItem {
|
|
title,
|
|
kind,
|
|
edit,
|
|
command,
|
|
})
|
|
}
|
|
|
|
/// Per-server, per-uri code-action state.
|
|
#[derive(Default)]
|
|
pub struct CodeActionStore {
|
|
by_key: HashMap<CodeActionKey, CodeActionResponse>,
|
|
}
|
|
|
|
/// Key into [`CodeActionStore`].
|
|
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
|
|
pub struct CodeActionKey {
|
|
/// Decimal LSP server id.
|
|
pub server: String,
|
|
/// Document URI the request was made on.
|
|
pub uri: String,
|
|
}
|
|
|
|
impl CodeActionKey {
|
|
/// Construct a key.
|
|
#[must_use]
|
|
pub fn new(server: impl Into<String>, uri: impl Into<String>) -> Self {
|
|
Self {
|
|
server: server.into(),
|
|
uri: uri.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CodeActionStore {
|
|
/// Empty store.
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Replace the response at `key`.
|
|
pub fn set(&mut self, key: CodeActionKey, response: CodeActionResponse) {
|
|
self.by_key.insert(key, response);
|
|
}
|
|
|
|
/// Drop the entry at `key`.
|
|
pub fn clear(&mut self, key: &CodeActionKey) {
|
|
self.by_key.remove(key);
|
|
}
|
|
|
|
/// Look up the entry at `key`.
|
|
#[must_use]
|
|
pub fn get(&self, key: &CodeActionKey) -> Option<&CodeActionResponse> {
|
|
self.by_key.get(key)
|
|
}
|
|
}
|
|
|
|
/// Cheaply-cloneable shared handle.
|
|
pub type SharedCodeActionStore = Arc<Mutex<CodeActionStore>>;
|
|
|
|
/// Build a fresh shared store.
|
|
#[must_use]
|
|
pub fn make_shared_store() -> SharedCodeActionStore {
|
|
Arc::new(Mutex::new(CodeActionStore::new()))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn parses_bare_command() {
|
|
let v = json!([{
|
|
"title": "Run me",
|
|
"command": "pmacs.do",
|
|
"arguments": [1, "x"]
|
|
}]);
|
|
let r = CodeActionResponse::from_lsp_value(&v);
|
|
assert_eq!(r.actions.len(), 1);
|
|
let a = &r.actions[0];
|
|
assert_eq!(a.title, "Run me");
|
|
assert!(!a.has_edit());
|
|
let c = a.command.as_ref().unwrap();
|
|
assert_eq!(c.command, "pmacs.do");
|
|
assert_eq!(c.arguments.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_code_action_with_inline_edit() {
|
|
let v = json!([{
|
|
"title": "Fix it",
|
|
"kind": "quickfix",
|
|
"edit": {
|
|
"changes": {
|
|
"file:///a.rs": [{
|
|
"range": {
|
|
"start": { "line": 0, "character": 0 },
|
|
"end": { "line": 0, "character": 3 }
|
|
},
|
|
"newText": "ok"
|
|
}]
|
|
}
|
|
}
|
|
}]);
|
|
let r = CodeActionResponse::from_lsp_value(&v);
|
|
let a = &r.actions[0];
|
|
assert_eq!(a.kind.as_deref(), Some("quickfix"));
|
|
assert!(a.has_edit());
|
|
assert_eq!(a.edit.files()[0].uri, "file:///a.rs");
|
|
assert!(a.command.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn parses_code_action_with_nested_command() {
|
|
let v = json!([{
|
|
"title": "Refactor",
|
|
"kind": "refactor",
|
|
"command": { "title": "Apply", "command": "srv.apply", "arguments": [] }
|
|
}]);
|
|
let r = CodeActionResponse::from_lsp_value(&v);
|
|
let a = &r.actions[0];
|
|
assert!(!a.has_edit());
|
|
assert_eq!(a.command.as_ref().unwrap().command, "srv.apply");
|
|
}
|
|
|
|
#[test]
|
|
fn null_response_is_empty() {
|
|
assert!(CodeActionResponse::from_lsp_value(&Value::Null).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn store_set_get_clear() {
|
|
let mut s = CodeActionStore::new();
|
|
let key = CodeActionKey::new("1", "file:///a");
|
|
s.set(
|
|
key.clone(),
|
|
CodeActionResponse {
|
|
actions: vec![CodeActionItem {
|
|
title: "t".into(),
|
|
..Default::default()
|
|
}],
|
|
},
|
|
);
|
|
assert_eq!(s.get(&key).unwrap().actions.len(), 1);
|
|
s.clear(&key);
|
|
assert!(s.get(&key).is_none());
|
|
}
|
|
}
|