Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: code actions - document edits #478

Merged
merged 18 commits into from
Jul 24, 2021
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions helix-lsp/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -713,4 +733,31 @@ impl Client {

self.call::<lsp::request::DocumentSymbolRequest>(params)
}

// empty string to get all symbols
pub fn workspace_symbols(&self, query: String) -> impl Future<Output = Result<Value>> {
let params = lsp::WorkspaceSymbolParams {
query,
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
partial_result_params: lsp::PartialResultParams::default(),
};

self.call::<lsp::request::WorkspaceSymbol>(params)
}

pub fn code_actions(
&self,
text_document: lsp::TextDocumentIdentifier,
range: lsp::Range,
) -> impl Future<Output = Result<Value>> {
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::<lsp::request::CodeActionRequest>(params)
}
}
117 changes: 115 additions & 2 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -215,6 +215,7 @@ impl Command {
append_mode,
command_mode,
file_picker,
code_action,
buffer_picker,
symbol_picker,
last_picker,
Expand Down Expand Up @@ -2092,6 +2093,116 @@ 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<lsp::CodeActionResponse>| {
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) => {
todo!("command: {:?}", command);
}
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 {
todo!("workspace changes: {:?}", changes);
// 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undo doesn't work after applying a change:

Suggested change
doc.apply(&transaction, view.id);
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view.id);

Copy link
Contributor Author

@gbaranski gbaranski Jul 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It did work after pressing ESC, but with this change works even without it. I can't commit changes since the PR have been already merged. Maybe you could create a new PR with this change?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ESC is bound to normal_mode command (even inside normal mode) and it has an append_changes_to_history call. I see you created a new PR 👍🏾

}
}
lsp::DocumentChanges::Operations(operations) => {
todo!("operations: {:?}", operations)
}
}
}
}

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| {
Expand Down Expand Up @@ -3781,6 +3892,8 @@ mode_info! {
"P" => paste_clipboard_before,
/// replace selections with clipboard
"R" => replace_selections_with_clipboard,
/// perform code action
"a" => code_action,
gbaranski marked this conversation as resolved.
Show resolved Hide resolved
/// keep primary selection
"space" => keep_primary_selection,
}
Expand Down