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>
This commit is contained in:
Matouš Dzivjak 2024-01-17 17:24:38 +00:00 committed by GitHub
parent 6754acd83f
commit c60ba4ba04
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 102 additions and 19 deletions

View file

@ -549,6 +549,7 @@ pub enum MethodCall {
WorkspaceConfiguration(lsp::ConfigurationParams), WorkspaceConfiguration(lsp::ConfigurationParams),
RegisterCapability(lsp::RegistrationParams), RegisterCapability(lsp::RegistrationParams),
UnregisterCapability(lsp::UnregistrationParams), UnregisterCapability(lsp::UnregistrationParams),
ShowDocument(lsp::ShowDocumentParams),
} }
impl MethodCall { impl MethodCall {
@ -576,6 +577,10 @@ impl MethodCall {
let params: lsp::UnregistrationParams = params.parse()?; let params: lsp::UnregistrationParams = params.parse()?;
Self::UnregisterCapability(params) Self::UnregisterCapability(params)
} }
lsp::request::ShowDocument::METHOD => {
let params: lsp::ShowDocumentParams = params.parse()?;
Self::ShowDocument(params)
}
_ => { _ => {
return Err(Error::Unhandled); return Err(Error::Unhandled);
} }

View file

@ -3,6 +3,7 @@ use futures_util::Stream;
use helix_core::{path::get_relative_path, pos_at_coords, syntax, Selection}; use helix_core::{path::get_relative_path, pos_at_coords, syntax, Selection};
use helix_lsp::{ use helix_lsp::{
lsp::{self, notification::Notification}, lsp::{self, notification::Notification},
util::lsp_range_to_range,
LspProgressMap, LspProgressMap,
}; };
use helix_view::{ use helix_view::{
@ -1100,6 +1101,13 @@ impl Application {
} }
Ok(serde_json::Value::Null) 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)); 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<()> { async fn claim_term(&mut self) -> std::io::Result<()> {
let terminal_config = self.config.load().editor.clone().into(); let terminal_config = self.config.load().editor.clone().into();
self.terminal.claim(terminal_config) self.terminal.claim(terminal_config)

View file

@ -1227,7 +1227,7 @@ fn open_url(cx: &mut Context, url: Url, action: Action) {
.unwrap_or_default(); .unwrap_or_default();
if url.scheme() != "file" { 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| { 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 // 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 // program as well, e.g. pdf files or images
match content_type { 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(_) => { Ok(_) | Err(_) => {
let path = &rel_path.join(url.path()); let path = &rel_path.join(url.path());
if path.is_dir() { 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) fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
where where
F: Fn(RopeSlice, Range, usize) -> Range, F: Fn(RopeSlice, Range, usize) -> Range,

View file

@ -12,7 +12,11 @@ pub mod keymap;
pub mod ui; pub mod ui;
use std::path::Path; use std::path::Path;
use futures_util::Future;
use ignore::DirEntry; use ignore::DirEntry;
use url::Url;
pub use keymap::macros::*;
#[cfg(not(windows))] #[cfg(not(windows))]
fn true_color() -> bool { fn true_color() -> bool {
@ -46,3 +50,22 @@ fn filter_picker_entry(entry: &DirEntry, root: &Path, dedup_symlinks: bool) -> b
true 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")
})))
}
}