Skip to content

Commit

Permalink
feat(lsp): implement show document request (helix-editor#8865)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
2 people authored and Schuyler Mortimer committed Jul 10, 2024
1 parent e9e55cb commit 7a8520b
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 19 deletions.
5 changes: 5 additions & 0 deletions helix-lsp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@ pub enum MethodCall {
WorkspaceConfiguration(lsp::ConfigurationParams),
RegisterCapability(lsp::RegistrationParams),
UnregisterCapability(lsp::UnregistrationParams),
ShowDocument(lsp::ShowDocumentParams),
ShowMessageRequest(lsp::ShowMessageRequestParams),
}

Expand Down Expand Up @@ -577,6 +578,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)
}
lsp::request::ShowMessageRequest::METHOD => {
Self::ShowMessageRequest(params.parse::<lsp::ShowMessageRequestParams>()?)
}
Expand Down
70 changes: 70 additions & 0 deletions helix-term/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use futures_util::Stream;
use helix_core::{path::get_relative_path, pos_at_coords, syntax, Rope, Selection};
use helix_lsp::{
lsp::{self, notification::Notification, MessageType},
util::lsp_range_to_range,
LspProgressMap,
};
use helix_view::{
Expand Down Expand Up @@ -1152,6 +1153,13 @@ impl Application {
}
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))
}
};

if let Some(reply) = reply {
Expand All @@ -1170,6 +1178,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)
Expand Down
23 changes: 4 additions & 19 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
Expand All @@ -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() {
Expand All @@ -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,
Expand Down
23 changes: 23 additions & 0 deletions helix-term/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
})))
}
}

0 comments on commit 7a8520b

Please sign in to comment.