diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index f93e582639a1..4bf56ce5882d 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -408,6 +408,11 @@ impl Client { ..Default::default() }), window: Some(lsp::WindowClientCapabilities { + show_message: Some(lsp::ShowMessageRequestClientCapabilities { + message_action_item: Some(lsp::MessageActionItemCapabilities { + additional_properties_support: Some(true), + }), + }), work_done_progress: Some(true), ..Default::default() }), diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 5609a624fecc..0e0c21013f28 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -527,6 +527,7 @@ pub enum MethodCall { WorkspaceFolders, WorkspaceConfiguration(lsp::ConfigurationParams), RegisterCapability(lsp::RegistrationParams), + ShowMessageRequest(lsp::ShowMessageRequestParams), } impl MethodCall { @@ -550,6 +551,9 @@ impl MethodCall { let params: lsp::RegistrationParams = params.parse()?; Self::RegisterCapability(params) } + lsp::request::ShowMessageRequest::METHOD => { + Self::ShowMessageRequest(params.parse::()?) + } _ => { return Err(Error::Unhandled); } diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 95faa01b0a3e..986319e5f171 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -3,9 +3,13 @@ use futures_util::Stream; use helix_core::{ diagnostic::{DiagnosticTag, NumberOrString}, path::get_relative_path, - pos_at_coords, syntax, Selection, + pos_at_coords, syntax, Rope, Selection, +}; +use helix_lsp::{ + lsp::{self, MessageType}, + util::lsp_pos_to_pos, + LspProgressMap, }; -use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; use helix_view::{ align_view, document::DocumentSavedEventResult, @@ -25,7 +29,7 @@ use crate::{ config::Config, job::Jobs, keymap::Keymaps, - ui::{self, overlay::overlayed}, + ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup}, }; use log::{debug, error, warn}; @@ -965,11 +969,11 @@ impl Application { "Language Server: Method {} not found in request {}", method, id ); - Err(helix_lsp::jsonrpc::Error { + Some(Err(helix_lsp::jsonrpc::Error { code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound, message: format!("Method not found: {}", method), data: None, - }) + })) } Err(err) => { log::error!( @@ -978,11 +982,11 @@ impl Application { id, err ); - Err(helix_lsp::jsonrpc::Error { + Some(Err(helix_lsp::jsonrpc::Error { code: helix_lsp::jsonrpc::ErrorCode::ParseError, message: format!("Malformed method call: {}", method), data: None, - }) + })) } Ok(MethodCall::WorkDoneProgressCreate(params)) => { self.lsp_progress.create(server_id, params.token); @@ -996,7 +1000,7 @@ impl Application { spinner.start(); } - Ok(serde_json::Value::Null) + Some(Ok(serde_json::Value::Null)) } Ok(MethodCall::ApplyWorkspaceEdit(params)) => { let res = apply_workspace_edit( @@ -1005,20 +1009,69 @@ impl Application { ¶ms.edit, ); - Ok(json!(lsp::ApplyWorkspaceEditResponse { + Some(Ok(json!(lsp::ApplyWorkspaceEditResponse { applied: res.is_ok(), failure_reason: res.as_ref().err().map(|err| err.kind.to_string()), failed_change: res .as_ref() .err() .map(|err| err.failed_change_idx as u32), - })) + }))) } Ok(MethodCall::WorkspaceFolders) => { let language_server = self.editor.language_servers.get_by_id(server_id).unwrap(); - Ok(json!(language_server.workspace_folders())) + Some(Ok(json!(language_server.workspace_folders()))) + } + Ok(MethodCall::ShowMessageRequest(params)) => { + if let Some(actions) = params.actions { + let call_id = id.clone(); + let message_type = match params.typ { + MessageType::ERROR => "ERROR", + MessageType::WARNING => "WARNING", + MessageType::INFO => "INFO", + _ => "LOG", + }; + let message = format!("{}: {}", message_type, ¶ms.message); + let rope = Rope::from(message); + let picker = FilePicker::new( + actions, + (), + move |ctx, message_action, _event| { + let server_from_id = + ctx.editor.language_servers.get_by_id(server_id); + let language_server = match server_from_id { + Some(language_server) => language_server, + None => { + warn!( + "can't find language server with id `{server_id}`" + ); + return; + } + }; + let response = match message_action { + Some(item) => json!(item), + None => serde_json::Value::Null, + }; + + tokio::spawn( + language_server.reply(call_id.clone(), Ok(response)), + ); + }, + move |_editor, _value| { + let file_location: FileLocation = (rope.clone().into(), None); + Some(file_location) + }, + ); + let popup_id = "show-message-request"; + let popup = Popup::new(popup_id, picker); + self.compositor.replace_or_push(popup_id, popup); + // do not send a reply just yet + None + } else { + Some(Ok(serde_json::Value::Null)) + } } Ok(MethodCall::WorkspaceConfiguration(params)) => { let result: Vec<_> = params @@ -1046,7 +1099,7 @@ impl Application { Some(config) }) .collect(); - Ok(json!(result)) + Some(Ok(json!(result))) } Ok(MethodCall::RegisterCapability(_params)) => { log::warn!("Ignoring a client/registerCapability request because dynamic capability registration is not enabled. Please report this upstream to the language server"); @@ -1057,19 +1110,21 @@ impl Application { // exit. So we work around this by ignoring the request and sending back an OK // response. - Ok(serde_json::Value::Null) + Some(Ok(serde_json::Value::Null)) } }; - let language_server = match self.editor.language_servers.get_by_id(server_id) { - Some(language_server) => language_server, - None => { - warn!("can't find language server with id `{}`", server_id); - return; - } - }; + if let Some(reply) = reply { + let language_server = match self.editor.language_servers.get_by_id(server_id) { + Some(language_server) => language_server, + None => { + warn!("can't find language server with id `{}`", server_id); + return; + } + }; - tokio::spawn(language_server.reply(id, reply)); + tokio::spawn(language_server.reply(id, reply)); + } } Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id), } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 1c1edece1a31..0829128906a8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2116,7 +2116,8 @@ fn global_search(cx: &mut Context) { let picker = FilePicker::new( all_matches, current_path, - move |cx, FileResult { path, line_num }, action| { + move |cx, file_result, action| { + let Some(FileResult { path, line_num }) = file_result else { return }; match cx.editor.open(path, action) { Ok(_) => {} Err(e) => { @@ -2496,7 +2497,8 @@ fn buffer_picker(cx: &mut Context) { .map(|doc| new_meta(doc)) .collect(), (), - |cx, meta, action| { + |cx, buffer_meta, action| { + let Some(meta) = buffer_meta else { return }; cx.editor.switch(meta.id, action); }, |editor, meta| { @@ -2578,7 +2580,8 @@ fn jumplist_picker(cx: &mut Context) { }) .collect(), (), - |cx, meta, action| { + |cx, jump_meta, action| { + let Some(meta) = jump_meta else { return }; cx.editor.switch(meta.id, action); let config = cx.editor.config(); let (view, doc) = current!(cx.editor); @@ -2639,7 +2642,8 @@ pub fn command_palette(cx: &mut Context) { } })); - let picker = Picker::new(commands, keymap, move |cx, command, _action| { + let picker = Picker::new(commands, keymap, move |cx, mappable_command, _action| { + let Some(command) = mappable_command else { return }; let mut ctx = Context { register: None, count: std::num::NonZeroUsize::new(1), diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index dac1e9d5258d..7cb4ffc5e3ef 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -76,7 +76,10 @@ fn thread_picker( let picker = FilePicker::new( threads, thread_states, - move |cx, thread, _action| callback_fn(cx.editor, thread), + move |cx, thread, _action| { + let Some(t) = thread else { return }; + callback_fn(cx.editor, t) + }, move |editor, thread| { let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?; let frame = frames.get(0)?; @@ -273,7 +276,8 @@ pub fn dap_launch(cx: &mut Context) { cx.push_layer(Box::new(overlayed(Picker::new( templates, (), - |cx, template, _action| { + |cx, debug_template, _action| { + let Some(template) = debug_template else { return }; let completions = template.completion.clone(); let name = template.name.clone(); let callback = Box::pin(async move { @@ -731,7 +735,8 @@ pub fn dap_switch_stack_frame(cx: &mut Context) { let picker = FilePicker::new( frames, (), - move |cx, frame, _action| { + move |cx, stack_frame, _action| { + let Some(frame) = stack_frame else { return }; let debugger = debugger!(cx.editor); // TODO: this should be simpler to find let pos = debugger.stack_frames[&thread_id] diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 0b0d1db4d4bc..52e60f1209d3 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -218,7 +218,8 @@ fn sym_picker( FilePicker::new( symbols, current_path.clone(), - move |cx, symbol, action| { + move |cx, symbol_information, action| { + let Some(symbol) = symbol_information else { return }; let (view, doc) = current!(cx.editor); push_jump(view, doc); @@ -293,7 +294,8 @@ fn diag_picker( FilePicker::new( flat_diag, (styles, format), - move |cx, PickerDiagnostic { url, diag }, action| { + move |cx, picker_diagnostic, action| { + let Some(PickerDiagnostic { url, diag }) = picker_diagnostic else { return }; if current_path.as_ref() == Some(url) { let (view, doc) = current!(cx.editor); push_jump(view, doc); @@ -484,6 +486,13 @@ impl ui::menu::Item for lsp::CodeActionOrCommand { } } +impl ui::menu::Item for lsp::MessageActionItem { + type Data = (); + fn format(&self, _data: &Self::Data) -> Row { + self.title.as_str().into() + } +} + /// Determines the category of the `CodeAction` using the `CodeAction::kind` field. /// Returns a number that represent these categories. /// Categories with a lower number should be displayed first. @@ -951,7 +960,8 @@ fn goto_impl( locations, cwdir, move |cx, location, action| { - jump_to_location(cx.editor, location, offset_encoding, action) + let Some(l) = location else { return }; + jump_to_location(cx.editor, l, offset_encoding, action) }, move |_editor, location| Some(location_to_file_location(location)), ); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 6fbdc0d7afa3..40df52b346d5 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1333,7 +1333,8 @@ fn lsp_workspace_command( let call: job::Callback = Callback::EditorCompositor(Box::new( move |_editor: &mut Editor, compositor: &mut Compositor| { let picker = ui::Picker::new(commands, (), |cx, command, _action| { - execute_lsp_command(cx.editor, command.clone()); + let Some(c) = command else { return }; + execute_lsp_command(cx.editor, c.clone()); }); compositor.push(Box::new(overlayed(picker))) }, diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 3e9a14b06307..ecd68e9109f0 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -220,7 +220,8 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi FilePicker::new( files, root, - move |cx, path: &PathBuf, action| { + move |cx, path_buff, action| { + let Some(path) = path_buff else { return }; if let Err(e) = cx.editor.open(path, action) { let err = if let Some(err) = e.source() { format!("{}", err) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index bc2f98ee6cac..1e2ca440fc4e 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -26,7 +26,7 @@ use std::{collections::HashMap, io::Read, path::PathBuf}; use crate::ui::{Prompt, PromptEvent}; use helix_core::{ movement::Direction, text_annotations::TextAnnotations, - unicode::segmentation::UnicodeSegmentation, Position, + unicode::segmentation::UnicodeSegmentation, Position, Rope, }; use helix_view::{ editor::Action, @@ -46,6 +46,7 @@ pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024; pub enum PathOrId { Id(DocumentId), Path(PathBuf), + Text(Rope), } impl PathOrId { @@ -54,10 +55,17 @@ impl PathOrId { Ok(match self { Path(path) => Path(helix_core::path::get_canonicalized_path(&path)?), Id(id) => Id(id), + Text(rope) => Text(rope), }) } } +impl From for PathOrId { + fn from(text: Rope) -> Self { + Self::Text(text) + } +} + impl From for PathOrId { fn from(v: PathBuf) -> Self { Self::Path(v) @@ -97,6 +105,7 @@ pub enum CachedPreview { pub enum Preview<'picker, 'editor> { Cached(&'picker CachedPreview), EditorDocument(&'editor Document), + Text(Box), } impl Preview<'_, '_> { @@ -104,6 +113,7 @@ impl Preview<'_, '_> { match self { Preview::EditorDocument(doc) => Some(doc), Preview::Cached(CachedPreview::Document(doc)) => Some(doc), + Preview::Text(doc) => Some(doc.as_ref()), _ => None, } } @@ -111,6 +121,7 @@ impl Preview<'_, '_> { /// Alternate text to show for the preview. fn placeholder(&self) -> &str { match *self { + Self::Text(_) => "", Self::EditorDocument(_) => "", Self::Cached(preview) => match preview { CachedPreview::Document(_) => "", @@ -126,7 +137,7 @@ impl FilePicker { pub fn new( options: Vec, editor_data: T::Data, - callback_fn: impl Fn(&mut Context, &T, Action) + 'static, + callback_fn: impl Fn(&mut Context, Option<&T>, Action) + 'static, preview_fn: impl Fn(&Editor, &T) -> Option + 'static, ) -> Self { let truncate_start = true; @@ -204,6 +215,10 @@ impl FilePicker { let doc = editor.documents.get(&id).unwrap(); Preview::EditorDocument(doc) } + PathOrId::Text(rope) => { + let doc = Document::from(rope, None, editor.config.clone()); + Preview::Text(Box::new(doc)) + } } } @@ -217,6 +232,7 @@ impl FilePicker { Some(CachedPreview::Document(doc)) => Some(doc), _ => None, }, + PathOrId::Text(_rope) => None, }); // Then attempt to highlight it if it has no language set @@ -400,7 +416,7 @@ impl Ord for PickerMatch { } } -type PickerCallback = Box; +type PickerCallback = Box, Action)>; pub struct Picker { options: Vec, @@ -422,7 +438,6 @@ pub struct Picker { show_preview: bool, /// Constraints for tabular formatting widths: Vec, - callback_fn: PickerCallback, } @@ -430,7 +445,7 @@ impl Picker { pub fn new( options: Vec, editor_data: T::Data, - callback_fn: impl Fn(&mut Context, &T, Action) + 'static, + callback_fn: impl Fn(&mut Context, Option<&T>, Action) + 'static, ) -> Self { let prompt = Prompt::new( "".into(), @@ -687,29 +702,23 @@ impl Component for Picker { self.to_end(); } key!(Esc) | ctrl!('c') => { + // Action shouldn't matter here because no item was selected + (self.callback_fn)(cx, None, Action::Replace); return close_fn; } alt!(Enter) => { - if let Some(option) = self.selection() { - (self.callback_fn)(cx, option, Action::Load); - } + (self.callback_fn)(cx, self.selection(), Action::Load); } key!(Enter) => { - if let Some(option) = self.selection() { - (self.callback_fn)(cx, option, Action::Replace); - } + (self.callback_fn)(cx, self.selection(), Action::Replace); return close_fn; } ctrl!('s') => { - if let Some(option) = self.selection() { - (self.callback_fn)(cx, option, Action::HorizontalSplit); - } + (self.callback_fn)(cx, self.selection(), Action::HorizontalSplit); return close_fn; } ctrl!('v') => { - if let Some(option) = self.selection() { - (self.callback_fn)(cx, option, Action::VerticalSplit); - } + (self.callback_fn)(cx, self.selection(), Action::VerticalSplit); return close_fn; } ctrl!('t') => {