From 48e344a2a813b875ca813be726387692b7ba0762 Mon Sep 17 00:00:00 2001 From: Grzegorz Baranski Date: Sat, 24 Jul 2021 03:26:43 +0200 Subject: [PATCH] feat: code actions - document edits (#478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip: Code actions * fix(term): use current macro instead Context::context * feat(lsp): set code_action capabilities * feat(term): set SPC-a to code_action * feat(term): wip on applying code actions * deps: `cargo update` * feat(term): applying code actions edits * fix(term): cleanup of apply_edit * fix(term): applying edits as a whole thing instead one by one * refactor(term): move apply_edits below * fix(term): improve unimplemented messages for further investigation * fix(term): change code action command comment Co-authored-by: Ivan Tham * fix(term): add matching `}` * fix(term): cleanup, todo!() on workspace edit * fix(term): remove unrelated workspace_symbol_picker * fix(term): apply cargo-clippy suggestions * fix(term): replace todo!'s with editor.set_error Co-authored-by: Blaž Hrastnik Co-authored-by: Ivan Tham --- Cargo.lock | 24 ++++---- helix-lsp/src/client.rs | 47 ++++++++++++++ helix-term/src/commands.rs | 121 ++++++++++++++++++++++++++++++++++++- 3 files changed, 178 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d96b7008..a1f0a7980 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -422,9 +422,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] @@ -460,9 +460,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" dependencies = [ "cfg-if 1.0.0", ] @@ -494,9 +494,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.97" +version = "0.2.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" [[package]] name = "libloading" @@ -662,9 +662,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pin-project-lite" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" [[package]] name = "pin-utils" @@ -994,9 +994,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" +checksum = "4ac2e1d4bd0f75279cfd5a076e0d578bbf02c22b7c39e766c437dd49b3ec43e0" dependencies = [ "tinyvec_macros", ] @@ -1029,9 +1029,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37" +checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" dependencies = [ "proc-macro2", "quote", diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 1c2a49b52..fd34f45d2 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -247,6 +247,26 @@ impl Client { content_format: Some(vec![lsp::MarkupKind::Markdown]), ..Default::default() }), + code_action: Some(lsp::CodeActionClientCapabilities { + code_action_literal_support: Some(lsp::CodeActionLiteralSupport { + code_action_kind: lsp::CodeActionKindLiteralSupport { + value_set: [ + lsp::CodeActionKind::EMPTY, + lsp::CodeActionKind::QUICKFIX, + lsp::CodeActionKind::REFACTOR, + lsp::CodeActionKind::REFACTOR_EXTRACT, + lsp::CodeActionKind::REFACTOR_INLINE, + lsp::CodeActionKind::REFACTOR_REWRITE, + lsp::CodeActionKind::SOURCE, + lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS, + ] + .iter() + .map(|kind| kind.as_str().to_string()) + .collect(), + }, + }), + ..Default::default() + }), ..Default::default() }), window: Some(lsp::WindowClientCapabilities { @@ -713,4 +733,31 @@ impl Client { self.call::(params) } + + // empty string to get all symbols + pub fn workspace_symbols(&self, query: String) -> impl Future> { + let params = lsp::WorkspaceSymbolParams { + query, + work_done_progress_params: lsp::WorkDoneProgressParams::default(), + partial_result_params: lsp::PartialResultParams::default(), + }; + + self.call::(params) + } + + pub fn code_actions( + &self, + text_document: lsp::TextDocumentIdentifier, + range: lsp::Range, + ) -> impl Future> { + let params = lsp::CodeActionParams { + text_document, + range, + context: lsp::CodeActionContext::default(), + work_done_progress_params: lsp::WorkDoneProgressParams::default(), + partial_result_params: lsp::PartialResultParams::default(), + }; + + self.call::(params) + } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 9b72a8e90..24c53897c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -9,8 +9,8 @@ use helix_core::{ object, pos_at_coords, regex::{self, Regex}, register::Register, - search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes, - RopeSlice, Selection, SmallVec, Tendril, Transaction, + search, selection, surround, textobject, Change, LineEnding, Position, Range, Rope, + RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril, Transaction, }; use helix_view::{ @@ -215,6 +215,7 @@ impl Command { append_mode, command_mode, file_picker, + code_action, buffer_picker, symbol_picker, last_picker, @@ -2092,6 +2093,120 @@ fn symbol_picker(cx: &mut Context) { ) } +pub fn code_action(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + + let language_server = match doc.language_server() { + Some(language_server) => language_server, + None => return, + }; + + let range = range_to_lsp_range( + doc.text(), + doc.selection(view.id).primary(), + language_server.offset_encoding(), + ); + + let future = language_server.code_actions(doc.identifier(), range); + let offset_encoding = language_server.offset_encoding(); + + cx.callback( + future, + move |_editor: &mut Editor, + compositor: &mut Compositor, + response: Option| { + if let Some(actions) = response { + let picker = Picker::new( + actions, + |action| match action { + lsp::CodeActionOrCommand::CodeAction(action) => { + action.title.as_str().into() + } + lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(), + }, + move |editor, code_action, _action| match code_action { + lsp::CodeActionOrCommand::Command(command) => { + log::debug!("code action command: {:?}", command); + editor.set_error(String::from("Handling code action command is not implemented yet, see https://github.com/helix-editor/helix/issues/183")); + } + lsp::CodeActionOrCommand::CodeAction(code_action) => { + log::debug!("code action: {:?}", code_action); + if let Some(ref workspace_edit) = code_action.edit { + apply_workspace_edit(editor, offset_encoding, workspace_edit) + } + } + }, + ); + compositor.push(Box::new(picker)) + } + }, + ) +} + +fn apply_workspace_edit( + editor: &mut Editor, + offset_encoding: OffsetEncoding, + workspace_edit: &lsp::WorkspaceEdit, +) { + let edits_to_transaction = |doc: &Rope, edits: &Vec<&lsp::TextEdit>| { + let lsp_pos_to_pos = |lsp_pos| lsp_pos_to_pos(&doc, lsp_pos, offset_encoding).unwrap(); + let changes = edits.iter().map(|edit| -> Change { + log::debug!("text edit: {:?}", edit); + // This clone probably could be optimized if Picker::new would give T instead of &T + let text_replacement = Tendril::from(edit.new_text.clone()); + ( + lsp_pos_to_pos(edit.range.start), + lsp_pos_to_pos(edit.range.end), + Some(text_replacement), + ) + }); + Transaction::change(doc, changes) + }; + + if let Some(ref changes) = workspace_edit.changes { + log::debug!("workspace changes: {:?}", changes); + editor.set_error(String::from("Handling workspace changesis not implemented yet, see https://github.com/helix-editor/helix/issues/183")); + return; + // Not sure if it works properly, it'll be safer to just panic here to avoid breaking some parts of code on which code actions will be used + // TODO: find some example that uses workspace changes, and test it + // for (url, edits) in changes.iter() { + // let file_path = url.origin().ascii_serialization(); + // let file_path = std::path::PathBuf::from(file_path); + // let file = std::fs::File::open(file_path).unwrap(); + // let mut text = Rope::from_reader(file).unwrap(); + // let transaction = edits_to_changes(&text, edits); + // transaction.apply(&mut text); + // } + } + + if let Some(ref document_changes) = workspace_edit.document_changes { + match document_changes { + lsp::DocumentChanges::Edits(document_edits) => { + for document_edit in document_edits { + let (view, doc) = current!(editor); + assert_eq!(doc.url().unwrap(), document_edit.text_document.uri); + let edits = document_edit + .edits + .iter() + .map(|edit| match edit { + lsp::OneOf::Left(text_edit) => text_edit, + lsp::OneOf::Right(annotated_text_edit) => { + &annotated_text_edit.text_edit + } + }) + .collect(); + let transaction = edits_to_transaction(doc.text(), &edits); + doc.apply(&transaction, view.id); + } + } + lsp::DocumentChanges::Operations(operations) => { + log::debug!("document changes - operations: {:?}", operations); + editor.set_error(String::from("Handling document operations is not implemented yet, see https://github.com/helix-editor/helix/issues/183")); + } + } + } +} + fn last_picker(cx: &mut Context) { // TODO: last picker does not seemed to work well with buffer_picker cx.callback = Some(Box::new(|compositor: &mut Compositor| { @@ -3781,6 +3896,8 @@ mode_info! { "P" => paste_clipboard_before, /// replace selections with clipboard "R" => replace_selections_with_clipboard, + /// perform code action + "a" => code_action, /// keep primary selection "space" => keep_primary_selection, }