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"
},
"scripts": {
"compile": "tsc -p ./",
"vscode:prepublish": "tsc -p ./",
"compile": "tsc -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install"
},
"dependencies": {

View File

@ -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
}
}

View File

@ -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},
};

View File

@ -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" }

View File

@ -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()],
}),
}
}

View File

@ -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))
}
}

View File

@ -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,

View File

@ -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,
}

View File

@ -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(())

View File

@ -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()]),
}
}

View File

@ -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| {

View File

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