From 877c332439da960da58f71b0fb68e6a3c02d0a84 Mon Sep 17 00:00:00 2001 From: Schuyler Mortimer Date: Tue, 2 Jul 2024 15:19:09 -0700 Subject: [PATCH] apply https://github.com/helix-editor/helix/pull/5492 to latest --- helix-lsp/src/client.rs | 5 ++ helix-lsp/src/lib.rs | 4 ++ helix-term/src/application.rs | 94 +++++++++++++++++++++++++------- helix-term/src/commands.rs | 19 +++++-- helix-term/src/commands/dap.rs | 11 +++- helix-term/src/commands/lsp.rs | 29 +++++++--- helix-term/src/commands/typed.rs | 3 +- helix-term/src/ui/mod.rs | 25 +++++---- helix-term/src/ui/picker.rs | 43 +++++++++------ 9 files changed, 169 insertions(+), 64 deletions(-) diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 643aa9a26..f86b32480 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -665,6 +665,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 ec89e1f82..af1fa4bf9 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -567,6 +567,7 @@ pub enum MethodCall { RegisterCapability(lsp::RegistrationParams), UnregisterCapability(lsp::UnregistrationParams), ShowDocument(lsp::ShowDocumentParams), + ShowMessageRequest(lsp::ShowMessageRequestParams), } impl MethodCall { @@ -598,6 +599,9 @@ impl MethodCall { let params: lsp::ShowDocumentParams = params.parse()?; Self::ShowDocument(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 9adc764cc..e39174844 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,8 +1,8 @@ use arc_swap::{access::Map, ArcSwap}; use futures_util::Stream; -use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Selection}; +use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Rope, Selection}; use helix_lsp::{ - lsp::{self, notification::Notification}, + lsp::{self, notification::Notification, MessageType}, util::lsp_range_to_range, LanguageServerId, LspProgressMap, }; @@ -26,7 +26,7 @@ use crate::{ handlers, job::Jobs, keymap::Keymaps, - ui::{self, overlay::overlaid}, + ui::{self, overlay::overlaid, FileLocation, Picker, Popup}, }; use log::{debug, error, info, warn}; @@ -960,11 +960,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!( @@ -973,11 +973,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); @@ -991,7 +991,7 @@ impl Application { spinner.start(); } - Ok(serde_json::Value::Null) + Some(Ok(serde_json::Value::Null)) } Ok(MethodCall::ApplyWorkspaceEdit(params)) => { let language_server = language_server!(); @@ -1001,25 +1001,73 @@ impl Application { .editor .apply_workspace_edit(offset_encoding, ¶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), - })) + }))) } else { - Err(helix_lsp::jsonrpc::Error { + Some(Err(helix_lsp::jsonrpc::Error { code: helix_lsp::jsonrpc::ErrorCode::InvalidRequest, message: "Server must be initialized to request workspace edits" .to_string(), data: None, - }) + })) } } Ok(MethodCall::WorkspaceFolders) => { - Ok(json!(&*language_server!().workspace_folders().await)) + Some(Ok(json!(&*language_server!().workspace_folders().await))) + } + 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 = + Picker::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)), + ); + }) + .with_preview( + 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 language_server = language_server!(); @@ -1039,7 +1087,7 @@ impl Application { Some(config) }) .collect(); - Ok(json!(result)) + Some(Ok(json!(result))) } Ok(MethodCall::RegisterCapability(params)) => { if let Some(client) = self.editor.language_servers.get_by_id(server_id) { @@ -1077,7 +1125,7 @@ impl Application { } } - Ok(serde_json::Value::Null) + Some(Ok(serde_json::Value::Null)) } Ok(MethodCall::UnregisterCapability(params)) => { for unreg in params.unregisterations { @@ -1093,18 +1141,26 @@ impl Application { } } } - Ok(serde_json::Value::Null) + Some(Ok(serde_json::Value::Null)) } Ok(MethodCall::ShowDocument(params)) => { let language_server = language_server!(); let offset_encoding = language_server.offset_encoding(); let result = self.handle_show_document(params, offset_encoding); - Ok(json!(result)) + Some(Ok(json!(result))) } }; - - tokio::spawn(language_server!().reply(id, reply)); + if let Some(reply) = reply { + let lanaguage_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(lanaguage_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 67955aec0..165b42677 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2425,7 +2425,10 @@ fn global_search(cx: &mut Context) { let picker = Picker::with_stream( picker, injector, - move |cx, FileResult { path, line_num }, action| { + move |cx, file_result, action| { + let Some(FileResult { path, line_num }) = file_result else { + return; + }; let doc = match cx.editor.open(path, action) { Ok(id) => doc_mut!(cx.editor, &id), Err(e) => { @@ -2941,7 +2944,8 @@ fn buffer_picker(cx: &mut Context) { // mru items.sort_unstable_by_key(|item| std::cmp::Reverse(item.focused_at)); - let picker = Picker::new(items, (), |cx, meta, action| { + let picker = Picker::new(items, (), |cx, buffer_meta, action| { + let Some(meta) = buffer_meta else { return }; cx.editor.switch(meta.id, action); }) .with_preview(|editor, meta| { @@ -3030,7 +3034,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) = (view_mut!(cx.editor), doc_mut!(cx.editor, &meta.id)); @@ -3108,7 +3113,8 @@ fn changed_file_picker(cx: &mut Context) { style_deleted: deleted, style_renamed: renamed, }, - |cx, meta: &FileChange, action| { + |cx, file_meta: Option<&FileChange>, action| { + let Some(meta) = file_meta else { return }; let path_to_open = meta.path(); if let Err(e) = cx.editor.open(path_to_open, action) { let err = if let Some(err) = e.source() { @@ -3184,7 +3190,10 @@ 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, count, diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 0e50377ac..14e3a193a 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -74,7 +74,8 @@ fn thread_picker( let thread_states = debugger.thread_states.clone(); let picker = Picker::new(threads, thread_states, move |cx, thread, _action| { - callback_fn(cx.editor, thread) + let Some(t) = thread else { return }; + callback_fn(cx.editor, t) }) .with_preview(move |editor, thread| { let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?; @@ -271,7 +272,10 @@ pub fn dap_launch(cx: &mut Context) { cx.push_layer(Box::new(overlaid(Picker::new( templates, (), - |cx, template, _action| { + |cx, debug_template, _action| { + let Some(template) = debug_template else { + return; + }; if template.completion.is_empty() { if let Err(err) = dap_start_impl(cx, Some(&template.name), None, None) { cx.editor.set_error(err.to_string()); @@ -736,7 +740,8 @@ pub fn dap_switch_stack_frame(cx: &mut Context) { let frames = debugger.stack_frames[&thread_id].clone(); - let picker = Picker::new(frames, (), move |cx, frame, _action| { + let picker = Picker::new(frames, (), 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 d585e1bed..c96df2b60 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -245,7 +245,8 @@ type SymbolPicker = Picker; fn sym_picker(symbols: Vec, current_path: Option) -> SymbolPicker { // TODO: drop current_path comparison and instead use workspace: bool flag? - Picker::new(symbols, current_path, move |cx, item, action| { + Picker::new(symbols, current_path, move |cx, symbol_item, action| { + let Some(item) = symbol_item else { return }; jump_to_location( cx.editor, &item.symbol.location, @@ -296,13 +297,15 @@ fn diag_picker( Picker::new( flat_diag, (styles, format), - move |cx, - PickerDiagnostic { - path, - diag, - offset_encoding, - }, - action| { + move |cx, picker_diagnostic, action| { + let Some(PickerDiagnostic { + path, + diag, + offset_encoding, + }) = picker_diagnostic + else { + return; + }; jump_to_position(cx.editor, path, diag.range, *offset_encoding, action) }, ) @@ -509,6 +512,13 @@ impl ui::menu::Item for CodeActionOrCommandItem { } } +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. @@ -818,7 +828,8 @@ fn goto_impl( [] => unreachable!("`locations` should be non-empty for `goto_impl`"), _locations => { let picker = Picker::new(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) }) .with_preview(move |_editor, location| Some(location_to_file_location(location))); compositor.push(Box::new(overlaid(picker))); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 032f016f5..763da1c43 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1413,7 +1413,8 @@ fn lsp_workspace_command( commands, (), move |cx, LsIdCommand(ls_id, command), _action| { - execute_lsp_command(cx.editor, *ls_id, command.clone()); + let Some(c) = command else { return }; + execute_lsp_command(cx.editor, *ls_id, c.clone()); }, ); compositor.push(Box::new(overlaid(picker))) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 0a65b12b5..35cf1e98e 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -217,16 +217,21 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker }); log::debug!("file_picker init {:?}", Instant::now().duration_since(now)); - let picker = Picker::new(Vec::new(), root, move |cx, path: &PathBuf, action| { - if let Err(e) = cx.editor.open(path, action) { - let err = if let Some(err) = e.source() { - format!("{}", err) - } else { - format!("unable to open \"{}\"", path.display()) - }; - cx.editor.set_error(err); - } - }) + let picker = Picker::new( + Vec::new(), + root, + move |cx, path_buff: Option<&PathBuf>, 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) + } else { + format!("unable to open \"{}\"", path.display()) + }; + cx.editor.set_error(err); + } + }, + ) .with_preview(|_editor, path| Some((path.clone().into(), None))); let injector = picker.injector(); let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30); diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index b8ec57d51..9a7c9062b 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -35,7 +35,7 @@ use std::{ use crate::ui::{Prompt, PromptEvent}; use helix_core::{ char_idx_at_visual_offset, fuzzy::MATCHER, movement::Direction, - text_annotations::TextAnnotations, unicode::segmentation::UnicodeSegmentation, Position, + text_annotations::TextAnnotations, unicode::segmentation::UnicodeSegmentation, Position, Rope, Syntax, }; use helix_view::{ @@ -57,6 +57,7 @@ pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024; pub enum PathOrId { Id(DocumentId), Path(PathBuf), + Text(Rope), } impl PathOrId { @@ -65,10 +66,17 @@ impl PathOrId { match self { Path(path) => Path(helix_stdx::path::canonicalize(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) @@ -98,6 +106,7 @@ pub enum CachedPreview { pub enum Preview<'picker, 'editor> { Cached(&'picker CachedPreview), EditorDocument(&'editor Document), + Text(Box), } impl Preview<'_, '_> { @@ -105,6 +114,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, } } @@ -112,6 +122,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(_) => "", @@ -222,7 +233,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 matcher = Nucleo::new( Config::DEFAULT, @@ -247,7 +258,7 @@ impl Picker { pub fn with_stream( matcher: Nucleo, injector: Injector, - callback_fn: impl Fn(&mut Context, &T, Action) + 'static, + callback_fn: impl Fn(&mut Context, Option<&T>, Action) + 'static, ) -> Self { Self::with(matcher, injector.editor_data, injector.shutown, callback_fn) } @@ -256,7 +267,7 @@ impl Picker { matcher: Nucleo, editor_data: Arc, shutdown: Arc, - 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(), @@ -440,6 +451,10 @@ impl Picker { 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)) + } } } @@ -455,6 +470,7 @@ impl Picker { Some(CachedPreview::Document(ref mut doc)) => doc, _ => return EventResult::Consumed(None), }, + PathOrId::Text(_) => return EventResult::Consumed(None), }; let mut callback: Option = None; @@ -502,6 +518,7 @@ impl Picker { } _ => return, }, + PathOrId::Text(_rope) => return, }; doc.syntax = Some(syntax); }; @@ -885,26 +902,18 @@ impl Component for Picker { } key!(Esc) | ctrl!('c') => return close_fn(self), alt!(Enter) => { - if let Some(option) = self.selection() { - (self.callback_fn)(ctx, option, Action::Load); - } + (self.callback_fn)(ctx, self.selection(), Action::Load); } key!(Enter) => { - if let Some(option) = self.selection() { - (self.callback_fn)(ctx, option, Action::Replace); - } + (self.callback_fn)(ctx, self.selection(), Action::Replace); return close_fn(self); } ctrl!('s') => { - if let Some(option) = self.selection() { - (self.callback_fn)(ctx, option, Action::HorizontalSplit); - } + (self.callback_fn)(ctx, self.selection(), Action::HorizontalSplit); return close_fn(self); } ctrl!('v') => { - if let Some(option) = self.selection() { - (self.callback_fn)(ctx, option, Action::VerticalSplit); - } + (self.callback_fn)(ctx, self.selection(), Action::VerticalSplit); return close_fn(self); } ctrl!('t') => { @@ -945,7 +954,7 @@ impl Drop for Picker { } } -type PickerCallback = Box; +type PickerCallback = Box, Action)>; /// Returns a new list of options to replace the contents of the picker /// when called with the current picker query,