From 3d5201e36853b6a857bdb44479304d7aae367d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Tue, 21 Nov 2023 13:47:27 +0100 Subject: [PATCH 1/4] feat(lsp): implement show document request Implement [window.showDocument](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#window_showDocument) LSP server-sent request. This PR builds on top of helix-editor#5820, moves the external-URL opening functionality into shared crate-level function that returns a callback that is now used by both the `open_file` command as well as the window.showDocument handler if the URL is marked as external. --- helix-lsp/src/lib.rs | 5 +++ helix-term/src/application.rs | 59 ++++++++++++++++++++++++++++++++++- helix-term/src/commands.rs | 23 +++----------- helix-term/src/lib.rs | 23 ++++++++++++++ 4 files changed, 90 insertions(+), 20 deletions(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 34278cd54ffa..83625897e965 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -549,6 +549,7 @@ pub enum MethodCall { WorkspaceConfiguration(lsp::ConfigurationParams), RegisterCapability(lsp::RegistrationParams), UnregisterCapability(lsp::UnregistrationParams), + ShowDocument(lsp::ShowDocumentParams), } impl MethodCall { @@ -576,6 +577,10 @@ impl MethodCall { let params: lsp::UnregistrationParams = params.parse()?; Self::UnregisterCapability(params) } + lsp::request::ShowDocument::METHOD => { + let params: lsp::ShowDocumentParams = params.parse()?; + Self::ShowDocument(params) + } _ => { return Err(Error::Unhandled); } diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 3abe9cae54ac..21c56db44ce7 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -8,7 +8,7 @@ use helix_core::{ }; use helix_lsp::{ lsp::{self, notification::Notification}, - util::lsp_pos_to_pos, + util::{lsp_pos_to_pos, lsp_range_to_range}, LspProgressMap, }; use helix_view::{ @@ -1176,6 +1176,63 @@ impl Application { } Ok(serde_json::Value::Null) } + Ok(MethodCall::ShowDocument(params)) => { + let language_server = language_server!(); + let offset_encoding = language_server.offset_encoding(); + + if params.external.unwrap_or_default() { + self.jobs + .callback(crate::open_external_url_callback(params.uri)); + return; + } + + let path = match params.uri.to_file_path() { + Ok(path) => path, + Err(_) => { + log::error!("Unsupported file URI: {}", params.uri); + return; + } + }; + + let action = match params.take_focus { + Some(true) => helix_view::editor::Action::Replace, + _ => helix_view::editor::Action::HorizontalSplit, + }; + + let doc = match self.editor.open(&path, action) { + Ok(id) => doc_mut!(self.editor, &id), + Err(err) => { + log::error!("failed to open path: {:?}: {:?}", params.uri, err); + return; + } + }; + + if let Some(range) = params.selection { + // TODO: convert inside server + let new_range = if let Some(new_range) = + lsp_range_to_range(doc.text(), range, offset_encoding) + { + new_range + } else { + log::warn!("lsp position out of bounds - {:?}", range); + return; + }; + + let view = view_mut!(self.editor); + + // 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(new_range.head, new_range.anchor), + ); + if action.align_view(view, doc.id()) { + align_view(doc, view, Align::Center); + } + } + + Ok(serde_json::Value::Null) + } }; tokio::spawn(language_server!().reply(id, reply)); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ff0849f2093e..41e9d3299a57 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1227,7 +1227,7 @@ fn open_url(cx: &mut Context, url: Url, action: Action) { .unwrap_or_default(); if url.scheme() != "file" { - return open_external_url(cx, url); + return cx.jobs.callback(crate::open_external_url_callback(url)); } let content_type = std::fs::File::open(url.path()).and_then(|file| { @@ -1240,7 +1240,9 @@ fn open_url(cx: &mut Context, url: Url, action: Action) { // we attempt to open binary files - files that can't be open in helix - using external // program as well, e.g. pdf files or images match content_type { - Ok(content_inspector::ContentType::BINARY) => open_external_url(cx, url), + Ok(content_inspector::ContentType::BINARY) => { + cx.jobs.callback(crate::open_external_url_callback(url)) + } Ok(_) | Err(_) => { let path = &rel_path.join(url.path()); if path.is_dir() { @@ -1253,23 +1255,6 @@ fn open_url(cx: &mut Context, url: Url, action: Action) { } } -/// Opens URL in external program. -fn open_external_url(cx: &mut Context, url: Url) { - let commands = open::commands(url.as_str()); - cx.jobs.callback(async { - for cmd in commands { - let mut command = tokio::process::Command::new(cmd.get_program()); - command.args(cmd.get_args()); - if command.output().await.is_ok() { - return Ok(job::Callback::Editor(Box::new(|_| {}))); - } - } - Ok(job::Callback::Editor(Box::new(move |editor| { - editor.set_error("Opening URL in external program failed") - }))) - }); -} - fn extend_word_impl(cx: &mut Context, extend_fn: F) where F: Fn(RopeSlice, Range, usize) -> Range, diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs index a94c5e494991..a1d603298ec7 100644 --- a/helix-term/src/lib.rs +++ b/helix-term/src/lib.rs @@ -12,7 +12,11 @@ pub mod keymap; pub mod ui; use std::path::Path; +use futures_util::Future; use ignore::DirEntry; +use url::Url; + +pub use keymap::macros::*; #[cfg(not(windows))] fn true_color() -> bool { @@ -46,3 +50,22 @@ fn filter_picker_entry(entry: &DirEntry, root: &Path, dedup_symlinks: bool) -> b true } + +/// Opens URL in external program. +fn open_external_url_callback( + url: Url, +) -> impl Future> + Send + 'static { + let commands = open::commands(url.as_str()); + async { + for cmd in commands { + let mut command = tokio::process::Command::new(cmd.get_program()); + command.args(cmd.get_args()); + if command.output().await.is_ok() { + return Ok(job::Callback::Editor(Box::new(|_| {}))); + } + } + Ok(job::Callback::Editor(Box::new(move |editor| { + editor.set_error("Opening URL in external program failed") + }))) + } +} From 7633b26841f7d6cd103c7ee45bd89fec8b6e46c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Thu, 23 Nov 2023 00:34:25 +0100 Subject: [PATCH 2/4] add return --- helix-term/src/application.rs | 117 ++++++++++++++++++++-------------- 1 file changed, 69 insertions(+), 48 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 21c56db44ce7..74c0a04f7a14 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1180,58 +1180,79 @@ impl Application { let language_server = language_server!(); let offset_encoding = language_server.offset_encoding(); - if params.external.unwrap_or_default() { - self.jobs - .callback(crate::open_external_url_callback(params.uri)); - return; - } - - let path = match params.uri.to_file_path() { - Ok(path) => path, - Err(_) => { - log::error!("Unsupported file URI: {}", params.uri); - return; + let success = match params { + lsp::ShowDocumentParams { + external: Some(true), + uri, + .. + } => { + self.jobs.callback(crate::open_external_url_callback(uri)); + true } - }; - - let action = match params.take_focus { - Some(true) => helix_view::editor::Action::Replace, - _ => helix_view::editor::Action::HorizontalSplit, - }; + lsp::ShowDocumentParams { + uri, + selection, + take_focus, + .. + } => { + match uri.to_file_path() { + Ok(path) => { + let action = match take_focus { + Some(true) => helix_view::editor::Action::Replace, + _ => helix_view::editor::Action::HorizontalSplit, + }; - let doc = match self.editor.open(&path, action) { - Ok(id) => doc_mut!(self.editor, &id), - Err(err) => { - log::error!("failed to open path: {:?}: {:?}", params.uri, err); - return; + match self.editor.open(&path, action) { + Ok(id) => { + let doc = doc_mut!(self.editor, &id); + if let Some(range) = selection { + // TODO: convert inside server + if let Some(new_range) = lsp_range_to_range( + doc.text(), + range, + offset_encoding, + ) { + let view = view_mut!(self.editor); + + // 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( + new_range.head, + new_range.anchor, + ), + ); + if action.align_view(view, doc.id()) { + align_view(doc, view, Align::Center); + } + } else { + log::warn!( + "lsp position out of bounds - {:?}", + range + ); + }; + }; + true + } + Err(err) => { + log::error!( + "failed to open path: {:?}: {:?}", + uri, + err + ); + false + } + } + } + Err(err) => { + log::error!("unsupported file URI: {}: {:?}", uri, err); + false + } + } } }; - - if let Some(range) = params.selection { - // TODO: convert inside server - let new_range = if let Some(new_range) = - lsp_range_to_range(doc.text(), range, offset_encoding) - { - new_range - } else { - log::warn!("lsp position out of bounds - {:?}", range); - return; - }; - - let view = view_mut!(self.editor); - - // 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(new_range.head, new_range.anchor), - ); - if action.align_view(view, doc.id()) { - align_view(doc, view, Align::Center); - } - } - - Ok(serde_json::Value::Null) + Ok(json!(lsp::ShowDocumentResult { success })) } }; From ab2e872e1e0de92d429aa9cb3203fa1975654393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Thu, 23 Nov 2023 01:42:48 +0100 Subject: [PATCH 3/4] use vertical split --- helix-term/src/application.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 74c0a04f7a14..755fbed19e4f 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1199,7 +1199,7 @@ impl Application { Ok(path) => { let action = match take_focus { Some(true) => helix_view::editor::Action::Replace, - _ => helix_view::editor::Action::HorizontalSplit, + _ => helix_view::editor::Action::VerticalSplit, }; match self.editor.open(&path, action) { From f7cd43c577f7a62648ab7ab64c990fcdc4d79a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Wed, 13 Dec 2023 21:04:38 +0100 Subject: [PATCH 4/4] refactor --- helix-term/src/application.rs | 137 ++++++++++++++++------------------ 1 file changed, 64 insertions(+), 73 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 755fbed19e4f..121e027572bc 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1180,79 +1180,8 @@ impl Application { let language_server = language_server!(); let offset_encoding = language_server.offset_encoding(); - let success = match params { - lsp::ShowDocumentParams { - external: Some(true), - uri, - .. - } => { - self.jobs.callback(crate::open_external_url_callback(uri)); - true - } - lsp::ShowDocumentParams { - uri, - selection, - take_focus, - .. - } => { - match uri.to_file_path() { - Ok(path) => { - let action = match take_focus { - Some(true) => helix_view::editor::Action::Replace, - _ => helix_view::editor::Action::VerticalSplit, - }; - - match self.editor.open(&path, action) { - Ok(id) => { - let doc = doc_mut!(self.editor, &id); - if let Some(range) = selection { - // TODO: convert inside server - if let Some(new_range) = lsp_range_to_range( - doc.text(), - range, - offset_encoding, - ) { - let view = view_mut!(self.editor); - - // 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( - new_range.head, - new_range.anchor, - ), - ); - if action.align_view(view, doc.id()) { - align_view(doc, view, Align::Center); - } - } else { - log::warn!( - "lsp position out of bounds - {:?}", - range - ); - }; - }; - true - } - Err(err) => { - log::error!( - "failed to open path: {:?}: {:?}", - uri, - err - ); - false - } - } - } - Err(err) => { - log::error!("unsupported file URI: {}: {:?}", uri, err); - false - } - } - } - }; - Ok(json!(lsp::ShowDocumentResult { success })) + let result = self.handle_show_document(params, offset_encoding); + Ok(json!(result)) } }; @@ -1262,6 +1191,68 @@ impl Application { } } + fn handle_show_document( + &mut self, + params: lsp::ShowDocumentParams, + offset_encoding: helix_lsp::OffsetEncoding, + ) -> lsp::ShowDocumentResult { + if let lsp::ShowDocumentParams { + external: Some(true), + uri, + .. + } = params + { + self.jobs.callback(crate::open_external_url_callback(uri)); + return lsp::ShowDocumentResult { success: true }; + }; + + let lsp::ShowDocumentParams { + uri, + selection, + take_focus, + .. + } = params; + + let path = match uri.to_file_path() { + Ok(path) => path, + Err(err) => { + log::error!("unsupported file URI: {}: {:?}", uri, err); + return lsp::ShowDocumentResult { success: false }; + } + }; + + let action = match take_focus { + Some(true) => helix_view::editor::Action::Replace, + _ => helix_view::editor::Action::VerticalSplit, + }; + + let doc_id = match self.editor.open(&path, action) { + Ok(id) => id, + Err(err) => { + log::error!("failed to open path: {:?}: {:?}", uri, err); + return lsp::ShowDocumentResult { success: false }; + } + }; + + let doc = doc_mut!(self.editor, &doc_id); + if let Some(range) = selection { + // TODO: convert inside server + if let Some(new_range) = lsp_range_to_range(doc.text(), range, offset_encoding) { + let view = view_mut!(self.editor); + + // 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(new_range.head, new_range.anchor)); + if action.align_view(view, doc.id()) { + align_view(doc, view, Align::Center); + } + } else { + log::warn!("lsp position out of bounds - {:?}", range); + }; + }; + lsp::ShowDocumentResult { success: true } + } + async fn claim_term(&mut self) -> std::io::Result<()> { let terminal_config = self.config.load().editor.clone().into(); self.terminal.claim(terminal_config)