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 12 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)
}
}
150 changes: 148 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,
prepend_to_line,
Expand Down Expand Up @@ -2091,6 +2092,149 @@ fn symbol_picker(cx: &mut Context) {
)
}

pub fn workspace_symbol_picker(cx: &mut Context) {
let (view, doc) = current!(cx.editor);

let language_server = match doc.language_server() {
Some(language_server) => language_server,
None => return,
};
let offset_encoding = language_server.offset_encoding();

let future = language_server.workspace_symbols("".to_string());

cx.callback(
future,
move |editor: &mut Editor,
compositor: &mut Compositor,
response: Option<Vec<lsp::SymbolInformation>>| {
if let Some(symbols) = response {
let picker = Picker::new(
symbols,
|symbol| (&symbol.name).into(),
move |editor: &mut Editor, symbol, _action| {
push_jump(editor);
let (view, doc) = current!(editor);

// if let Some(range) =
// lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding)
// {
// doc.set_selection(view.id, Selection::single(range.to(), range.from()));
// align_view(doc, view, Align::Center);
// }
},
);
compositor.push(Box::new(picker))
}
},
)
}

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().clone();

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| {
make_code_action_callback(editor, code_action, offset_encoding)
},
);
compositor.push(Box::new(picker))
}
},
)
}

fn make_code_action_callback(
editor: &mut Editor,
code_action: &lsp::CodeActionOrCommand,
offset_encoding: OffsetEncoding,
) {
match code_action {
lsp::CodeActionOrCommand::Command(command) => {
todo!("command: {:?}", command);
gbaranski marked this conversation as resolved.
Show resolved Hide resolved
}
lsp::CodeActionOrCommand::CodeAction(code_action) => {
log::debug!("code action: {:?}", code_action);
if let Some(ref edit) = code_action.edit {
gbaranski marked this conversation as resolved.
Show resolved Hide resolved
if let Some(ref changes) = edit.document_changes {
match changes {
lsp::DocumentChanges::Edits(document_edits) => {
for document_edit in document_edits {
apply_edits(
editor,
&document_edit.text_document,
offset_encoding,
&document_edit.edits,
);
}
}
lsp::DocumentChanges::Operations(operations) => {
todo!("operations: {:?}", operations)
gbaranski marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
}
}
}

fn apply_edits(
editor: &mut Editor,
edited_document: &lsp::OptionalVersionedTextDocumentIdentifier,
offset_encoding: OffsetEncoding,
edits: &Vec<lsp::OneOf<lsp::TextEdit, lsp::AnnotatedTextEdit>>,
) {
let (view, doc) = current!(editor);
assert_eq!(doc.url().unwrap(), edited_document.uri);
let lsp_pos_to_pos = |lsp_pos| lsp_pos_to_pos(doc.text(), lsp_pos, offset_encoding).unwrap();

let changes = edits
.iter()
.map(|edit| match edit {
lsp::OneOf::Left(text_edit) => text_edit,
lsp::OneOf::Right(annotated_text_edit) => &annotated_text_edit.text_edit, // TODO: Handle annotations
})
.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.clone().into()),
)
});
let transaction = Transaction::change(doc.text(), changes);
doc.apply(&transaction, view.id);
}

// I inserts at the first nonwhitespace character of each line with a selection
fn prepend_to_line(cx: &mut Context) {
goto_first_nonwhitespace(cx);
Expand Down Expand Up @@ -3767,6 +3911,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