mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-25 20:43:21 +00:00
Apply code actions
This commit is contained in:
parent
25aebb5225
commit
be742a5877
12 changed files with 247 additions and 118 deletions
|
@ -10,7 +10,8 @@
|
|||
"vscode": "^1.25.0"
|
||||
},
|
||||
"scripts": {
|
||||
"compile": "tsc -p ./",
|
||||
"vscode:prepublish": "tsc -p ./",
|
||||
"compile": "tsc -watch -p ./",
|
||||
"postinstall": "node ./node_modules/vscode/bin/install"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use {TextRange, TextUnit};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Edit {
|
||||
pub atoms: Vec<AtomEdit>,
|
||||
atoms: Vec<AtomEdit>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AtomEdit {
|
||||
pub delete: TextRange,
|
||||
pub insert: String,
|
||||
|
@ -22,7 +22,6 @@ impl EditBuilder {
|
|||
}
|
||||
|
||||
pub fn replace(&mut self, range: TextRange, replacement: String) {
|
||||
let range = self.translate(range);
|
||||
self.atoms.push(AtomEdit { delete: range, insert: replacement })
|
||||
}
|
||||
|
||||
|
@ -35,59 +34,47 @@ impl EditBuilder {
|
|||
}
|
||||
|
||||
pub fn finish(self) -> Edit {
|
||||
Edit { atoms: self.atoms }
|
||||
let mut atoms = self.atoms;
|
||||
atoms.sort_by_key(|a| a.delete.start());
|
||||
for (a1, a2) in atoms.iter().zip(atoms.iter().skip(1)) {
|
||||
assert!(a1.end() <= a2.start())
|
||||
}
|
||||
|
||||
fn translate(&self, range: TextRange) -> TextRange {
|
||||
let mut range = range;
|
||||
for atom in self.atoms.iter() {
|
||||
range = atom.apply_to_range(range)
|
||||
.expect("conflicting edits");
|
||||
}
|
||||
range
|
||||
Edit { atoms }
|
||||
}
|
||||
}
|
||||
|
||||
impl Edit {
|
||||
pub fn apply(&self, text: &str) -> String {
|
||||
let mut text = text.to_owned();
|
||||
for atom in self.atoms.iter() {
|
||||
text = atom.apply(&text);
|
||||
pub fn into_atoms(self) -> Vec<AtomEdit> {
|
||||
self.atoms
|
||||
}
|
||||
text
|
||||
|
||||
pub fn apply(&self, text: &str) -> String {
|
||||
let mut total_len = text.len();
|
||||
for atom in self.atoms.iter() {
|
||||
total_len += atom.insert.len();
|
||||
total_len -= atom.end() - atom.start();
|
||||
}
|
||||
let mut buf = String::with_capacity(total_len);
|
||||
let mut prev = 0;
|
||||
for atom in self.atoms.iter() {
|
||||
if atom.start() > prev {
|
||||
buf.push_str(&text[prev..atom.start()]);
|
||||
}
|
||||
buf.push_str(&atom.insert);
|
||||
prev = atom.end();
|
||||
}
|
||||
buf.push_str(&text[prev..text.len()]);
|
||||
assert_eq!(buf.len(), total_len);
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
impl AtomEdit {
|
||||
fn apply(&self, text: &str) -> String {
|
||||
let prefix = &text[
|
||||
TextRange::from_to(0.into(), self.delete.start())
|
||||
];
|
||||
let suffix = &text[
|
||||
TextRange::from_to(self.delete.end(), TextUnit::of_str(text))
|
||||
];
|
||||
let mut res = String::with_capacity(prefix.len() + self.insert.len() + suffix.len());
|
||||
res.push_str(prefix);
|
||||
res.push_str(&self.insert);
|
||||
res.push_str(suffix);
|
||||
res
|
||||
fn start(&self) -> usize {
|
||||
u32::from(self.delete.start()) as usize
|
||||
}
|
||||
|
||||
fn apply_to_position(&self, pos: TextUnit) -> Option<TextUnit> {
|
||||
if pos <= self.delete.start() {
|
||||
return Some(pos);
|
||||
}
|
||||
if pos < self.delete.end() {
|
||||
return None;
|
||||
}
|
||||
Some(pos - self.delete.len() + TextUnit::of_str(&self.insert))
|
||||
}
|
||||
|
||||
fn apply_to_range(&self, range: TextRange) -> Option<TextRange> {
|
||||
Some(TextRange::from_to(
|
||||
self.apply_to_position(range.start())?,
|
||||
self.apply_to_position(range.end())?,
|
||||
))
|
||||
fn end(&self) -> usize {
|
||||
u32::from(self.delete.end()) as usize
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ pub use self::{
|
|||
line_index::{LineIndex, LineCol},
|
||||
extend_selection::extend_selection,
|
||||
symbols::{FileSymbol, file_symbols},
|
||||
edit::{EditBuilder, Edit},
|
||||
edit::{EditBuilder, Edit, AtomEdit},
|
||||
code_actions::{flip_comma},
|
||||
};
|
||||
|
||||
|
|
|
@ -11,10 +11,11 @@ serde_derive = "1.0.71"
|
|||
drop_bomb = "0.1.0"
|
||||
crossbeam-channel = "0.2.4"
|
||||
threadpool = "1.7.1"
|
||||
flexi_logger = "0.9.0"
|
||||
flexi_logger = "0.9.1"
|
||||
log = "0.4.3"
|
||||
url_serde = "0.2.0"
|
||||
languageserver-types = "0.49.0"
|
||||
text_unit = { version = "0.1.2", features = ["serde"] }
|
||||
|
||||
libsyntax2 = { path = "../libsyntax2" }
|
||||
libeditor = { path = "../libeditor" }
|
||||
|
|
|
@ -3,9 +3,11 @@ use languageserver_types::{
|
|||
TextDocumentSyncCapability,
|
||||
TextDocumentSyncOptions,
|
||||
TextDocumentSyncKind,
|
||||
ExecuteCommandOptions,
|
||||
};
|
||||
|
||||
pub const SERVER_CAPABILITIES: ServerCapabilities = ServerCapabilities {
|
||||
pub fn server_capabilities() -> ServerCapabilities {
|
||||
ServerCapabilities {
|
||||
text_document_sync: Some(TextDocumentSyncCapability::Options(
|
||||
TextDocumentSyncOptions {
|
||||
open_close: Some(true),
|
||||
|
@ -32,5 +34,8 @@ pub const SERVER_CAPABILITIES: ServerCapabilities = ServerCapabilities {
|
|||
document_on_type_formatting_provider: None,
|
||||
rename_provider: None,
|
||||
color_provider: None,
|
||||
execute_command_provider: None,
|
||||
};
|
||||
execute_command_provider: Some(ExecuteCommandOptions {
|
||||
commands: vec!["apply_code_action".to_string()],
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
use languageserver_types::{Range, SymbolKind, Position};
|
||||
use libeditor::{LineIndex, LineCol};
|
||||
use languageserver_types::{Range, SymbolKind, Position, TextEdit};
|
||||
use libeditor::{LineIndex, LineCol, Edit, AtomEdit};
|
||||
use libsyntax2::{SyntaxKind, TextUnit, TextRange};
|
||||
|
||||
pub trait Conv {
|
||||
type Output;
|
||||
fn conv(&self) -> Self::Output;
|
||||
fn conv(self) -> Self::Output;
|
||||
}
|
||||
|
||||
pub trait ConvWith {
|
||||
type Ctx;
|
||||
type Output;
|
||||
fn conv_with(&self, ctx: &Self::Ctx) -> Self::Output;
|
||||
fn conv_with(self, ctx: &Self::Ctx) -> Self::Output;
|
||||
}
|
||||
|
||||
impl Conv for SyntaxKind {
|
||||
type Output = SymbolKind;
|
||||
|
||||
fn conv(&self) -> <Self as Conv>::Output {
|
||||
match *self {
|
||||
fn conv(self) -> <Self as Conv>::Output {
|
||||
match self {
|
||||
SyntaxKind::FUNCTION => SymbolKind::Function,
|
||||
SyntaxKind::STRUCT => SymbolKind::Struct,
|
||||
SyntaxKind::ENUM => SymbolKind::Enum,
|
||||
|
@ -35,7 +35,7 @@ impl ConvWith for Position {
|
|||
type Ctx = LineIndex;
|
||||
type Output = TextUnit;
|
||||
|
||||
fn conv_with(&self, line_index: &LineIndex) -> TextUnit {
|
||||
fn conv_with(self, line_index: &LineIndex) -> TextUnit {
|
||||
// TODO: UTF-16
|
||||
let line_col = LineCol {
|
||||
line: self.line as u32,
|
||||
|
@ -49,8 +49,8 @@ impl ConvWith for TextUnit {
|
|||
type Ctx = LineIndex;
|
||||
type Output = Position;
|
||||
|
||||
fn conv_with(&self, line_index: &LineIndex) -> Position {
|
||||
let line_col = line_index.line_col(*self);
|
||||
fn conv_with(self, line_index: &LineIndex) -> Position {
|
||||
let line_col = line_index.line_col(self);
|
||||
// TODO: UTF-16
|
||||
Position::new(line_col.line as u64, u32::from(line_col.col) as u64)
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ impl ConvWith for TextRange {
|
|||
type Ctx = LineIndex;
|
||||
type Output = Range;
|
||||
|
||||
fn conv_with(&self, line_index: &LineIndex) -> Range {
|
||||
fn conv_with(self, line_index: &LineIndex) -> Range {
|
||||
Range::new(
|
||||
self.start().conv_with(line_index),
|
||||
self.end().conv_with(line_index),
|
||||
|
@ -72,10 +72,70 @@ impl ConvWith for Range {
|
|||
type Ctx = LineIndex;
|
||||
type Output = TextRange;
|
||||
|
||||
fn conv_with(&self, line_index: &LineIndex) -> TextRange {
|
||||
fn conv_with(self, line_index: &LineIndex) -> TextRange {
|
||||
TextRange::from_to(
|
||||
self.start.conv_with(line_index),
|
||||
self.end.conv_with(line_index),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConvWith for Edit {
|
||||
type Ctx = LineIndex;
|
||||
type Output = Vec<TextEdit>;
|
||||
|
||||
fn conv_with(self, line_index: &LineIndex) -> Vec<TextEdit> {
|
||||
self.into_atoms()
|
||||
.into_iter()
|
||||
.map_conv_with(line_index)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl ConvWith for AtomEdit {
|
||||
type Ctx = LineIndex;
|
||||
type Output = TextEdit;
|
||||
|
||||
fn conv_with(self, line_index: &LineIndex) -> TextEdit {
|
||||
TextEdit {
|
||||
range: self.delete.conv_with(line_index),
|
||||
new_text: self.insert,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub trait MapConvWith<'a>: Sized {
|
||||
type Ctx;
|
||||
type Output;
|
||||
|
||||
fn map_conv_with(self, ctx: &'a Self::Ctx) -> ConvWithIter<'a, Self, Self::Ctx> {
|
||||
ConvWithIter { iter: self, ctx }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, I> MapConvWith<'a> for I
|
||||
where I: Iterator,
|
||||
I::Item: ConvWith
|
||||
{
|
||||
type Ctx = <I::Item as ConvWith>::Ctx;
|
||||
type Output = <I::Item as ConvWith>::Output;
|
||||
}
|
||||
|
||||
pub struct ConvWithIter<'a, I, Ctx: 'a> {
|
||||
iter: I,
|
||||
ctx: &'a Ctx,
|
||||
}
|
||||
|
||||
impl<'a, I, Ctx> Iterator for ConvWithIter<'a, I, Ctx>
|
||||
where
|
||||
I: Iterator,
|
||||
I::Item: ConvWith<Ctx=Ctx>,
|
||||
{
|
||||
type Item = <I::Item as ConvWith>::Output;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.iter.next().map(|item| item.conv_with(self.ctx))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ impl<R: ClientRequest> Responder<R> {
|
|||
let res = match result {
|
||||
Ok(result) => {
|
||||
RawResponse {
|
||||
id: Some(self.id),
|
||||
id: self.id,
|
||||
result: serde_json::to_value(result)?,
|
||||
error: serde_json::Value::Null,
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ fn error_response(id: u64, code: ErrorCode, message: &'static str) -> Result<Raw
|
|||
message: &'static str,
|
||||
}
|
||||
let resp = RawResponse {
|
||||
id: Some(id),
|
||||
id,
|
||||
result: serde_json::Value::Null,
|
||||
error: serde_json::to_value(Error {
|
||||
code: code as i32,
|
||||
|
|
|
@ -34,8 +34,13 @@ pub struct RawNotification {
|
|||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RawResponse {
|
||||
pub id: Option<u64>,
|
||||
// JSON RPC allows this to be null if it was impossible
|
||||
// to decode the request's id. Ignore this special case
|
||||
// and just die horribly.
|
||||
pub id: u64,
|
||||
#[serde(default)]
|
||||
pub result: Value,
|
||||
#[serde(default)]
|
||||
pub error: Value,
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ mod main_loop;
|
|||
|
||||
use threadpool::ThreadPool;
|
||||
use crossbeam_channel::bounded;
|
||||
use flexi_logger::Logger;
|
||||
use flexi_logger::{Logger, Duplicate};
|
||||
use libanalysis::WorldState;
|
||||
|
||||
use ::{
|
||||
|
@ -38,6 +38,7 @@ pub type Result<T> = ::std::result::Result<T, ::failure::Error>;
|
|||
|
||||
fn main() -> Result<()> {
|
||||
Logger::with_env()
|
||||
.duplicate_to_stderr(Duplicate::All)
|
||||
.log_to_file()
|
||||
.directory("log")
|
||||
.start()?;
|
||||
|
@ -81,7 +82,7 @@ fn initialize(io: &mut Io) -> Result<()> {
|
|||
RawMsg::Request(req) => {
|
||||
let mut req = Some(req);
|
||||
dispatch::handle_request::<req::Initialize, _>(&mut req, |_params, resp| {
|
||||
let res = req::InitializeResult { capabilities: caps::SERVER_CAPABILITIES };
|
||||
let res = req::InitializeResult { capabilities: caps::server_capabilities() };
|
||||
let resp = resp.into_response(Ok(res))?;
|
||||
io.send(RawMsg::Response(resp));
|
||||
Ok(())
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use languageserver_types::{
|
||||
Diagnostic, DiagnosticSeverity, Url, DocumentSymbol,
|
||||
Command
|
||||
Command, TextDocumentIdentifier, WorkspaceEdit
|
||||
};
|
||||
use libanalysis::World;
|
||||
use libeditor;
|
||||
use serde_json::to_value;
|
||||
use libsyntax2::TextUnit;
|
||||
use serde_json::{to_value, from_value};
|
||||
|
||||
use ::{
|
||||
req::{self, Decoration}, Result,
|
||||
util::FilePath,
|
||||
conv::{Conv, ConvWith},
|
||||
conv::{Conv, ConvWith, MapConvWith},
|
||||
};
|
||||
|
||||
pub fn handle_syntax_tree(
|
||||
|
@ -29,9 +32,9 @@ pub fn handle_extend_selection(
|
|||
let file = world.file_syntax(&path)?;
|
||||
let line_index = world.file_line_index(&path)?;
|
||||
let selections = params.selections.into_iter()
|
||||
.map(|r| r.conv_with(&line_index))
|
||||
.map_conv_with(&line_index)
|
||||
.map(|r| libeditor::extend_selection(&file, r).unwrap_or(r))
|
||||
.map(|r| r.conv_with(&line_index))
|
||||
.map_conv_with(&line_index)
|
||||
.collect();
|
||||
Ok(req::ExtendSelectionResult { selections })
|
||||
}
|
||||
|
@ -78,18 +81,71 @@ pub fn handle_code_action(
|
|||
let line_index = world.file_line_index(&path)?;
|
||||
let offset = params.range.conv_with(&line_index).start();
|
||||
let ret = if libeditor::flip_comma(&file, offset).is_some() {
|
||||
Some(vec![apply_code_action_cmd(ActionId::FlipComma)])
|
||||
let cmd = apply_code_action_cmd(
|
||||
ActionId::FlipComma,
|
||||
params.text_document,
|
||||
offset,
|
||||
);
|
||||
Some(vec![cmd])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn apply_code_action_cmd(id: ActionId) -> Command {
|
||||
pub fn handle_execute_command(
|
||||
world: World,
|
||||
mut params: req::ExecuteCommandParams,
|
||||
) -> Result<req::ApplyWorkspaceEditParams> {
|
||||
if params.command.as_str() != "apply_code_action" {
|
||||
bail!("unknown cmd: {:?}", params.command);
|
||||
}
|
||||
if params.arguments.len() != 1 {
|
||||
bail!("expected single arg, got {}", params.arguments.len());
|
||||
}
|
||||
let arg = params.arguments.pop().unwrap();
|
||||
let arg: ActionRequest = from_value(arg)?;
|
||||
match arg.id {
|
||||
ActionId::FlipComma => {
|
||||
let path = arg.text_document.file_path()?;
|
||||
let file = world.file_syntax(&path)?;
|
||||
let line_index = world.file_line_index(&path)?;
|
||||
let edit = match libeditor::flip_comma(&file, arg.offset) {
|
||||
Some(edit) => edit(),
|
||||
None => bail!("command not applicable"),
|
||||
};
|
||||
let mut changes = HashMap::new();
|
||||
changes.insert(
|
||||
arg.text_document.uri,
|
||||
edit.conv_with(&line_index),
|
||||
);
|
||||
let edit = WorkspaceEdit {
|
||||
changes: Some(changes),
|
||||
document_changes: None,
|
||||
};
|
||||
|
||||
Ok(req::ApplyWorkspaceEditParams { edit })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ActionRequest {
|
||||
id: ActionId,
|
||||
text_document: TextDocumentIdentifier,
|
||||
offset: TextUnit,
|
||||
}
|
||||
|
||||
fn apply_code_action_cmd(id: ActionId, doc: TextDocumentIdentifier, offset: TextUnit) -> Command {
|
||||
let action_request = ActionRequest {
|
||||
id,
|
||||
text_document: doc,
|
||||
offset,
|
||||
};
|
||||
Command {
|
||||
title: id.title().to_string(),
|
||||
command: "apply_code_action".to_string(),
|
||||
arguments: Some(vec![to_value(id).unwrap()]),
|
||||
arguments: Some(vec![to_value(action_request).unwrap()]),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ use threadpool::ThreadPool;
|
|||
use crossbeam_channel::{Sender, Receiver};
|
||||
use languageserver_types::Url;
|
||||
use libanalysis::{World, WorldState};
|
||||
use serde_json::to_value;
|
||||
|
||||
use {
|
||||
req, dispatch,
|
||||
|
@ -19,6 +20,7 @@ use {
|
|||
publish_decorations,
|
||||
handle_document_symbol,
|
||||
handle_code_action,
|
||||
handle_execute_command,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -79,10 +81,12 @@ pub(super) fn main_loop(
|
|||
on_notification(io, world, pool, &sender, not)?
|
||||
}
|
||||
RawMsg::Response(resp) => {
|
||||
if !pending_requests.remove(&resp.id) {
|
||||
error!("unexpected response: {:?}", resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -107,22 +111,30 @@ fn on_request(
|
|||
handle_request_on_threadpool::<req::CodeActionRequest>(
|
||||
&mut req, pool, world, sender, handle_code_action,
|
||||
)?;
|
||||
// dispatch::handle_request::<req::ExecuteCommand, _>(&mut req, |(), resp| {
|
||||
// let world = world.snapshot();
|
||||
// let sender = sender.clone();
|
||||
// pool.execute(move || {
|
||||
// let task = match handle_execute_command(world, params) {
|
||||
// Ok(req) => Task::Request(req),
|
||||
// Err(e) => Task::Die(e),
|
||||
// };
|
||||
// sender.send(task)
|
||||
// });
|
||||
//
|
||||
// let resp = resp.into_response(Ok(()))?;
|
||||
// io.send(RawMsg::Response(resp));
|
||||
// shutdown = true;
|
||||
// Ok(())
|
||||
// })?;
|
||||
dispatch::handle_request::<req::ExecuteCommand, _>(&mut req, |params, resp| {
|
||||
io.send(RawMsg::Response(resp.into_response(Ok(None))?));
|
||||
|
||||
let world = world.snapshot();
|
||||
let sender = sender.clone();
|
||||
pool.execute(move || {
|
||||
let task = match handle_execute_command(world, params) {
|
||||
Ok(req) => match to_value(req) {
|
||||
Err(e) => Task::Die(e.into()),
|
||||
Ok(params) => {
|
||||
let request = RawRequest {
|
||||
id: 0,
|
||||
method: <req::ApplyWorkspaceEdit as req::ClientRequest>::METHOD.to_string(),
|
||||
params,
|
||||
};
|
||||
Task::Request(request)
|
||||
}
|
||||
},
|
||||
Err(e) => Task::Die(e),
|
||||
};
|
||||
sender.send(task)
|
||||
});
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let mut shutdown = false;
|
||||
dispatch::handle_request::<req::Shutdown, _>(&mut req, |(), resp| {
|
||||
|
|
|
@ -6,7 +6,8 @@ pub use languageserver_types::{
|
|||
request::*, notification::*,
|
||||
InitializeResult, PublishDiagnosticsParams,
|
||||
DocumentSymbolParams, DocumentSymbolResponse,
|
||||
CodeActionParams,
|
||||
CodeActionParams, ApplyWorkspaceEditParams,
|
||||
ExecuteCommandParams,
|
||||
};
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue