From c60ba4ba04de56f37f4f4b19051bc4a1e68b3484 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= <matousdzivjak@gmail.com>
Date: Wed, 17 Jan 2024 17:24:38 +0000
Subject: [PATCH] feat(lsp): implement show document request (#8865)

* 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.

* add return

* use vertical split

* refactor

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
---
 helix-lsp/src/lib.rs          |  5 +++
 helix-term/src/application.rs | 70 +++++++++++++++++++++++++++++++++++
 helix-term/src/commands.rs    | 23 ++----------
 helix-term/src/lib.rs         | 23 ++++++++++++
 4 files changed, 102 insertions(+), 19 deletions(-)

diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs
index 34278cd5..83625897 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 01c120d0..1b0a06dd 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -3,6 +3,7 @@ use futures_util::Stream;
 use helix_core::{path::get_relative_path, pos_at_coords, syntax, Selection};
 use helix_lsp::{
     lsp::{self, notification::Notification},
+    util::lsp_range_to_range,
     LspProgressMap,
 };
 use helix_view::{
@@ -1100,6 +1101,13 @@ impl Application {
                         }
                         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))
+                    }
                 };
 
                 tokio::spawn(language_server!().reply(id, reply));
@@ -1108,6 +1116,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)
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index c9593380..e436e1cf 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<F>(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 a94c5e49..a1d60329 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<Output = Result<job::Callback, anyhow::Error>> + 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")
+        })))
+    }
+}