Apply code actions

This commit is contained in:
Aleksey Kladov 2018-08-13 02:38:34 +03:00
parent 25aebb5225
commit be742a5877
12 changed files with 247 additions and 118 deletions

View file

@ -10,7 +10,8 @@
"vscode": "^1.25.0" "vscode": "^1.25.0"
}, },
"scripts": { "scripts": {
"compile": "tsc -p ./", "vscode:prepublish": "tsc -p ./",
"compile": "tsc -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install" "postinstall": "node ./node_modules/vscode/bin/install"
}, },
"dependencies": { "dependencies": {

View file

@ -1,11 +1,11 @@
use {TextRange, TextUnit}; use {TextRange, TextUnit};
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Edit { pub struct Edit {
pub atoms: Vec<AtomEdit>, atoms: Vec<AtomEdit>,
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct AtomEdit { pub struct AtomEdit {
pub delete: TextRange, pub delete: TextRange,
pub insert: String, pub insert: String,
@ -22,7 +22,6 @@ impl EditBuilder {
} }
pub fn replace(&mut self, range: TextRange, replacement: String) { pub fn replace(&mut self, range: TextRange, replacement: String) {
let range = self.translate(range);
self.atoms.push(AtomEdit { delete: range, insert: replacement }) self.atoms.push(AtomEdit { delete: range, insert: replacement })
} }
@ -35,59 +34,47 @@ impl EditBuilder {
} }
pub fn finish(self) -> Edit { 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())
} }
Edit { atoms }
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
} }
} }
impl Edit { impl Edit {
pub fn apply(&self, text: &str) -> String { pub fn into_atoms(self) -> Vec<AtomEdit> {
let mut text = text.to_owned(); self.atoms
for atom in self.atoms.iter() {
text = atom.apply(&text);
} }
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 { impl AtomEdit {
fn apply(&self, text: &str) -> String { fn start(&self) -> usize {
let prefix = &text[ u32::from(self.delete.start()) as usize
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 apply_to_position(&self, pos: TextUnit) -> Option<TextUnit> { fn end(&self) -> usize {
if pos <= self.delete.start() { u32::from(self.delete.end()) as usize
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())?,
))
} }
} }

View file

@ -19,7 +19,7 @@ pub use self::{
line_index::{LineIndex, LineCol}, line_index::{LineIndex, LineCol},
extend_selection::extend_selection, extend_selection::extend_selection,
symbols::{FileSymbol, file_symbols}, symbols::{FileSymbol, file_symbols},
edit::{EditBuilder, Edit}, edit::{EditBuilder, Edit, AtomEdit},
code_actions::{flip_comma}, code_actions::{flip_comma},
}; };

View file

@ -11,10 +11,11 @@ serde_derive = "1.0.71"
drop_bomb = "0.1.0" drop_bomb = "0.1.0"
crossbeam-channel = "0.2.4" crossbeam-channel = "0.2.4"
threadpool = "1.7.1" threadpool = "1.7.1"
flexi_logger = "0.9.0" flexi_logger = "0.9.1"
log = "0.4.3" log = "0.4.3"
url_serde = "0.2.0" url_serde = "0.2.0"
languageserver-types = "0.49.0" languageserver-types = "0.49.0"
text_unit = { version = "0.1.2", features = ["serde"] }
libsyntax2 = { path = "../libsyntax2" } libsyntax2 = { path = "../libsyntax2" }
libeditor = { path = "../libeditor" } libeditor = { path = "../libeditor" }

View file

@ -3,9 +3,11 @@ use languageserver_types::{
TextDocumentSyncCapability, TextDocumentSyncCapability,
TextDocumentSyncOptions, TextDocumentSyncOptions,
TextDocumentSyncKind, TextDocumentSyncKind,
ExecuteCommandOptions,
}; };
pub const SERVER_CAPABILITIES: ServerCapabilities = ServerCapabilities { pub fn server_capabilities() -> ServerCapabilities {
ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Options( text_document_sync: Some(TextDocumentSyncCapability::Options(
TextDocumentSyncOptions { TextDocumentSyncOptions {
open_close: Some(true), open_close: Some(true),
@ -32,5 +34,8 @@ pub const SERVER_CAPABILITIES: ServerCapabilities = ServerCapabilities {
document_on_type_formatting_provider: None, document_on_type_formatting_provider: None,
rename_provider: None, rename_provider: None,
color_provider: None, color_provider: None,
execute_command_provider: None, execute_command_provider: Some(ExecuteCommandOptions {
}; commands: vec!["apply_code_action".to_string()],
}),
}
}

View file

@ -1,23 +1,23 @@
use languageserver_types::{Range, SymbolKind, Position}; use languageserver_types::{Range, SymbolKind, Position, TextEdit};
use libeditor::{LineIndex, LineCol}; use libeditor::{LineIndex, LineCol, Edit, AtomEdit};
use libsyntax2::{SyntaxKind, TextUnit, TextRange}; use libsyntax2::{SyntaxKind, TextUnit, TextRange};
pub trait Conv { pub trait Conv {
type Output; type Output;
fn conv(&self) -> Self::Output; fn conv(self) -> Self::Output;
} }
pub trait ConvWith { pub trait ConvWith {
type Ctx; type Ctx;
type Output; type Output;
fn conv_with(&self, ctx: &Self::Ctx) -> Self::Output; fn conv_with(self, ctx: &Self::Ctx) -> Self::Output;
} }
impl Conv for SyntaxKind { impl Conv for SyntaxKind {
type Output = SymbolKind; type Output = SymbolKind;
fn conv(&self) -> <Self as Conv>::Output { fn conv(self) -> <Self as Conv>::Output {
match *self { match self {
SyntaxKind::FUNCTION => SymbolKind::Function, SyntaxKind::FUNCTION => SymbolKind::Function,
SyntaxKind::STRUCT => SymbolKind::Struct, SyntaxKind::STRUCT => SymbolKind::Struct,
SyntaxKind::ENUM => SymbolKind::Enum, SyntaxKind::ENUM => SymbolKind::Enum,
@ -35,7 +35,7 @@ impl ConvWith for Position {
type Ctx = LineIndex; type Ctx = LineIndex;
type Output = TextUnit; type Output = TextUnit;
fn conv_with(&self, line_index: &LineIndex) -> TextUnit { fn conv_with(self, line_index: &LineIndex) -> TextUnit {
// TODO: UTF-16 // TODO: UTF-16
let line_col = LineCol { let line_col = LineCol {
line: self.line as u32, line: self.line as u32,
@ -49,8 +49,8 @@ impl ConvWith for TextUnit {
type Ctx = LineIndex; type Ctx = LineIndex;
type Output = Position; type Output = Position;
fn conv_with(&self, line_index: &LineIndex) -> Position { fn conv_with(self, line_index: &LineIndex) -> Position {
let line_col = line_index.line_col(*self); let line_col = line_index.line_col(self);
// TODO: UTF-16 // TODO: UTF-16
Position::new(line_col.line as u64, u32::from(line_col.col) as u64) 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 Ctx = LineIndex;
type Output = Range; type Output = Range;
fn conv_with(&self, line_index: &LineIndex) -> Range { fn conv_with(self, line_index: &LineIndex) -> Range {
Range::new( Range::new(
self.start().conv_with(line_index), self.start().conv_with(line_index),
self.end().conv_with(line_index), self.end().conv_with(line_index),
@ -72,10 +72,70 @@ impl ConvWith for Range {
type Ctx = LineIndex; type Ctx = LineIndex;
type Output = TextRange; type Output = TextRange;
fn conv_with(&self, line_index: &LineIndex) -> TextRange { fn conv_with(self, line_index: &LineIndex) -> TextRange {
TextRange::from_to( TextRange::from_to(
self.start.conv_with(line_index), self.start.conv_with(line_index),
self.end.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))
}
}

View file

@ -25,7 +25,7 @@ impl<R: ClientRequest> Responder<R> {
let res = match result { let res = match result {
Ok(result) => { Ok(result) => {
RawResponse { RawResponse {
id: Some(self.id), id: self.id,
result: serde_json::to_value(result)?, result: serde_json::to_value(result)?,
error: serde_json::Value::Null, error: serde_json::Value::Null,
} }
@ -125,7 +125,7 @@ fn error_response(id: u64, code: ErrorCode, message: &'static str) -> Result<Raw
message: &'static str, message: &'static str,
} }
let resp = RawResponse { let resp = RawResponse {
id: Some(id), id,
result: serde_json::Value::Null, result: serde_json::Value::Null,
error: serde_json::to_value(Error { error: serde_json::to_value(Error {
code: code as i32, code: code as i32,

View file

@ -34,8 +34,13 @@ pub struct RawNotification {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct RawResponse { 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, pub result: Value,
#[serde(default)]
pub error: Value, pub error: Value,
} }

View file

@ -27,7 +27,7 @@ mod main_loop;
use threadpool::ThreadPool; use threadpool::ThreadPool;
use crossbeam_channel::bounded; use crossbeam_channel::bounded;
use flexi_logger::Logger; use flexi_logger::{Logger, Duplicate};
use libanalysis::WorldState; use libanalysis::WorldState;
use ::{ use ::{
@ -38,6 +38,7 @@ pub type Result<T> = ::std::result::Result<T, ::failure::Error>;
fn main() -> Result<()> { fn main() -> Result<()> {
Logger::with_env() Logger::with_env()
.duplicate_to_stderr(Duplicate::All)
.log_to_file() .log_to_file()
.directory("log") .directory("log")
.start()?; .start()?;
@ -81,7 +82,7 @@ fn initialize(io: &mut Io) -> Result<()> {
RawMsg::Request(req) => { RawMsg::Request(req) => {
let mut req = Some(req); let mut req = Some(req);
dispatch::handle_request::<req::Initialize, _>(&mut req, |_params, resp| { 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))?; let resp = resp.into_response(Ok(res))?;
io.send(RawMsg::Response(resp)); io.send(RawMsg::Response(resp));
Ok(()) Ok(())

View file

@ -1,15 +1,18 @@
use std::collections::HashMap;
use languageserver_types::{ use languageserver_types::{
Diagnostic, DiagnosticSeverity, Url, DocumentSymbol, Diagnostic, DiagnosticSeverity, Url, DocumentSymbol,
Command Command, TextDocumentIdentifier, WorkspaceEdit
}; };
use libanalysis::World; use libanalysis::World;
use libeditor; use libeditor;
use serde_json::to_value; use libsyntax2::TextUnit;
use serde_json::{to_value, from_value};
use ::{ use ::{
req::{self, Decoration}, Result, req::{self, Decoration}, Result,
util::FilePath, util::FilePath,
conv::{Conv, ConvWith}, conv::{Conv, ConvWith, MapConvWith},
}; };
pub fn handle_syntax_tree( pub fn handle_syntax_tree(
@ -29,9 +32,9 @@ pub fn handle_extend_selection(
let file = world.file_syntax(&path)?; let file = world.file_syntax(&path)?;
let line_index = world.file_line_index(&path)?; let line_index = world.file_line_index(&path)?;
let selections = params.selections.into_iter() 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| libeditor::extend_selection(&file, r).unwrap_or(r))
.map(|r| r.conv_with(&line_index)) .map_conv_with(&line_index)
.collect(); .collect();
Ok(req::ExtendSelectionResult { selections }) Ok(req::ExtendSelectionResult { selections })
} }
@ -78,18 +81,71 @@ pub fn handle_code_action(
let line_index = world.file_line_index(&path)?; let line_index = world.file_line_index(&path)?;
let offset = params.range.conv_with(&line_index).start(); let offset = params.range.conv_with(&line_index).start();
let ret = if libeditor::flip_comma(&file, offset).is_some() { 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 { } else {
None None
}; };
Ok(ret) 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 { Command {
title: id.title().to_string(), title: id.title().to_string(),
command: "apply_code_action".to_string(), command: "apply_code_action".to_string(),
arguments: Some(vec![to_value(id).unwrap()]), arguments: Some(vec![to_value(action_request).unwrap()]),
} }
} }

View file

@ -6,6 +6,7 @@ use threadpool::ThreadPool;
use crossbeam_channel::{Sender, Receiver}; use crossbeam_channel::{Sender, Receiver};
use languageserver_types::Url; use languageserver_types::Url;
use libanalysis::{World, WorldState}; use libanalysis::{World, WorldState};
use serde_json::to_value;
use { use {
req, dispatch, req, dispatch,
@ -19,6 +20,7 @@ use {
publish_decorations, publish_decorations,
handle_document_symbol, handle_document_symbol,
handle_code_action, handle_code_action,
handle_execute_command,
}, },
}; };
@ -79,10 +81,12 @@ pub(super) fn main_loop(
on_notification(io, world, pool, &sender, not)? on_notification(io, world, pool, &sender, not)?
} }
RawMsg::Response(resp) => { RawMsg::Response(resp) => {
if !pending_requests.remove(&resp.id) {
error!("unexpected response: {:?}", resp) error!("unexpected response: {:?}", resp)
} }
} }
} }
}
}; };
} }
} }
@ -107,22 +111,30 @@ fn on_request(
handle_request_on_threadpool::<req::CodeActionRequest>( handle_request_on_threadpool::<req::CodeActionRequest>(
&mut req, pool, world, sender, handle_code_action, &mut req, pool, world, sender, handle_code_action,
)?; )?;
// dispatch::handle_request::<req::ExecuteCommand, _>(&mut req, |(), resp| { dispatch::handle_request::<req::ExecuteCommand, _>(&mut req, |params, resp| {
// let world = world.snapshot(); io.send(RawMsg::Response(resp.into_response(Ok(None))?));
// let sender = sender.clone();
// pool.execute(move || { let world = world.snapshot();
// let task = match handle_execute_command(world, params) { let sender = sender.clone();
// Ok(req) => Task::Request(req), pool.execute(move || {
// Err(e) => Task::Die(e), let task = match handle_execute_command(world, params) {
// }; Ok(req) => match to_value(req) {
// sender.send(task) Err(e) => Task::Die(e.into()),
// }); Ok(params) => {
// let request = RawRequest {
// let resp = resp.into_response(Ok(()))?; id: 0,
// io.send(RawMsg::Response(resp)); method: <req::ApplyWorkspaceEdit as req::ClientRequest>::METHOD.to_string(),
// shutdown = true; params,
// Ok(()) };
// })?; Task::Request(request)
}
},
Err(e) => Task::Die(e),
};
sender.send(task)
});
Ok(())
})?;
let mut shutdown = false; let mut shutdown = false;
dispatch::handle_request::<req::Shutdown, _>(&mut req, |(), resp| { dispatch::handle_request::<req::Shutdown, _>(&mut req, |(), resp| {

View file

@ -6,7 +6,8 @@ pub use languageserver_types::{
request::*, notification::*, request::*, notification::*,
InitializeResult, PublishDiagnosticsParams, InitializeResult, PublishDiagnosticsParams,
DocumentSymbolParams, DocumentSymbolResponse, DocumentSymbolParams, DocumentSymbolResponse,
CodeActionParams, CodeActionParams, ApplyWorkspaceEditParams,
ExecuteCommandParams,
}; };