on master help sign in
feat: code actions - document edits (#478)

* 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 <pickfire@riseup.net> * 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 <blaz@mxxn.io> Co-authored-by: Ivan Tham <pickfire@riseup.net>

commit48e344a2a813b875ca813be726387692b7ba0762 authorGrzegorz Baranski <gbaranski19@gmail.com> dateSat 24 Jul 2021, 03:26 (5 years ago) parentbda4f5c
3 files changed+178−14
Cargo.lock+12−12 helix-lsp/src/client.rs+47 helix-term/src/commands.rs+119−2
Cargo.lock+12−12
@@ -422,9 +422,9 @@ dependencies = [
422422
423423[[package]]
424424name = "hermit-abi"
425−version = "0.1.18"
425+version = "0.1.19"
426426source = "registry+https://github.com/rust-lang/crates.io-index"
427−checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
427+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
428428dependencies = [
429429 "libc",
430430]
@@ -460,9 +460,9 @@ dependencies = [
460460
461461[[package]]
462462name = "instant"
463−version = "0.1.9"
463+version = "0.1.10"
464464source = "registry+https://github.com/rust-lang/crates.io-index"
465−checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec"
465+checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d"
466466dependencies = [
467467 "cfg-if 1.0.0",
468468]
@@ -494,9 +494,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
494494
495495[[package]]
496496name = "libc"
497−version = "0.2.97"
497+version = "0.2.98"
498498source = "registry+https://github.com/rust-lang/crates.io-index"
499−checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6"
499+checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790"
500500
501501[[package]]
502502name = "libloading"
@@ -662,9 +662,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
662662
663663[[package]]
664664name = "pin-project-lite"
665−version = "0.2.6"
665+version = "0.2.7"
666666source = "registry+https://github.com/rust-lang/crates.io-index"
667−checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905"
667+checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
668668
669669[[package]]
670670name = "pin-utils"
@@ -994,9 +994,9 @@ dependencies = [
994994
995995[[package]]
996996name = "tinyvec"
997−version = "1.2.0"
997+version = "1.3.0"
998998source = "registry+https://github.com/rust-lang/crates.io-index"
999−checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342"
999+checksum = "4ac2e1d4bd0f75279cfd5a076e0d578bbf02c22b7c39e766c437dd49b3ec43e0"
10001000dependencies = [
10011001 "tinyvec_macros",
10021002]
@@ -1029,9 +1029,9 @@ dependencies = [
10291029
10301030[[package]]
10311031name = "tokio-macros"
1032−version = "1.2.0"
1032+version = "1.3.0"
10331033source = "registry+https://github.com/rust-lang/crates.io-index"
1034−checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37"
1034+checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110"
10351035dependencies = [
10361036 "proc-macro2",
10371037 "quote",
helix-lsp/src/client.rs+47
@@ -247,6 +247,26 @@ impl Client {
247247 content_format: Some(vec![lsp::MarkupKind::Markdown]),
248248 ..Default::default()
249249 }),
250+ code_action: Some(lsp::CodeActionClientCapabilities {
251+ code_action_literal_support: Some(lsp::CodeActionLiteralSupport {
252+ code_action_kind: lsp::CodeActionKindLiteralSupport {
253+ value_set: [
254+ lsp::CodeActionKind::EMPTY,
255+ lsp::CodeActionKind::QUICKFIX,
256+ lsp::CodeActionKind::REFACTOR,
257+ lsp::CodeActionKind::REFACTOR_EXTRACT,
258+ lsp::CodeActionKind::REFACTOR_INLINE,
259+ lsp::CodeActionKind::REFACTOR_REWRITE,
260+ lsp::CodeActionKind::SOURCE,
261+ lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
262+ ]
263+ .iter()
264+ .map(|kind| kind.as_str().to_string())
265+ .collect(),
266+ },
267+ }),
268+ ..Default::default()
269+ }),
250270 ..Default::default()
251271 }),
252272 window: Some(lsp::WindowClientCapabilities {
@@ -713,4 +733,31 @@ impl Client {
713733
714734 self.call::<lsp::request::DocumentSymbolRequest>(params)
715735 }
736+
737+ // empty string to get all symbols
738+ pub fn workspace_symbols(&self, query: String) -> impl Future<Output = Result<Value>> {
739+ let params = lsp::WorkspaceSymbolParams {
740+ query,
741+ work_done_progress_params: lsp::WorkDoneProgressParams::default(),
742+ partial_result_params: lsp::PartialResultParams::default(),
743+ };
744+
745+ self.call::<lsp::request::WorkspaceSymbol>(params)
746+ }
747+
748+ pub fn code_actions(
749+ &self,
750+ text_document: lsp::TextDocumentIdentifier,
751+ range: lsp::Range,
752+ ) -> impl Future<Output = Result<Value>> {
753+ let params = lsp::CodeActionParams {
754+ text_document,
755+ range,
756+ context: lsp::CodeActionContext::default(),
757+ work_done_progress_params: lsp::WorkDoneProgressParams::default(),
758+ partial_result_params: lsp::PartialResultParams::default(),
759+ };
760+
761+ self.call::<lsp::request::CodeActionRequest>(params)
762+ }
716763}
helix-term/src/commands.rs+119−2
@@ -9,8 +9,8 @@ use helix_core::{
99 object, pos_at_coords,
1010 regex::{self, Regex},
1111 register::Register,
12− search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes,
13− RopeSlice, Selection, SmallVec, Tendril, Transaction,
12+ search, selection, surround, textobject, Change, LineEnding, Position, Range, Rope,
13+ RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril, Transaction,
1414};
1515
1616use helix_view::{
@@ -215,6 +215,7 @@ impl Command {
215215 append_mode,
216216 command_mode,
217217 file_picker,
218+ code_action,
218219 buffer_picker,
219220 symbol_picker,
220221 last_picker,
@@ -2092,6 +2093,120 @@ fn symbol_picker(cx: &mut Context) {
20922093 )
20932094}
20942095
2096+pub fn code_action(cx: &mut Context) {
2097+ let (view, doc) = current!(cx.editor);
2098+
2099+ let language_server = match doc.language_server() {
2100+ Some(language_server) => language_server,
2101+ None => return,
2102+ };
2103+
2104+ let range = range_to_lsp_range(
2105+ doc.text(),
2106+ doc.selection(view.id).primary(),
2107+ language_server.offset_encoding(),
2108+ );
2109+
2110+ let future = language_server.code_actions(doc.identifier(), range);
2111+ let offset_encoding = language_server.offset_encoding();
2112+
2113+ cx.callback(
2114+ future,
2115+ move |_editor: &mut Editor,
2116+ compositor: &mut Compositor,
2117+ response: Option<lsp::CodeActionResponse>| {
2118+ if let Some(actions) = response {
2119+ let picker = Picker::new(
2120+ actions,
2121+ |action| match action {
2122+ lsp::CodeActionOrCommand::CodeAction(action) => {
2123+ action.title.as_str().into()
2124+ }
2125+ lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
2126+ },
2127+ move |editor, code_action, _action| match code_action {
2128+ lsp::CodeActionOrCommand::Command(command) => {
2129+ log::debug!("code action command: {:?}", command);
2130+ editor.set_error(String::from("Handling code action command is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
2131+ }
2132+ lsp::CodeActionOrCommand::CodeAction(code_action) => {
2133+ log::debug!("code action: {:?}", code_action);
2134+ if let Some(ref workspace_edit) = code_action.edit {
2135+ apply_workspace_edit(editor, offset_encoding, workspace_edit)
2136+ }
2137+ }
2138+ },
2139+ );
2140+ compositor.push(Box::new(picker))
2141+ }
2142+ },
2143+ )
2144+}
2145+
2146+fn apply_workspace_edit(
2147+ editor: &mut Editor,
2148+ offset_encoding: OffsetEncoding,
2149+ workspace_edit: &lsp::WorkspaceEdit,
2150+) {
2151+ let edits_to_transaction = |doc: &Rope, edits: &Vec<&lsp::TextEdit>| {
2152+ let lsp_pos_to_pos = |lsp_pos| lsp_pos_to_pos(&doc, lsp_pos, offset_encoding).unwrap();
2153+ let changes = edits.iter().map(|edit| -> Change {
2154+ log::debug!("text edit: {:?}", edit);
2155+ // This clone probably could be optimized if Picker::new would give T instead of &T
2156+ let text_replacement = Tendril::from(edit.new_text.clone());
2157+ (
2158+ lsp_pos_to_pos(edit.range.start),
2159+ lsp_pos_to_pos(edit.range.end),
2160+ Some(text_replacement),
2161+ )
2162+ });
2163+ Transaction::change(doc, changes)
2164+ };
2165+
2166+ if let Some(ref changes) = workspace_edit.changes {
2167+ log::debug!("workspace changes: {:?}", changes);
2168+ editor.set_error(String::from("Handling workspace changesis not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
2169+ return;
2170+ // 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
2171+ // TODO: find some example that uses workspace changes, and test it
2172+ // for (url, edits) in changes.iter() {
2173+ // let file_path = url.origin().ascii_serialization();
2174+ // let file_path = std::path::PathBuf::from(file_path);
2175+ // let file = std::fs::File::open(file_path).unwrap();
2176+ // let mut text = Rope::from_reader(file).unwrap();
2177+ // let transaction = edits_to_changes(&text, edits);
2178+ // transaction.apply(&mut text);
2179+ // }
2180+ }
2181+
2182+ if let Some(ref document_changes) = workspace_edit.document_changes {
2183+ match document_changes {
2184+ lsp::DocumentChanges::Edits(document_edits) => {
2185+ for document_edit in document_edits {
2186+ let (view, doc) = current!(editor);
2187+ assert_eq!(doc.url().unwrap(), document_edit.text_document.uri);
2188+ let edits = document_edit
2189+ .edits
2190+ .iter()
2191+ .map(|edit| match edit {
2192+ lsp::OneOf::Left(text_edit) => text_edit,
2193+ lsp::OneOf::Right(annotated_text_edit) => {
2194+ &annotated_text_edit.text_edit
2195+ }
2196+ })
2197+ .collect();
2198+ let transaction = edits_to_transaction(doc.text(), &edits);
2199+ doc.apply(&transaction, view.id);
2200+ }
2201+ }
2202+ lsp::DocumentChanges::Operations(operations) => {
2203+ log::debug!("document changes - operations: {:?}", operations);
2204+ editor.set_error(String::from("Handling document operations is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
2205+ }
2206+ }
2207+ }
2208+}
2209+
20952210fn last_picker(cx: &mut Context) {
20962211 // TODO: last picker does not seemed to work well with buffer_picker
20972212 cx.callback = Some(Box::new(|compositor: &mut Compositor| {
@@ -3781,6 +3896,8 @@ mode_info! {
37813896 "P" => paste_clipboard_before,
37823897 /// replace selections with clipboard
37833898 "R" => replace_selections_with_clipboard,
3899+ /// perform code action
3900+ "a" => code_action,
37843901 /// keep primary selection
37853902 "space" => keep_primary_selection,
37863903}