mirror of
https://github.com/helix-editor/helix.git
synced 2025-01-19 21:47:07 +04:00
Move most LSP specific commmands to commands::lsp
This commit is contained in:
parent
7b1d682fe5
commit
504d5ce8bd
@ -1,6 +1,8 @@
|
|||||||
pub(crate) mod dap;
|
pub(crate) mod dap;
|
||||||
|
pub(crate) mod lsp;
|
||||||
|
|
||||||
pub use dap::*;
|
pub use dap::*;
|
||||||
|
pub use lsp::*;
|
||||||
|
|
||||||
use helix_core::{
|
use helix_core::{
|
||||||
comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes,
|
comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes,
|
||||||
@ -33,11 +35,6 @@
|
|||||||
|
|
||||||
use anyhow::{anyhow, bail, ensure, Context as _};
|
use anyhow::{anyhow, bail, ensure, Context as _};
|
||||||
use fuzzy_matcher::FuzzyMatcher;
|
use fuzzy_matcher::FuzzyMatcher;
|
||||||
use helix_lsp::{
|
|
||||||
block_on, lsp,
|
|
||||||
util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range},
|
|
||||||
OffsetEncoding,
|
|
||||||
};
|
|
||||||
use insert::*;
|
use insert::*;
|
||||||
use movement::Movement;
|
use movement::Movement;
|
||||||
|
|
||||||
@ -2754,7 +2751,7 @@ fn debug_eval(
|
|||||||
// TODO: support no frame_id
|
// TODO: support no frame_id
|
||||||
|
|
||||||
let frame_id = debugger.stack_frames[&thread_id][frame].id;
|
let frame_id = debugger.stack_frames[&thread_id][frame].id;
|
||||||
let response = block_on(debugger.eval(args.join(" "), Some(frame_id)))?;
|
let response = helix_lsp::block_on(debugger.eval(args.join(" "), Some(frame_id)))?;
|
||||||
cx.editor.set_status(response.result);
|
cx.editor.set_status(response.result);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -3474,217 +3471,6 @@ fn format(&self) -> Cow<str> {
|
|||||||
cx.push_layer(Box::new(overlayed(picker)));
|
cx.push_layer(Box::new(overlayed(picker)));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sym_picker(
|
|
||||||
symbols: Vec<lsp::SymbolInformation>,
|
|
||||||
current_path: Option<lsp::Url>,
|
|
||||||
offset_encoding: OffsetEncoding,
|
|
||||||
) -> FilePicker<lsp::SymbolInformation> {
|
|
||||||
// TODO: drop current_path comparison and instead use workspace: bool flag?
|
|
||||||
let current_path2 = current_path.clone();
|
|
||||||
let mut picker = FilePicker::new(
|
|
||||||
symbols,
|
|
||||||
move |symbol| {
|
|
||||||
if current_path.as_ref() == Some(&symbol.location.uri) {
|
|
||||||
symbol.name.as_str().into()
|
|
||||||
} else {
|
|
||||||
let path = symbol.location.uri.to_file_path().unwrap();
|
|
||||||
let relative_path = helix_core::path::get_relative_path(path.as_path())
|
|
||||||
.to_string_lossy()
|
|
||||||
.into_owned();
|
|
||||||
format!("{} ({})", &symbol.name, relative_path).into()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
move |cx, symbol, action| {
|
|
||||||
if current_path2.as_ref() == Some(&symbol.location.uri) {
|
|
||||||
push_jump(cx.editor);
|
|
||||||
} else {
|
|
||||||
let path = symbol.location.uri.to_file_path().unwrap();
|
|
||||||
cx.editor.open(path, action).expect("editor.open failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
let (view, doc) = current!(cx.editor);
|
|
||||||
|
|
||||||
if let Some(range) =
|
|
||||||
lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding)
|
|
||||||
{
|
|
||||||
// we flip the range so that the cursor sits on the start of the symbol
|
|
||||||
// (for example start of the function).
|
|
||||||
doc.set_selection(view.id, Selection::single(range.head, range.anchor));
|
|
||||||
align_view(doc, view, Align::Center);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
move |_editor, symbol| {
|
|
||||||
let path = symbol.location.uri.to_file_path().unwrap();
|
|
||||||
let line = Some((
|
|
||||||
symbol.location.range.start.line as usize,
|
|
||||||
symbol.location.range.end.line as usize,
|
|
||||||
));
|
|
||||||
Some((path, line))
|
|
||||||
},
|
|
||||||
);
|
|
||||||
picker.truncate_start = false;
|
|
||||||
picker
|
|
||||||
}
|
|
||||||
|
|
||||||
fn symbol_picker(cx: &mut Context) {
|
|
||||||
fn nested_to_flat(
|
|
||||||
list: &mut Vec<lsp::SymbolInformation>,
|
|
||||||
file: &lsp::TextDocumentIdentifier,
|
|
||||||
symbol: lsp::DocumentSymbol,
|
|
||||||
) {
|
|
||||||
#[allow(deprecated)]
|
|
||||||
list.push(lsp::SymbolInformation {
|
|
||||||
name: symbol.name,
|
|
||||||
kind: symbol.kind,
|
|
||||||
tags: symbol.tags,
|
|
||||||
deprecated: symbol.deprecated,
|
|
||||||
location: lsp::Location::new(file.uri.clone(), symbol.selection_range),
|
|
||||||
container_name: None,
|
|
||||||
});
|
|
||||||
for child in symbol.children.into_iter().flatten() {
|
|
||||||
nested_to_flat(list, file, child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let doc = doc!(cx.editor);
|
|
||||||
|
|
||||||
let language_server = match doc.language_server() {
|
|
||||||
Some(language_server) => language_server,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
let current_url = doc.url();
|
|
||||||
let offset_encoding = language_server.offset_encoding();
|
|
||||||
|
|
||||||
let future = language_server.document_symbols(doc.identifier());
|
|
||||||
|
|
||||||
cx.callback(
|
|
||||||
future,
|
|
||||||
move |editor: &mut Editor,
|
|
||||||
compositor: &mut Compositor,
|
|
||||||
response: Option<lsp::DocumentSymbolResponse>| {
|
|
||||||
if let Some(symbols) = response {
|
|
||||||
// lsp has two ways to represent symbols (flat/nested)
|
|
||||||
// convert the nested variant to flat, so that we have a homogeneous list
|
|
||||||
let symbols = match symbols {
|
|
||||||
lsp::DocumentSymbolResponse::Flat(symbols) => symbols,
|
|
||||||
lsp::DocumentSymbolResponse::Nested(symbols) => {
|
|
||||||
let doc = doc!(editor);
|
|
||||||
let mut flat_symbols = Vec::new();
|
|
||||||
for symbol in symbols {
|
|
||||||
nested_to_flat(&mut flat_symbols, &doc.identifier(), symbol)
|
|
||||||
}
|
|
||||||
flat_symbols
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let picker = sym_picker(symbols, current_url, offset_encoding);
|
|
||||||
compositor.push(Box::new(overlayed(picker)))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn workspace_symbol_picker(cx: &mut Context) {
|
|
||||||
let doc = doc!(cx.editor);
|
|
||||||
let current_url = doc.url();
|
|
||||||
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 = sym_picker(symbols, current_url, offset_encoding);
|
|
||||||
compositor.push(Box::new(overlayed(picker)))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ui::menu::Item for lsp::CodeActionOrCommand {
|
|
||||||
fn label(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str(),
|
|
||||||
lsp::CodeActionOrCommand::Command(command) => command.title.as_str(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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>| {
|
|
||||||
let actions = match response {
|
|
||||||
Some(a) => a,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
if actions.is_empty() {
|
|
||||||
editor.set_status("No code actions available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut picker = ui::Menu::new(actions, move |editor, code_action, event| {
|
|
||||||
if event != PromptEvent::Validate {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// always present here
|
|
||||||
let code_action = code_action.unwrap();
|
|
||||||
|
|
||||||
match code_action {
|
|
||||||
lsp::CodeActionOrCommand::Command(command) => {
|
|
||||||
log::debug!("code action command: {:?}", command);
|
|
||||||
execute_lsp_command(editor, command.clone());
|
|
||||||
}
|
|
||||||
lsp::CodeActionOrCommand::CodeAction(code_action) => {
|
|
||||||
log::debug!("code action: {:?}", code_action);
|
|
||||||
if let Some(ref workspace_edit) = code_action.edit {
|
|
||||||
log::debug!("edit: {:?}", workspace_edit);
|
|
||||||
apply_workspace_edit(editor, offset_encoding, workspace_edit);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if code action provides both edit and command first the edit
|
|
||||||
// should be applied and then the command
|
|
||||||
if let Some(command) = &code_action.command {
|
|
||||||
execute_lsp_command(editor, command.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
picker.move_down(); // pre-select the first item
|
|
||||||
|
|
||||||
let popup = Popup::new("code-action", picker).margin(helix_view::graphics::Margin {
|
|
||||||
vertical: 1,
|
|
||||||
horizontal: 1,
|
|
||||||
});
|
|
||||||
compositor.replace_or_push("code-action", Box::new(popup));
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn command_palette(cx: &mut Context) {
|
pub fn command_palette(cx: &mut Context) {
|
||||||
cx.callback = Some(Box::new(
|
cx.callback = Some(Box::new(
|
||||||
move |compositor: &mut Compositor, cx: &mut compositor::Context| {
|
move |compositor: &mut Compositor, cx: &mut compositor::Context| {
|
||||||
@ -3748,180 +3534,6 @@ pub fn command_palette(cx: &mut Context) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
|
|
||||||
let doc = doc!(editor);
|
|
||||||
let language_server = match doc.language_server() {
|
|
||||||
Some(language_server) => language_server,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
// the command is executed on the server and communicated back
|
|
||||||
// to the client asynchronously using workspace edits
|
|
||||||
let command_future = language_server.command(cmd);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let res = command_future.await;
|
|
||||||
|
|
||||||
if let Err(e) = res {
|
|
||||||
log::error!("execute LSP command: {}", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
|
|
||||||
use lsp::ResourceOp;
|
|
||||||
use std::fs;
|
|
||||||
match op {
|
|
||||||
ResourceOp::Create(op) => {
|
|
||||||
let path = op.uri.to_file_path().unwrap();
|
|
||||||
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
|
|
||||||
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
|
|
||||||
});
|
|
||||||
if ignore_if_exists && path.exists() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
fs::write(&path, [])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ResourceOp::Delete(op) => {
|
|
||||||
let path = op.uri.to_file_path().unwrap();
|
|
||||||
if path.is_dir() {
|
|
||||||
let recursive = op
|
|
||||||
.options
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|options| options.recursive)
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
if recursive {
|
|
||||||
fs::remove_dir_all(&path)
|
|
||||||
} else {
|
|
||||||
fs::remove_dir(&path)
|
|
||||||
}
|
|
||||||
} else if path.is_file() {
|
|
||||||
fs::remove_file(&path)
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ResourceOp::Rename(op) => {
|
|
||||||
let from = op.old_uri.to_file_path().unwrap();
|
|
||||||
let to = op.new_uri.to_file_path().unwrap();
|
|
||||||
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
|
|
||||||
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
|
|
||||||
});
|
|
||||||
if ignore_if_exists && to.exists() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
fs::rename(&from, &to)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply_workspace_edit(
|
|
||||||
editor: &mut Editor,
|
|
||||||
offset_encoding: OffsetEncoding,
|
|
||||||
workspace_edit: &lsp::WorkspaceEdit,
|
|
||||||
) {
|
|
||||||
let mut apply_edits = |uri: &helix_lsp::Url, text_edits: Vec<lsp::TextEdit>| {
|
|
||||||
let path = uri
|
|
||||||
.to_file_path()
|
|
||||||
.expect("unable to convert URI to filepath");
|
|
||||||
|
|
||||||
let current_view_id = view!(editor).id;
|
|
||||||
let doc_id = editor.open(path, Action::Load).unwrap();
|
|
||||||
let doc = editor
|
|
||||||
.document_mut(doc_id)
|
|
||||||
.expect("Document for document_changes not found");
|
|
||||||
|
|
||||||
// Need to determine a view for apply/append_changes_to_history
|
|
||||||
let selections = doc.selections();
|
|
||||||
let view_id = if selections.contains_key(¤t_view_id) {
|
|
||||||
// use current if possible
|
|
||||||
current_view_id
|
|
||||||
} else {
|
|
||||||
// Hack: we take the first available view_id
|
|
||||||
selections
|
|
||||||
.keys()
|
|
||||||
.next()
|
|
||||||
.copied()
|
|
||||||
.expect("No view_id available")
|
|
||||||
};
|
|
||||||
|
|
||||||
let transaction = helix_lsp::util::generate_transaction_from_edits(
|
|
||||||
doc.text(),
|
|
||||||
text_edits,
|
|
||||||
offset_encoding,
|
|
||||||
);
|
|
||||||
doc.apply(&transaction, view_id);
|
|
||||||
doc.append_changes_to_history(view_id);
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(ref changes) = workspace_edit.changes {
|
|
||||||
log::debug!("workspace changes: {:?}", changes);
|
|
||||||
for (uri, text_edits) in changes {
|
|
||||||
let text_edits = text_edits.to_vec();
|
|
||||||
apply_edits(uri, text_edits);
|
|
||||||
}
|
|
||||||
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 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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
apply_edits(&document_edit.text_document.uri, edits);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lsp::DocumentChanges::Operations(operations) => {
|
|
||||||
log::debug!("document changes - operations: {:?}", operations);
|
|
||||||
for operateion in operations {
|
|
||||||
match operateion {
|
|
||||||
lsp::DocumentChangeOperation::Op(op) => {
|
|
||||||
apply_document_resource_op(op).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
lsp::DocumentChangeOperation::Edit(document_edit) => {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
apply_edits(&document_edit.text_document.uri, edits);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn last_picker(cx: &mut Context) {
|
fn last_picker(cx: &mut Context) {
|
||||||
// TODO: last picker does not seem to work well with buffer_picker
|
// TODO: last picker does not seem to work well with buffer_picker
|
||||||
cx.callback = Some(Box::new(|compositor: &mut Compositor, _| {
|
cx.callback = Some(Box::new(|compositor: &mut Compositor, _| {
|
||||||
@ -4250,247 +3862,6 @@ fn exit_select_mode(cx: &mut Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn goto_impl(
|
|
||||||
editor: &mut Editor,
|
|
||||||
compositor: &mut Compositor,
|
|
||||||
locations: Vec<lsp::Location>,
|
|
||||||
offset_encoding: OffsetEncoding,
|
|
||||||
) {
|
|
||||||
push_jump(editor);
|
|
||||||
|
|
||||||
// TODO: share with symbol picker(symbol.location)
|
|
||||||
fn jump_to(
|
|
||||||
editor: &mut Editor,
|
|
||||||
location: &lsp::Location,
|
|
||||||
offset_encoding: OffsetEncoding,
|
|
||||||
action: Action,
|
|
||||||
) {
|
|
||||||
let path = location
|
|
||||||
.uri
|
|
||||||
.to_file_path()
|
|
||||||
.expect("unable to convert URI to filepath");
|
|
||||||
let _id = editor.open(path, action).expect("editor.open failed");
|
|
||||||
let (view, doc) = current!(editor);
|
|
||||||
let definition_pos = location.range.start;
|
|
||||||
// TODO: convert inside server
|
|
||||||
let new_pos =
|
|
||||||
if let Some(new_pos) = lsp_pos_to_pos(doc.text(), definition_pos, offset_encoding) {
|
|
||||||
new_pos
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
doc.set_selection(view.id, Selection::point(new_pos));
|
|
||||||
align_view(doc, view, Align::Center);
|
|
||||||
}
|
|
||||||
|
|
||||||
let cwdir = std::env::current_dir().expect("couldn't determine current directory");
|
|
||||||
|
|
||||||
match locations.as_slice() {
|
|
||||||
[location] => {
|
|
||||||
jump_to(editor, location, offset_encoding, Action::Replace);
|
|
||||||
}
|
|
||||||
[] => {
|
|
||||||
editor.set_error("No definition found.");
|
|
||||||
}
|
|
||||||
_locations => {
|
|
||||||
let picker = FilePicker::new(
|
|
||||||
locations,
|
|
||||||
move |location| {
|
|
||||||
let file: Cow<'_, str> = (location.uri.scheme() == "file")
|
|
||||||
.then(|| {
|
|
||||||
location
|
|
||||||
.uri
|
|
||||||
.to_file_path()
|
|
||||||
.map(|path| {
|
|
||||||
// strip root prefix
|
|
||||||
path.strip_prefix(&cwdir)
|
|
||||||
.map(|path| path.to_path_buf())
|
|
||||||
.unwrap_or(path)
|
|
||||||
})
|
|
||||||
.map(|path| Cow::from(path.to_string_lossy().into_owned()))
|
|
||||||
.ok()
|
|
||||||
})
|
|
||||||
.flatten()
|
|
||||||
.unwrap_or_else(|| location.uri.as_str().into());
|
|
||||||
let line = location.range.start.line;
|
|
||||||
format!("{}:{}", file, line).into()
|
|
||||||
},
|
|
||||||
move |cx, location, action| jump_to(cx.editor, location, offset_encoding, action),
|
|
||||||
|_editor, location| {
|
|
||||||
// TODO: share code for symbol.location and location
|
|
||||||
let path = location.uri.to_file_path().unwrap();
|
|
||||||
let line = Some((
|
|
||||||
location.range.start.line as usize,
|
|
||||||
location.range.end.line as usize,
|
|
||||||
));
|
|
||||||
Some((path, line))
|
|
||||||
},
|
|
||||||
);
|
|
||||||
compositor.push(Box::new(overlayed(picker)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn goto_definition(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 pos = pos_to_lsp_pos(
|
|
||||||
doc.text(),
|
|
||||||
doc.selection(view.id)
|
|
||||||
.primary()
|
|
||||||
.cursor(doc.text().slice(..)),
|
|
||||||
offset_encoding,
|
|
||||||
);
|
|
||||||
|
|
||||||
let future = language_server.goto_definition(doc.identifier(), pos, None);
|
|
||||||
|
|
||||||
cx.callback(
|
|
||||||
future,
|
|
||||||
move |editor: &mut Editor,
|
|
||||||
compositor: &mut Compositor,
|
|
||||||
response: Option<lsp::GotoDefinitionResponse>| {
|
|
||||||
let items = match response {
|
|
||||||
Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location],
|
|
||||||
Some(lsp::GotoDefinitionResponse::Array(locations)) => locations,
|
|
||||||
Some(lsp::GotoDefinitionResponse::Link(locations)) => locations
|
|
||||||
.into_iter()
|
|
||||||
.map(|location_link| lsp::Location {
|
|
||||||
uri: location_link.target_uri,
|
|
||||||
range: location_link.target_range,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
None => Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
goto_impl(editor, compositor, items, offset_encoding);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn goto_type_definition(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 pos = pos_to_lsp_pos(
|
|
||||||
doc.text(),
|
|
||||||
doc.selection(view.id)
|
|
||||||
.primary()
|
|
||||||
.cursor(doc.text().slice(..)),
|
|
||||||
offset_encoding,
|
|
||||||
);
|
|
||||||
|
|
||||||
let future = language_server.goto_type_definition(doc.identifier(), pos, None);
|
|
||||||
|
|
||||||
cx.callback(
|
|
||||||
future,
|
|
||||||
move |editor: &mut Editor,
|
|
||||||
compositor: &mut Compositor,
|
|
||||||
response: Option<lsp::GotoDefinitionResponse>| {
|
|
||||||
let items = match response {
|
|
||||||
Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location],
|
|
||||||
Some(lsp::GotoDefinitionResponse::Array(locations)) => locations,
|
|
||||||
Some(lsp::GotoDefinitionResponse::Link(locations)) => locations
|
|
||||||
.into_iter()
|
|
||||||
.map(|location_link| lsp::Location {
|
|
||||||
uri: location_link.target_uri,
|
|
||||||
range: location_link.target_range,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
None => Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
goto_impl(editor, compositor, items, offset_encoding);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn goto_implementation(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 pos = pos_to_lsp_pos(
|
|
||||||
doc.text(),
|
|
||||||
doc.selection(view.id)
|
|
||||||
.primary()
|
|
||||||
.cursor(doc.text().slice(..)),
|
|
||||||
offset_encoding,
|
|
||||||
);
|
|
||||||
|
|
||||||
let future = language_server.goto_implementation(doc.identifier(), pos, None);
|
|
||||||
|
|
||||||
cx.callback(
|
|
||||||
future,
|
|
||||||
move |editor: &mut Editor,
|
|
||||||
compositor: &mut Compositor,
|
|
||||||
response: Option<lsp::GotoDefinitionResponse>| {
|
|
||||||
let items = match response {
|
|
||||||
Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location],
|
|
||||||
Some(lsp::GotoDefinitionResponse::Array(locations)) => locations,
|
|
||||||
Some(lsp::GotoDefinitionResponse::Link(locations)) => locations
|
|
||||||
.into_iter()
|
|
||||||
.map(|location_link| lsp::Location {
|
|
||||||
uri: location_link.target_uri,
|
|
||||||
range: location_link.target_range,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
None => Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
goto_impl(editor, compositor, items, offset_encoding);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn goto_reference(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 pos = pos_to_lsp_pos(
|
|
||||||
doc.text(),
|
|
||||||
doc.selection(view.id)
|
|
||||||
.primary()
|
|
||||||
.cursor(doc.text().slice(..)),
|
|
||||||
offset_encoding,
|
|
||||||
);
|
|
||||||
|
|
||||||
let future = language_server.goto_reference(doc.identifier(), pos, None);
|
|
||||||
|
|
||||||
cx.callback(
|
|
||||||
future,
|
|
||||||
move |editor: &mut Editor,
|
|
||||||
compositor: &mut Compositor,
|
|
||||||
items: Option<Vec<lsp::Location>>| {
|
|
||||||
goto_impl(
|
|
||||||
editor,
|
|
||||||
compositor,
|
|
||||||
items.unwrap_or_default(),
|
|
||||||
offset_encoding,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn goto_pos(editor: &mut Editor, pos: usize) {
|
fn goto_pos(editor: &mut Editor, pos: usize) {
|
||||||
push_jump(editor);
|
push_jump(editor);
|
||||||
|
|
||||||
@ -4565,46 +3936,6 @@ fn goto_prev_diag(cx: &mut Context) {
|
|||||||
goto_pos(editor, pos);
|
goto_pos(editor, pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn signature_help(cx: &mut Context) {
|
|
||||||
let (view, doc) = current!(cx.editor);
|
|
||||||
|
|
||||||
let language_server = match doc.language_server() {
|
|
||||||
Some(language_server) => language_server,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
let pos = pos_to_lsp_pos(
|
|
||||||
doc.text(),
|
|
||||||
doc.selection(view.id)
|
|
||||||
.primary()
|
|
||||||
.cursor(doc.text().slice(..)),
|
|
||||||
language_server.offset_encoding(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let future = language_server.text_document_signature_help(doc.identifier(), pos, None);
|
|
||||||
|
|
||||||
cx.callback(
|
|
||||||
future,
|
|
||||||
move |_editor: &mut Editor,
|
|
||||||
_compositor: &mut Compositor,
|
|
||||||
response: Option<lsp::SignatureHelp>| {
|
|
||||||
if let Some(signature_help) = response {
|
|
||||||
log::info!("{:?}", signature_help);
|
|
||||||
// signatures
|
|
||||||
// active_signature
|
|
||||||
// active_parameter
|
|
||||||
// render as:
|
|
||||||
|
|
||||||
// signature
|
|
||||||
// ----------
|
|
||||||
// doc
|
|
||||||
|
|
||||||
// with active param highlighted
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod insert {
|
pub mod insert {
|
||||||
use super::*;
|
use super::*;
|
||||||
pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
|
pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
|
||||||
@ -4630,6 +3961,7 @@ pub fn idle_completion(cx: &mut Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn language_server_completion(cx: &mut Context, ch: char) {
|
fn language_server_completion(cx: &mut Context, ch: char) {
|
||||||
|
use helix_lsp::lsp;
|
||||||
// if ch matches completion char, trigger completion
|
// if ch matches completion char, trigger completion
|
||||||
let doc = doc_mut!(cx.editor);
|
let doc = doc_mut!(cx.editor);
|
||||||
let language_server = match doc.language_server() {
|
let language_server = match doc.language_server() {
|
||||||
@ -4653,6 +3985,7 @@ fn language_server_completion(cx: &mut Context, ch: char) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn signature_help(cx: &mut Context, ch: char) {
|
fn signature_help(cx: &mut Context, ch: char) {
|
||||||
|
use helix_lsp::lsp;
|
||||||
// if ch matches signature_help char, trigger
|
// if ch matches signature_help char, trigger
|
||||||
let doc = doc_mut!(cx.editor);
|
let doc = doc_mut!(cx.editor);
|
||||||
let language_server = match doc.language_server() {
|
let language_server = match doc.language_server() {
|
||||||
@ -5360,6 +4693,8 @@ fn unindent(cx: &mut Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn format_selections(cx: &mut Context) {
|
fn format_selections(cx: &mut Context) {
|
||||||
|
use helix_lsp::{lsp, util::range_to_lsp_range};
|
||||||
|
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
|
|
||||||
// via lsp if available
|
// via lsp if available
|
||||||
@ -5504,6 +4839,8 @@ fn remove_primary_selection(cx: &mut Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn completion(cx: &mut Context) {
|
pub fn completion(cx: &mut Context) {
|
||||||
|
use helix_lsp::{lsp, util::pos_to_lsp_pos};
|
||||||
|
|
||||||
// trigger on trigger char, or if user calls it
|
// trigger on trigger char, or if user calls it
|
||||||
// (or on word char typing??)
|
// (or on word char typing??)
|
||||||
// after it's triggered, if response marked is_incomplete, update on every subsequent keypress
|
// after it's triggered, if response marked is_incomplete, update on every subsequent keypress
|
||||||
@ -5618,66 +4955,6 @@ pub fn completion(cx: &mut Context) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hover(cx: &mut Context) {
|
|
||||||
let (view, doc) = current!(cx.editor);
|
|
||||||
|
|
||||||
let language_server = match doc.language_server() {
|
|
||||||
Some(language_server) => language_server,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier
|
|
||||||
|
|
||||||
let pos = pos_to_lsp_pos(
|
|
||||||
doc.text(),
|
|
||||||
doc.selection(view.id)
|
|
||||||
.primary()
|
|
||||||
.cursor(doc.text().slice(..)),
|
|
||||||
language_server.offset_encoding(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let future = language_server.text_document_hover(doc.identifier(), pos, None);
|
|
||||||
|
|
||||||
cx.callback(
|
|
||||||
future,
|
|
||||||
move |editor: &mut Editor, compositor: &mut Compositor, response: Option<lsp::Hover>| {
|
|
||||||
if let Some(hover) = response {
|
|
||||||
// hover.contents / .range <- used for visualizing
|
|
||||||
|
|
||||||
fn marked_string_to_markdown(contents: lsp::MarkedString) -> String {
|
|
||||||
match contents {
|
|
||||||
lsp::MarkedString::String(contents) => contents,
|
|
||||||
lsp::MarkedString::LanguageString(string) => {
|
|
||||||
if string.language == "markdown" {
|
|
||||||
string.value
|
|
||||||
} else {
|
|
||||||
format!("```{}\n{}\n```", string.language, string.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let contents = match hover.contents {
|
|
||||||
lsp::HoverContents::Scalar(contents) => marked_string_to_markdown(contents),
|
|
||||||
lsp::HoverContents::Array(contents) => contents
|
|
||||||
.into_iter()
|
|
||||||
.map(marked_string_to_markdown)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n\n"),
|
|
||||||
lsp::HoverContents::Markup(contents) => contents.value,
|
|
||||||
};
|
|
||||||
|
|
||||||
// skip if contents empty
|
|
||||||
|
|
||||||
let contents =
|
|
||||||
ui::Markdown::new(contents, editor.syn_loader.clone()).style_group("hover");
|
|
||||||
let popup = Popup::new("hover", contents);
|
|
||||||
compositor.replace_or_push("hover", Box::new(popup));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// comments
|
// comments
|
||||||
fn toggle_comments(cx: &mut Context) {
|
fn toggle_comments(cx: &mut Context) {
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
@ -6406,43 +5683,6 @@ fn add_newline_impl(cx: &mut Context, open: Open) {
|
|||||||
doc.apply(&transaction, view.id);
|
doc.apply(&transaction, view.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rename_symbol(cx: &mut Context) {
|
|
||||||
let prompt = Prompt::new(
|
|
||||||
"rename-to:".into(),
|
|
||||||
None,
|
|
||||||
ui::completers::none,
|
|
||||||
move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
|
|
||||||
if event != PromptEvent::Validate {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log::debug!("renaming to: {:?}", input);
|
|
||||||
|
|
||||||
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 pos = pos_to_lsp_pos(
|
|
||||||
doc.text(),
|
|
||||||
doc.selection(view.id)
|
|
||||||
.primary()
|
|
||||||
.cursor(doc.text().slice(..)),
|
|
||||||
offset_encoding,
|
|
||||||
);
|
|
||||||
|
|
||||||
let task = language_server.rename_symbol(doc.identifier(), pos, input.to_string());
|
|
||||||
let edits = block_on(task).unwrap_or_default();
|
|
||||||
log::debug!("Edits from LSP: {:?}", edits);
|
|
||||||
apply_workspace_edit(cx.editor, offset_encoding, &edits);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
cx.push_layer(Box::new(prompt));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Increment object under cursor by count.
|
/// Increment object under cursor by count.
|
||||||
fn increment(cx: &mut Context) {
|
fn increment(cx: &mut Context) {
|
||||||
increment_impl(cx, cx.count() as i64);
|
increment_impl(cx, cx.count() as i64);
|
||||||
|
776
helix-term/src/commands/lsp.rs
Normal file
776
helix-term/src/commands/lsp.rs
Normal file
@ -0,0 +1,776 @@
|
|||||||
|
use helix_lsp::{
|
||||||
|
block_on, lsp,
|
||||||
|
util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range},
|
||||||
|
OffsetEncoding,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{align_view, push_jump, Align, Context, Editor};
|
||||||
|
|
||||||
|
use helix_core::Selection;
|
||||||
|
use helix_view::editor::Action;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
compositor::{self, Compositor},
|
||||||
|
ui::{self, overlay::overlayed, FilePicker, Popup, Prompt, PromptEvent},
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
fn sym_picker(
|
||||||
|
symbols: Vec<lsp::SymbolInformation>,
|
||||||
|
current_path: Option<lsp::Url>,
|
||||||
|
offset_encoding: OffsetEncoding,
|
||||||
|
) -> FilePicker<lsp::SymbolInformation> {
|
||||||
|
// TODO: drop current_path comparison and instead use workspace: bool flag?
|
||||||
|
let current_path2 = current_path.clone();
|
||||||
|
let mut picker = FilePicker::new(
|
||||||
|
symbols,
|
||||||
|
move |symbol| {
|
||||||
|
if current_path.as_ref() == Some(&symbol.location.uri) {
|
||||||
|
symbol.name.as_str().into()
|
||||||
|
} else {
|
||||||
|
let path = symbol.location.uri.to_file_path().unwrap();
|
||||||
|
let relative_path = helix_core::path::get_relative_path(path.as_path())
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned();
|
||||||
|
format!("{} ({})", &symbol.name, relative_path).into()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
move |cx, symbol, action| {
|
||||||
|
if current_path2.as_ref() == Some(&symbol.location.uri) {
|
||||||
|
push_jump(cx.editor);
|
||||||
|
} else {
|
||||||
|
let path = symbol.location.uri.to_file_path().unwrap();
|
||||||
|
cx.editor.open(path, action).expect("editor.open failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
let (view, doc) = current!(cx.editor);
|
||||||
|
|
||||||
|
if let Some(range) =
|
||||||
|
lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding)
|
||||||
|
{
|
||||||
|
// we flip the range so that the cursor sits on the start of the symbol
|
||||||
|
// (for example start of the function).
|
||||||
|
doc.set_selection(view.id, Selection::single(range.head, range.anchor));
|
||||||
|
align_view(doc, view, Align::Center);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
move |_editor, symbol| {
|
||||||
|
let path = symbol.location.uri.to_file_path().unwrap();
|
||||||
|
let line = Some((
|
||||||
|
symbol.location.range.start.line as usize,
|
||||||
|
symbol.location.range.end.line as usize,
|
||||||
|
));
|
||||||
|
Some((path, line))
|
||||||
|
},
|
||||||
|
);
|
||||||
|
picker.truncate_start = false;
|
||||||
|
picker
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn symbol_picker(cx: &mut Context) {
|
||||||
|
fn nested_to_flat(
|
||||||
|
list: &mut Vec<lsp::SymbolInformation>,
|
||||||
|
file: &lsp::TextDocumentIdentifier,
|
||||||
|
symbol: lsp::DocumentSymbol,
|
||||||
|
) {
|
||||||
|
#[allow(deprecated)]
|
||||||
|
list.push(lsp::SymbolInformation {
|
||||||
|
name: symbol.name,
|
||||||
|
kind: symbol.kind,
|
||||||
|
tags: symbol.tags,
|
||||||
|
deprecated: symbol.deprecated,
|
||||||
|
location: lsp::Location::new(file.uri.clone(), symbol.selection_range),
|
||||||
|
container_name: None,
|
||||||
|
});
|
||||||
|
for child in symbol.children.into_iter().flatten() {
|
||||||
|
nested_to_flat(list, file, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let doc = doc!(cx.editor);
|
||||||
|
|
||||||
|
let language_server = match doc.language_server() {
|
||||||
|
Some(language_server) => language_server,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let current_url = doc.url();
|
||||||
|
let offset_encoding = language_server.offset_encoding();
|
||||||
|
|
||||||
|
let future = language_server.document_symbols(doc.identifier());
|
||||||
|
|
||||||
|
cx.callback(
|
||||||
|
future,
|
||||||
|
move |editor: &mut Editor,
|
||||||
|
compositor: &mut Compositor,
|
||||||
|
response: Option<lsp::DocumentSymbolResponse>| {
|
||||||
|
if let Some(symbols) = response {
|
||||||
|
// lsp has two ways to represent symbols (flat/nested)
|
||||||
|
// convert the nested variant to flat, so that we have a homogeneous list
|
||||||
|
let symbols = match symbols {
|
||||||
|
lsp::DocumentSymbolResponse::Flat(symbols) => symbols,
|
||||||
|
lsp::DocumentSymbolResponse::Nested(symbols) => {
|
||||||
|
let doc = doc!(editor);
|
||||||
|
let mut flat_symbols = Vec::new();
|
||||||
|
for symbol in symbols {
|
||||||
|
nested_to_flat(&mut flat_symbols, &doc.identifier(), symbol)
|
||||||
|
}
|
||||||
|
flat_symbols
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let picker = sym_picker(symbols, current_url, offset_encoding);
|
||||||
|
compositor.push(Box::new(overlayed(picker)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn workspace_symbol_picker(cx: &mut Context) {
|
||||||
|
let doc = doc!(cx.editor);
|
||||||
|
let current_url = doc.url();
|
||||||
|
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 = sym_picker(symbols, current_url, offset_encoding);
|
||||||
|
compositor.push(Box::new(overlayed(picker)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ui::menu::Item for lsp::CodeActionOrCommand {
|
||||||
|
fn label(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str(),
|
||||||
|
lsp::CodeActionOrCommand::Command(command) => command.title.as_str(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>| {
|
||||||
|
let actions = match response {
|
||||||
|
Some(a) => a,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
if actions.is_empty() {
|
||||||
|
editor.set_status("No code actions available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut picker = ui::Menu::new(actions, move |editor, code_action, event| {
|
||||||
|
if event != PromptEvent::Validate {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// always present here
|
||||||
|
let code_action = code_action.unwrap();
|
||||||
|
|
||||||
|
match code_action {
|
||||||
|
lsp::CodeActionOrCommand::Command(command) => {
|
||||||
|
log::debug!("code action command: {:?}", command);
|
||||||
|
execute_lsp_command(editor, command.clone());
|
||||||
|
}
|
||||||
|
lsp::CodeActionOrCommand::CodeAction(code_action) => {
|
||||||
|
log::debug!("code action: {:?}", code_action);
|
||||||
|
if let Some(ref workspace_edit) = code_action.edit {
|
||||||
|
log::debug!("edit: {:?}", workspace_edit);
|
||||||
|
apply_workspace_edit(editor, offset_encoding, workspace_edit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if code action provides both edit and command first the edit
|
||||||
|
// should be applied and then the command
|
||||||
|
if let Some(command) = &code_action.command {
|
||||||
|
execute_lsp_command(editor, command.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
picker.move_down(); // pre-select the first item
|
||||||
|
|
||||||
|
let popup = Popup::new("code-action", picker).margin(helix_view::graphics::Margin {
|
||||||
|
vertical: 1,
|
||||||
|
horizontal: 1,
|
||||||
|
});
|
||||||
|
compositor.replace_or_push("code-action", Box::new(popup));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
|
||||||
|
let doc = doc!(editor);
|
||||||
|
let language_server = match doc.language_server() {
|
||||||
|
Some(language_server) => language_server,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
// the command is executed on the server and communicated back
|
||||||
|
// to the client asynchronously using workspace edits
|
||||||
|
let command_future = language_server.command(cmd);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let res = command_future.await;
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
log::error!("execute LSP command: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
|
||||||
|
use lsp::ResourceOp;
|
||||||
|
use std::fs;
|
||||||
|
match op {
|
||||||
|
ResourceOp::Create(op) => {
|
||||||
|
let path = op.uri.to_file_path().unwrap();
|
||||||
|
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
|
||||||
|
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
|
||||||
|
});
|
||||||
|
if ignore_if_exists && path.exists() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
fs::write(&path, [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ResourceOp::Delete(op) => {
|
||||||
|
let path = op.uri.to_file_path().unwrap();
|
||||||
|
if path.is_dir() {
|
||||||
|
let recursive = op
|
||||||
|
.options
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|options| options.recursive)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if recursive {
|
||||||
|
fs::remove_dir_all(&path)
|
||||||
|
} else {
|
||||||
|
fs::remove_dir(&path)
|
||||||
|
}
|
||||||
|
} else if path.is_file() {
|
||||||
|
fs::remove_file(&path)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ResourceOp::Rename(op) => {
|
||||||
|
let from = op.old_uri.to_file_path().unwrap();
|
||||||
|
let to = op.new_uri.to_file_path().unwrap();
|
||||||
|
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
|
||||||
|
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
|
||||||
|
});
|
||||||
|
if ignore_if_exists && to.exists() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
fs::rename(&from, &to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_workspace_edit(
|
||||||
|
editor: &mut Editor,
|
||||||
|
offset_encoding: OffsetEncoding,
|
||||||
|
workspace_edit: &lsp::WorkspaceEdit,
|
||||||
|
) {
|
||||||
|
let mut apply_edits = |uri: &helix_lsp::Url, text_edits: Vec<lsp::TextEdit>| {
|
||||||
|
let path = uri
|
||||||
|
.to_file_path()
|
||||||
|
.expect("unable to convert URI to filepath");
|
||||||
|
|
||||||
|
let current_view_id = view!(editor).id;
|
||||||
|
let doc_id = editor.open(path, Action::Load).unwrap();
|
||||||
|
let doc = editor
|
||||||
|
.document_mut(doc_id)
|
||||||
|
.expect("Document for document_changes not found");
|
||||||
|
|
||||||
|
// Need to determine a view for apply/append_changes_to_history
|
||||||
|
let selections = doc.selections();
|
||||||
|
let view_id = if selections.contains_key(¤t_view_id) {
|
||||||
|
// use current if possible
|
||||||
|
current_view_id
|
||||||
|
} else {
|
||||||
|
// Hack: we take the first available view_id
|
||||||
|
selections
|
||||||
|
.keys()
|
||||||
|
.next()
|
||||||
|
.copied()
|
||||||
|
.expect("No view_id available")
|
||||||
|
};
|
||||||
|
|
||||||
|
let transaction = helix_lsp::util::generate_transaction_from_edits(
|
||||||
|
doc.text(),
|
||||||
|
text_edits,
|
||||||
|
offset_encoding,
|
||||||
|
);
|
||||||
|
doc.apply(&transaction, view_id);
|
||||||
|
doc.append_changes_to_history(view_id);
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ref changes) = workspace_edit.changes {
|
||||||
|
log::debug!("workspace changes: {:?}", changes);
|
||||||
|
for (uri, text_edits) in changes {
|
||||||
|
let text_edits = text_edits.to_vec();
|
||||||
|
apply_edits(uri, text_edits);
|
||||||
|
}
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
apply_edits(&document_edit.text_document.uri, edits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lsp::DocumentChanges::Operations(operations) => {
|
||||||
|
log::debug!("document changes - operations: {:?}", operations);
|
||||||
|
for operateion in operations {
|
||||||
|
match operateion {
|
||||||
|
lsp::DocumentChangeOperation::Op(op) => {
|
||||||
|
apply_document_resource_op(op).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
lsp::DocumentChangeOperation::Edit(document_edit) => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
apply_edits(&document_edit.text_document.uri, edits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn goto_impl(
|
||||||
|
editor: &mut Editor,
|
||||||
|
compositor: &mut Compositor,
|
||||||
|
locations: Vec<lsp::Location>,
|
||||||
|
offset_encoding: OffsetEncoding,
|
||||||
|
) {
|
||||||
|
push_jump(editor);
|
||||||
|
|
||||||
|
// TODO: share with symbol picker(symbol.location)
|
||||||
|
fn jump_to(
|
||||||
|
editor: &mut Editor,
|
||||||
|
location: &lsp::Location,
|
||||||
|
offset_encoding: OffsetEncoding,
|
||||||
|
action: Action,
|
||||||
|
) {
|
||||||
|
let path = location
|
||||||
|
.uri
|
||||||
|
.to_file_path()
|
||||||
|
.expect("unable to convert URI to filepath");
|
||||||
|
let _id = editor.open(path, action).expect("editor.open failed");
|
||||||
|
let (view, doc) = current!(editor);
|
||||||
|
let definition_pos = location.range.start;
|
||||||
|
// TODO: convert inside server
|
||||||
|
let new_pos =
|
||||||
|
if let Some(new_pos) = lsp_pos_to_pos(doc.text(), definition_pos, offset_encoding) {
|
||||||
|
new_pos
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
doc.set_selection(view.id, Selection::point(new_pos));
|
||||||
|
align_view(doc, view, Align::Center);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cwdir = std::env::current_dir().expect("couldn't determine current directory");
|
||||||
|
|
||||||
|
match locations.as_slice() {
|
||||||
|
[location] => {
|
||||||
|
jump_to(editor, location, offset_encoding, Action::Replace);
|
||||||
|
}
|
||||||
|
[] => {
|
||||||
|
editor.set_error("No definition found.");
|
||||||
|
}
|
||||||
|
_locations => {
|
||||||
|
let picker = FilePicker::new(
|
||||||
|
locations,
|
||||||
|
move |location| {
|
||||||
|
let file: Cow<'_, str> = (location.uri.scheme() == "file")
|
||||||
|
.then(|| {
|
||||||
|
location
|
||||||
|
.uri
|
||||||
|
.to_file_path()
|
||||||
|
.map(|path| {
|
||||||
|
// strip root prefix
|
||||||
|
path.strip_prefix(&cwdir)
|
||||||
|
.map(|path| path.to_path_buf())
|
||||||
|
.unwrap_or(path)
|
||||||
|
})
|
||||||
|
.map(|path| Cow::from(path.to_string_lossy().into_owned()))
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_else(|| location.uri.as_str().into());
|
||||||
|
let line = location.range.start.line;
|
||||||
|
format!("{}:{}", file, line).into()
|
||||||
|
},
|
||||||
|
move |cx, location, action| jump_to(cx.editor, location, offset_encoding, action),
|
||||||
|
|_editor, location| {
|
||||||
|
// TODO: share code for symbol.location and location
|
||||||
|
let path = location.uri.to_file_path().unwrap();
|
||||||
|
let line = Some((
|
||||||
|
location.range.start.line as usize,
|
||||||
|
location.range.end.line as usize,
|
||||||
|
));
|
||||||
|
Some((path, line))
|
||||||
|
},
|
||||||
|
);
|
||||||
|
compositor.push(Box::new(overlayed(picker)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn goto_definition(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 pos = pos_to_lsp_pos(
|
||||||
|
doc.text(),
|
||||||
|
doc.selection(view.id)
|
||||||
|
.primary()
|
||||||
|
.cursor(doc.text().slice(..)),
|
||||||
|
offset_encoding,
|
||||||
|
);
|
||||||
|
|
||||||
|
let future = language_server.goto_definition(doc.identifier(), pos, None);
|
||||||
|
|
||||||
|
cx.callback(
|
||||||
|
future,
|
||||||
|
move |editor: &mut Editor,
|
||||||
|
compositor: &mut Compositor,
|
||||||
|
response: Option<lsp::GotoDefinitionResponse>| {
|
||||||
|
let items = match response {
|
||||||
|
Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location],
|
||||||
|
Some(lsp::GotoDefinitionResponse::Array(locations)) => locations,
|
||||||
|
Some(lsp::GotoDefinitionResponse::Link(locations)) => locations
|
||||||
|
.into_iter()
|
||||||
|
.map(|location_link| lsp::Location {
|
||||||
|
uri: location_link.target_uri,
|
||||||
|
range: location_link.target_range,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
None => Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
goto_impl(editor, compositor, items, offset_encoding);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn goto_type_definition(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 pos = pos_to_lsp_pos(
|
||||||
|
doc.text(),
|
||||||
|
doc.selection(view.id)
|
||||||
|
.primary()
|
||||||
|
.cursor(doc.text().slice(..)),
|
||||||
|
offset_encoding,
|
||||||
|
);
|
||||||
|
|
||||||
|
let future = language_server.goto_type_definition(doc.identifier(), pos, None);
|
||||||
|
|
||||||
|
cx.callback(
|
||||||
|
future,
|
||||||
|
move |editor: &mut Editor,
|
||||||
|
compositor: &mut Compositor,
|
||||||
|
response: Option<lsp::GotoDefinitionResponse>| {
|
||||||
|
let items = match response {
|
||||||
|
Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location],
|
||||||
|
Some(lsp::GotoDefinitionResponse::Array(locations)) => locations,
|
||||||
|
Some(lsp::GotoDefinitionResponse::Link(locations)) => locations
|
||||||
|
.into_iter()
|
||||||
|
.map(|location_link| lsp::Location {
|
||||||
|
uri: location_link.target_uri,
|
||||||
|
range: location_link.target_range,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
None => Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
goto_impl(editor, compositor, items, offset_encoding);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn goto_implementation(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 pos = pos_to_lsp_pos(
|
||||||
|
doc.text(),
|
||||||
|
doc.selection(view.id)
|
||||||
|
.primary()
|
||||||
|
.cursor(doc.text().slice(..)),
|
||||||
|
offset_encoding,
|
||||||
|
);
|
||||||
|
|
||||||
|
let future = language_server.goto_implementation(doc.identifier(), pos, None);
|
||||||
|
|
||||||
|
cx.callback(
|
||||||
|
future,
|
||||||
|
move |editor: &mut Editor,
|
||||||
|
compositor: &mut Compositor,
|
||||||
|
response: Option<lsp::GotoDefinitionResponse>| {
|
||||||
|
let items = match response {
|
||||||
|
Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location],
|
||||||
|
Some(lsp::GotoDefinitionResponse::Array(locations)) => locations,
|
||||||
|
Some(lsp::GotoDefinitionResponse::Link(locations)) => locations
|
||||||
|
.into_iter()
|
||||||
|
.map(|location_link| lsp::Location {
|
||||||
|
uri: location_link.target_uri,
|
||||||
|
range: location_link.target_range,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
None => Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
goto_impl(editor, compositor, items, offset_encoding);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn goto_reference(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 pos = pos_to_lsp_pos(
|
||||||
|
doc.text(),
|
||||||
|
doc.selection(view.id)
|
||||||
|
.primary()
|
||||||
|
.cursor(doc.text().slice(..)),
|
||||||
|
offset_encoding,
|
||||||
|
);
|
||||||
|
|
||||||
|
let future = language_server.goto_reference(doc.identifier(), pos, None);
|
||||||
|
|
||||||
|
cx.callback(
|
||||||
|
future,
|
||||||
|
move |editor: &mut Editor,
|
||||||
|
compositor: &mut Compositor,
|
||||||
|
items: Option<Vec<lsp::Location>>| {
|
||||||
|
goto_impl(
|
||||||
|
editor,
|
||||||
|
compositor,
|
||||||
|
items.unwrap_or_default(),
|
||||||
|
offset_encoding,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn signature_help(cx: &mut Context) {
|
||||||
|
let (view, doc) = current!(cx.editor);
|
||||||
|
|
||||||
|
let language_server = match doc.language_server() {
|
||||||
|
Some(language_server) => language_server,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let pos = pos_to_lsp_pos(
|
||||||
|
doc.text(),
|
||||||
|
doc.selection(view.id)
|
||||||
|
.primary()
|
||||||
|
.cursor(doc.text().slice(..)),
|
||||||
|
language_server.offset_encoding(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let future = language_server.text_document_signature_help(doc.identifier(), pos, None);
|
||||||
|
|
||||||
|
cx.callback(
|
||||||
|
future,
|
||||||
|
move |_editor: &mut Editor,
|
||||||
|
_compositor: &mut Compositor,
|
||||||
|
response: Option<lsp::SignatureHelp>| {
|
||||||
|
if let Some(signature_help) = response {
|
||||||
|
log::info!("{:?}", signature_help);
|
||||||
|
// signatures
|
||||||
|
// active_signature
|
||||||
|
// active_parameter
|
||||||
|
// render as:
|
||||||
|
|
||||||
|
// signature
|
||||||
|
// ----------
|
||||||
|
// doc
|
||||||
|
|
||||||
|
// with active param highlighted
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
pub fn hover(cx: &mut Context) {
|
||||||
|
let (view, doc) = current!(cx.editor);
|
||||||
|
|
||||||
|
let language_server = match doc.language_server() {
|
||||||
|
Some(language_server) => language_server,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier
|
||||||
|
|
||||||
|
let pos = pos_to_lsp_pos(
|
||||||
|
doc.text(),
|
||||||
|
doc.selection(view.id)
|
||||||
|
.primary()
|
||||||
|
.cursor(doc.text().slice(..)),
|
||||||
|
language_server.offset_encoding(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let future = language_server.text_document_hover(doc.identifier(), pos, None);
|
||||||
|
|
||||||
|
cx.callback(
|
||||||
|
future,
|
||||||
|
move |editor: &mut Editor, compositor: &mut Compositor, response: Option<lsp::Hover>| {
|
||||||
|
if let Some(hover) = response {
|
||||||
|
// hover.contents / .range <- used for visualizing
|
||||||
|
|
||||||
|
fn marked_string_to_markdown(contents: lsp::MarkedString) -> String {
|
||||||
|
match contents {
|
||||||
|
lsp::MarkedString::String(contents) => contents,
|
||||||
|
lsp::MarkedString::LanguageString(string) => {
|
||||||
|
if string.language == "markdown" {
|
||||||
|
string.value
|
||||||
|
} else {
|
||||||
|
format!("```{}\n{}\n```", string.language, string.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents = match hover.contents {
|
||||||
|
lsp::HoverContents::Scalar(contents) => marked_string_to_markdown(contents),
|
||||||
|
lsp::HoverContents::Array(contents) => contents
|
||||||
|
.into_iter()
|
||||||
|
.map(marked_string_to_markdown)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n\n"),
|
||||||
|
lsp::HoverContents::Markup(contents) => contents.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
// skip if contents empty
|
||||||
|
|
||||||
|
let contents =
|
||||||
|
ui::Markdown::new(contents, editor.syn_loader.clone()).style_group("hover");
|
||||||
|
let popup = Popup::new("hover", contents);
|
||||||
|
compositor.replace_or_push("hover", Box::new(popup));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
pub fn rename_symbol(cx: &mut Context) {
|
||||||
|
let prompt = Prompt::new(
|
||||||
|
"rename-to:".into(),
|
||||||
|
None,
|
||||||
|
ui::completers::none,
|
||||||
|
move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
|
||||||
|
if event != PromptEvent::Validate {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("renaming to: {:?}", input);
|
||||||
|
|
||||||
|
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 pos = pos_to_lsp_pos(
|
||||||
|
doc.text(),
|
||||||
|
doc.selection(view.id)
|
||||||
|
.primary()
|
||||||
|
.cursor(doc.text().slice(..)),
|
||||||
|
offset_encoding,
|
||||||
|
);
|
||||||
|
|
||||||
|
let task = language_server.rename_symbol(doc.identifier(), pos, input.to_string());
|
||||||
|
let edits = block_on(task).unwrap_or_default();
|
||||||
|
log::debug!("Edits from LSP: {:?}", edits);
|
||||||
|
apply_workspace_edit(cx.editor, offset_encoding, &edits);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
cx.push_layer(Box::new(prompt));
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user