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:
parent
6754acd83f
commit
c60ba4ba04
4 changed files with 102 additions and 19 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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")
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue