From be742a587704f27f4e503c50f549aa9ec1527fcc Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Mon, 13 Aug 2018 02:38:34 +0300 Subject: Apply code actions --- code/package.json | 3 +- crates/libeditor/src/edit.rs | 75 +++++++++++++----------------- crates/libeditor/src/lib.rs | 2 +- crates/server/Cargo.toml | 3 +- crates/server/src/caps.rs | 63 +++++++++++++------------ crates/server/src/conv.rs | 82 ++++++++++++++++++++++++++++----- crates/server/src/dispatch.rs | 4 +- crates/server/src/io.rs | 7 ++- crates/server/src/main.rs | 5 +- crates/server/src/main_loop/handlers.rs | 72 +++++++++++++++++++++++++---- crates/server/src/main_loop/mod.rs | 46 +++++++++++------- crates/server/src/req.rs | 3 +- 12 files changed, 247 insertions(+), 118 deletions(-) diff --git a/code/package.json b/code/package.json index 327fbfb81..eef16522f 100644 --- a/code/package.json +++ b/code/package.json @@ -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": { diff --git a/crates/libeditor/src/edit.rs b/crates/libeditor/src/edit.rs index 163ecf6de..15a2a904f 100644 --- a/crates/libeditor/src/edit.rs +++ b/crates/libeditor/src/edit.rs @@ -1,11 +1,11 @@ use {TextRange, TextUnit}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Edit { - pub atoms: Vec, + atoms: Vec, } -#[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 } - } - - 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"); + 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()) } - range + Edit { atoms } } } impl Edit { + pub fn into_atoms(self) -> Vec { + self.atoms + } + pub fn apply(&self, text: &str) -> String { - let mut text = text.to_owned(); + let mut total_len = text.len(); for atom in self.atoms.iter() { - text = atom.apply(&text); + total_len += atom.insert.len(); + total_len -= atom.end() - atom.start(); } - text + 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 { - 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 { - 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 } } - diff --git a/crates/libeditor/src/lib.rs b/crates/libeditor/src/lib.rs index 103f32190..1e88f1471 100644 --- a/crates/libeditor/src/lib.rs +++ b/crates/libeditor/src/lib.rs @@ -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}, }; diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 2d0db2838..8e077ecf0 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -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" } diff --git a/crates/server/src/caps.rs b/crates/server/src/caps.rs index b502b0865..d06a43a82 100644 --- a/crates/server/src/caps.rs +++ b/crates/server/src/caps.rs @@ -3,34 +3,39 @@ use languageserver_types::{ TextDocumentSyncCapability, TextDocumentSyncOptions, TextDocumentSyncKind, + ExecuteCommandOptions, }; -pub const SERVER_CAPABILITIES: ServerCapabilities = ServerCapabilities { - text_document_sync: Some(TextDocumentSyncCapability::Options( - TextDocumentSyncOptions { - open_close: Some(true), - change: Some(TextDocumentSyncKind::Full), - will_save: None, - will_save_wait_until: None, - save: None, - } - )), - hover_provider: None, - completion_provider: None, - signature_help_provider: None, - definition_provider: None, - type_definition_provider: None, - implementation_provider: None, - references_provider: None, - document_highlight_provider: None, - document_symbol_provider: Some(true), - workspace_symbol_provider: None, - code_action_provider: Some(true), - code_lens_provider: None, - document_formatting_provider: None, - document_range_formatting_provider: None, - document_on_type_formatting_provider: None, - rename_provider: None, - color_provider: None, - execute_command_provider: None, -}; +pub fn server_capabilities() -> ServerCapabilities { + ServerCapabilities { + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::Full), + will_save: None, + will_save_wait_until: None, + save: None, + } + )), + hover_provider: None, + completion_provider: None, + signature_help_provider: None, + definition_provider: None, + type_definition_provider: None, + implementation_provider: None, + references_provider: None, + document_highlight_provider: None, + document_symbol_provider: Some(true), + workspace_symbol_provider: None, + code_action_provider: Some(true), + code_lens_provider: None, + document_formatting_provider: None, + document_range_formatting_provider: None, + document_on_type_formatting_provider: None, + rename_provider: None, + color_provider: None, + execute_command_provider: Some(ExecuteCommandOptions { + commands: vec!["apply_code_action".to_string()], + }), + } +} diff --git a/crates/server/src/conv.rs b/crates/server/src/conv.rs index c7bea15df..0ed989b32 100644 --- a/crates/server/src/conv.rs +++ b/crates/server/src/conv.rs @@ -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) -> ::Output { - match *self { + fn conv(self) -> ::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; + + fn conv_with(self, line_index: &LineIndex) -> Vec { + 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 = ::Ctx; + type Output = ::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, +{ + type Item = ::Output; + + fn next(&mut self) -> Option { + self.iter.next().map(|item| item.conv_with(self.ctx)) + } +} + diff --git a/crates/server/src/dispatch.rs b/crates/server/src/dispatch.rs index bfdbd30ca..3a3ee74bb 100644 --- a/crates/server/src/dispatch.rs +++ b/crates/server/src/dispatch.rs @@ -25,7 +25,7 @@ impl Responder { 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, + // 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, } diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index 7416de08a..c2952465e 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -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 = ::std::result::Result; 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::(&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(()) diff --git a/crates/server/src/main_loop/handlers.rs b/crates/server/src/main_loop/handlers.rs index c6db22289..d4ae2a368 100644 --- a/crates/server/src/main_loop/handlers.rs +++ b/crates/server/src/main_loop/handlers.rs @@ -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 { + 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()]), } } diff --git a/crates/server/src/main_loop/mod.rs b/crates/server/src/main_loop/mod.rs index 3d367f5f6..5b7093ad7 100644 --- a/crates/server/src/main_loop/mod.rs +++ b/crates/server/src/main_loop/mod.rs @@ -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,7 +81,9 @@ pub(super) fn main_loop( on_notification(io, world, pool, &sender, not)? } RawMsg::Response(resp) => { - error!("unexpected response: {:?}", resp) + if !pending_requests.remove(&resp.id) { + error!("unexpected response: {:?}", resp) + } } } } @@ -107,22 +111,30 @@ fn on_request( handle_request_on_threadpool::( &mut req, pool, world, sender, handle_code_action, )?; -// dispatch::handle_request::(&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::(&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: ::METHOD.to_string(), + params, + }; + Task::Request(request) + } + }, + Err(e) => Task::Die(e), + }; + sender.send(task) + }); + Ok(()) + })?; let mut shutdown = false; dispatch::handle_request::(&mut req, |(), resp| { diff --git a/crates/server/src/req.rs b/crates/server/src/req.rs index e3b1cdf50..a22ba4bc3 100644 --- a/crates/server/src/req.rs +++ b/crates/server/src/req.rs @@ -6,7 +6,8 @@ pub use languageserver_types::{ request::*, notification::*, InitializeResult, PublishDiagnosticsParams, DocumentSymbolParams, DocumentSymbolResponse, - CodeActionParams, + CodeActionParams, ApplyWorkspaceEditParams, + ExecuteCommandParams, }; -- cgit v1.2.3