diff options
-rw-r--r-- | helix-term/src/commands.rs | 778 | ||||
-rw-r--r-- | helix-term/src/commands/lsp.rs | 776 |
2 files changed, 785 insertions, 769 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 73addcee..7e8b1d53 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1,6 +1,8 @@ pub(crate) mod dap; +pub(crate) mod lsp; pub use dap::*; +pub use lsp::*; use helix_core::{ comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, @@ -33,11 +35,6 @@ use helix_view::{ use anyhow::{anyhow, bail, ensure, Context as _}; 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 movement::Movement; @@ -2754,7 +2751,7 @@ pub mod cmd { // TODO: support no 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); } Ok(()) @@ -3474,217 +3471,6 @@ fn buffer_picker(cx: &mut Context) { 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) { cx.callback = Some(Box::new( 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) { // TODO: last picker does not seem to work well with buffer_picker 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) { push_jump(editor); @@ -4565,46 +3936,6 @@ fn goto_prev_diag(cx: &mut Context) { 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 { use super::*; pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>; @@ -4630,6 +3961,7 @@ pub mod insert { } fn language_server_completion(cx: &mut Context, ch: char) { + use helix_lsp::lsp; // if ch matches completion char, trigger completion let doc = doc_mut!(cx.editor); let language_server = match doc.language_server() { @@ -4653,6 +3985,7 @@ pub mod insert { } fn signature_help(cx: &mut Context, ch: char) { + use helix_lsp::lsp; // if ch matches signature_help char, trigger let doc = doc_mut!(cx.editor); let language_server = match doc.language_server() { @@ -5360,6 +4693,8 @@ fn unindent(cx: &mut Context) { } fn format_selections(cx: &mut Context) { + use helix_lsp::{lsp, util::range_to_lsp_range}; + let (view, doc) = current!(cx.editor); // via lsp if available @@ -5504,6 +4839,8 @@ fn remove_primary_selection(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 // (or on word char typing??) // 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 fn toggle_comments(cx: &mut Context) { let (view, doc) = current!(cx.editor); @@ -6406,43 +5683,6 @@ fn add_newline_impl(cx: &mut Context, open: Open) { 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. fn increment(cx: &mut Context) { increment_impl(cx, cx.count() as i64); diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs new file mode 100644 index 00000000..084c7c6a --- /dev/null +++ b/helix-term/src/commands/lsp.rs @@ -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)); +} |