Filter language servers also by capabilities in doc.language_servers_with_feature
* Add `helix_lsp::client::Client::supports_feature(&self, LanguageServerFeature)` * Extend `doc.language_servers_with_feature` to use this method as filter as well * Add macro `language_server_with_feature!` to reduce boilerplate for non-mergeable language server requests (like goto-definition) * Refactored most of the `find_map` code to use the either the macro or filter directly via `doc.language_servers_with_feature`
This commit is contained in:
parent
9d089c27c7
commit
ff26208427
5 changed files with 349 additions and 311 deletions
|
@ -4,7 +4,7 @@ use crate::{
|
||||||
Call, Error, OffsetEncoding, Result,
|
Call, Error, OffsetEncoding, Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
use helix_core::{find_workspace, path, ChangeSet, Rope};
|
use helix_core::{find_workspace, path, syntax::LanguageServerFeature, ChangeSet, Rope};
|
||||||
use helix_loader::{self, VERSION_AND_GIT_HASH};
|
use helix_loader::{self, VERSION_AND_GIT_HASH};
|
||||||
use lsp::{
|
use lsp::{
|
||||||
notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
|
notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
|
||||||
|
@ -276,6 +276,93 @@ impl Client {
|
||||||
.expect("language server not yet initialized!")
|
.expect("language server not yet initialized!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline] // TODO inline?
|
||||||
|
pub fn supports_feature(&self, feature: LanguageServerFeature) -> bool {
|
||||||
|
let capabilities = match self.capabilities.get() {
|
||||||
|
Some(capabilities) => capabilities,
|
||||||
|
None => return false, // not initialized, TODO unwrap/expect instead?
|
||||||
|
};
|
||||||
|
match feature {
|
||||||
|
LanguageServerFeature::Format => matches!(
|
||||||
|
capabilities.document_formatting_provider,
|
||||||
|
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
|
||||||
|
),
|
||||||
|
LanguageServerFeature::GotoDeclaration => matches!(
|
||||||
|
capabilities.declaration_provider,
|
||||||
|
Some(
|
||||||
|
lsp::DeclarationCapability::Simple(true)
|
||||||
|
| lsp::DeclarationCapability::RegistrationOptions(_)
|
||||||
|
| lsp::DeclarationCapability::Options(_),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
LanguageServerFeature::GotoDefinition => matches!(
|
||||||
|
capabilities.definition_provider,
|
||||||
|
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
|
||||||
|
),
|
||||||
|
LanguageServerFeature::GotoTypeDefinition => matches!(
|
||||||
|
capabilities.type_definition_provider,
|
||||||
|
Some(
|
||||||
|
lsp::TypeDefinitionProviderCapability::Simple(true)
|
||||||
|
| lsp::TypeDefinitionProviderCapability::Options(_),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
LanguageServerFeature::GotoReference => matches!(
|
||||||
|
capabilities.references_provider,
|
||||||
|
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
|
||||||
|
),
|
||||||
|
LanguageServerFeature::GotoImplementation => matches!(
|
||||||
|
capabilities.implementation_provider,
|
||||||
|
Some(
|
||||||
|
lsp::ImplementationProviderCapability::Simple(true)
|
||||||
|
| lsp::ImplementationProviderCapability::Options(_),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
LanguageServerFeature::SignatureHelp => capabilities.signature_help_provider.is_some(),
|
||||||
|
LanguageServerFeature::Hover => matches!(
|
||||||
|
capabilities.hover_provider,
|
||||||
|
Some(
|
||||||
|
lsp::HoverProviderCapability::Simple(true)
|
||||||
|
| lsp::HoverProviderCapability::Options(_),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
LanguageServerFeature::DocumentHighlight => matches!(
|
||||||
|
capabilities.document_highlight_provider,
|
||||||
|
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
|
||||||
|
),
|
||||||
|
LanguageServerFeature::Completion => capabilities.completion_provider.is_some(),
|
||||||
|
LanguageServerFeature::CodeAction => matches!(
|
||||||
|
capabilities.code_action_provider,
|
||||||
|
Some(
|
||||||
|
lsp::CodeActionProviderCapability::Simple(true)
|
||||||
|
| lsp::CodeActionProviderCapability::Options(_),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
LanguageServerFeature::WorkspaceCommand => {
|
||||||
|
capabilities.execute_command_provider.is_some()
|
||||||
|
}
|
||||||
|
LanguageServerFeature::DocumentSymbols => matches!(
|
||||||
|
capabilities.document_symbol_provider,
|
||||||
|
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
|
||||||
|
),
|
||||||
|
LanguageServerFeature::WorkspaceSymbols => matches!(
|
||||||
|
capabilities.workspace_symbol_provider,
|
||||||
|
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
|
||||||
|
),
|
||||||
|
LanguageServerFeature::Diagnostics => true, // there's no extra server capability
|
||||||
|
LanguageServerFeature::RenameSymbol => matches!(
|
||||||
|
capabilities.rename_provider,
|
||||||
|
Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_))
|
||||||
|
),
|
||||||
|
LanguageServerFeature::InlayHints => matches!(
|
||||||
|
capabilities.inlay_hint_provider,
|
||||||
|
Some(
|
||||||
|
lsp::OneOf::Left(true)
|
||||||
|
| lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn offset_encoding(&self) -> OffsetEncoding {
|
pub fn offset_encoding(&self) -> OffsetEncoding {
|
||||||
self.capabilities()
|
self.capabilities()
|
||||||
.position_encoding
|
.position_encoding
|
||||||
|
@ -1301,21 +1388,13 @@ impl Client {
|
||||||
Some(self.call::<lsp::request::CodeActionRequest>(params))
|
Some(self.call::<lsp::request::CodeActionRequest>(params))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn supports_rename(&self) -> bool {
|
|
||||||
let capabilities = self.capabilities.get().unwrap();
|
|
||||||
matches!(
|
|
||||||
capabilities.rename_provider,
|
|
||||||
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rename_symbol(
|
pub fn rename_symbol(
|
||||||
&self,
|
&self,
|
||||||
text_document: lsp::TextDocumentIdentifier,
|
text_document: lsp::TextDocumentIdentifier,
|
||||||
position: lsp::Position,
|
position: lsp::Position,
|
||||||
new_name: String,
|
new_name: String,
|
||||||
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
|
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
|
||||||
if !self.supports_rename() {
|
if !self.supports_feature(LanguageServerFeature::RenameSymbol) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3236,10 +3236,8 @@ pub mod insert {
|
||||||
let trigger_completion = doc
|
let trigger_completion = doc
|
||||||
.language_servers_with_feature(LanguageServerFeature::Completion)
|
.language_servers_with_feature(LanguageServerFeature::Completion)
|
||||||
.any(|ls| {
|
.any(|ls| {
|
||||||
let capabilities = ls.capabilities();
|
|
||||||
|
|
||||||
// TODO: what if trigger is multiple chars long
|
// TODO: what if trigger is multiple chars long
|
||||||
matches!(&capabilities.completion_provider, Some(lsp::CompletionOptions {
|
matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions {
|
||||||
trigger_characters: Some(triggers),
|
trigger_characters: Some(triggers),
|
||||||
..
|
..
|
||||||
}) if triggers.iter().any(|trigger| trigger.contains(ch)))
|
}) if triggers.iter().any(|trigger| trigger.contains(ch)))
|
||||||
|
@ -3252,51 +3250,39 @@ pub mod insert {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn signature_help(cx: &mut Context, ch: char) {
|
fn signature_help(cx: &mut Context, ch: char) {
|
||||||
use futures_util::FutureExt;
|
|
||||||
use helix_lsp::lsp;
|
use helix_lsp::lsp;
|
||||||
// if ch matches signature_help char, trigger
|
// if ch matches signature_help char, trigger
|
||||||
let (view, doc) = current!(cx.editor);
|
let doc = doc_mut!(cx.editor);
|
||||||
// lsp doesn't tell us when to close the signature help, so we request
|
// TODO support multiple language servers (not just the first that is found), likely by merging UI somehow
|
||||||
// the help information again after common close triggers which should
|
let Some(language_server) = doc
|
||||||
// return None, which in turn closes the popup.
|
|
||||||
let close_triggers = &[')', ';', '.'];
|
|
||||||
// TODO support multiple language servers (not just the first that is found)
|
|
||||||
let future = doc
|
|
||||||
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
|
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
|
||||||
.find_map(|ls| {
|
.next()
|
||||||
let capabilities = ls.capabilities();
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
match capabilities {
|
let capabilities = language_server.capabilities();
|
||||||
lsp::ServerCapabilities {
|
|
||||||
signature_help_provider:
|
|
||||||
Some(lsp::SignatureHelpOptions {
|
|
||||||
trigger_characters: Some(triggers),
|
|
||||||
// TODO: retrigger_characters
|
|
||||||
..
|
|
||||||
}),
|
|
||||||
..
|
|
||||||
} if triggers.iter().any(|trigger| trigger.contains(ch))
|
|
||||||
|| close_triggers.contains(&ch) =>
|
|
||||||
{
|
|
||||||
let pos = doc.position(view.id, ls.offset_encoding());
|
|
||||||
ls.text_document_signature_help(doc.identifier(), pos, None)
|
|
||||||
}
|
|
||||||
_ if close_triggers.contains(&ch) => ls.text_document_signature_help(
|
|
||||||
doc.identifier(),
|
|
||||||
doc.position(view.id, ls.offset_encoding()),
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
// TODO: what if trigger is multiple chars long
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(future) = future {
|
if let lsp::ServerCapabilities {
|
||||||
super::signature_help_impl_with_future(
|
signature_help_provider:
|
||||||
cx,
|
Some(lsp::SignatureHelpOptions {
|
||||||
future.boxed(),
|
trigger_characters: Some(triggers),
|
||||||
SignatureHelpInvoked::Automatic,
|
// TODO: retrigger_characters
|
||||||
)
|
..
|
||||||
|
}),
|
||||||
|
..
|
||||||
|
} = capabilities
|
||||||
|
{
|
||||||
|
// TODO: what if trigger is multiple chars long
|
||||||
|
let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch));
|
||||||
|
// lsp doesn't tell us when to close the signature help, so we request
|
||||||
|
// the help information again after common close triggers which should
|
||||||
|
// return None, which in turn closes the popup.
|
||||||
|
let close_triggers = &[')', ';', '.'];
|
||||||
|
|
||||||
|
if is_trigger || close_triggers.contains(&ch) {
|
||||||
|
super::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3310,7 +3296,7 @@ pub mod insert {
|
||||||
Some(transaction)
|
Some(transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
use helix_core::{auto_pairs, syntax::LanguageServerFeature};
|
use helix_core::auto_pairs;
|
||||||
|
|
||||||
pub fn insert_char(cx: &mut Context, c: char) {
|
pub fn insert_char(cx: &mut Context, c: char) {
|
||||||
let (view, doc) = current_ref!(cx.editor);
|
let (view, doc) = current_ref!(cx.editor);
|
||||||
|
@ -4065,39 +4051,44 @@ fn format_selections(cx: &mut Context) {
|
||||||
.set_error("format_selections only supports a single selection for now");
|
.set_error("format_selections only supports a single selection for now");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let future_offset_encoding = doc
|
|
||||||
|
// TODO extra LanguageServerFeature::FormatSelections?
|
||||||
|
// maybe such that LanguageServerFeature::Format contains it as well
|
||||||
|
let Some(language_server) = doc
|
||||||
.language_servers_with_feature(LanguageServerFeature::Format)
|
.language_servers_with_feature(LanguageServerFeature::Format)
|
||||||
.find_map(|language_server| {
|
.find(|ls| {
|
||||||
let offset_encoding = language_server.offset_encoding();
|
matches!(
|
||||||
let ranges: Vec<lsp::Range> = doc
|
ls.capabilities().document_range_formatting_provider,
|
||||||
.selection(view_id)
|
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
|
||||||
.iter()
|
)
|
||||||
.map(|range| range_to_lsp_range(doc.text(), *range, offset_encoding))
|
})
|
||||||
.collect();
|
else {
|
||||||
|
cx.editor
|
||||||
// TODO: handle fails
|
.set_error("No configured language server does not support range formatting");
|
||||||
// TODO: concurrent map over all ranges
|
return;
|
||||||
|
|
||||||
let range = ranges[0];
|
|
||||||
|
|
||||||
let future = language_server.text_document_range_formatting(
|
|
||||||
doc.identifier(),
|
|
||||||
range,
|
|
||||||
lsp::FormattingOptions::default(),
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
Some((future, offset_encoding))
|
|
||||||
});
|
|
||||||
|
|
||||||
let (future, offset_encoding) = match future_offset_encoding {
|
|
||||||
Some(future_offset_encoding) => future_offset_encoding,
|
|
||||||
None => {
|
|
||||||
cx.editor
|
|
||||||
.set_error("No configured language server supports range formatting");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let offset_encoding = language_server.offset_encoding();
|
||||||
|
let ranges: Vec<lsp::Range> = doc
|
||||||
|
.selection(view_id)
|
||||||
|
.iter()
|
||||||
|
.map(|range| range_to_lsp_range(doc.text(), *range, offset_encoding))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// TODO: handle fails
|
||||||
|
// TODO: concurrent map over all ranges
|
||||||
|
|
||||||
|
let range = ranges[0];
|
||||||
|
|
||||||
|
let future = language_server
|
||||||
|
.text_document_range_formatting(
|
||||||
|
doc.identifier(),
|
||||||
|
range,
|
||||||
|
lsp::FormattingOptions::default(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let edits = tokio::task::block_in_place(|| helix_lsp::block_on(future)).unwrap_or_default();
|
let edits = tokio::task::block_in_place(|| helix_lsp::block_on(future)).unwrap_or_default();
|
||||||
|
|
||||||
let transaction =
|
let transaction =
|
||||||
|
@ -4247,15 +4238,15 @@ pub fn completion(cx: &mut Context) {
|
||||||
|
|
||||||
let mut futures: FuturesUnordered<_> = doc
|
let mut futures: FuturesUnordered<_> = doc
|
||||||
.language_servers_with_feature(LanguageServerFeature::Completion)
|
.language_servers_with_feature(LanguageServerFeature::Completion)
|
||||||
// TODO this should probably already been filtered in something like "language_servers_with_feature"
|
|
||||||
.filter(|ls| seen_language_servers.insert(ls.id()))
|
.filter(|ls| seen_language_servers.insert(ls.id()))
|
||||||
.filter_map(|language_server| {
|
.map(|language_server| {
|
||||||
let language_server_id = language_server.id();
|
let language_server_id = language_server.id();
|
||||||
let offset_encoding = language_server.offset_encoding();
|
let offset_encoding = language_server.offset_encoding();
|
||||||
let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding);
|
let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding);
|
||||||
let completion_request = language_server.completion(doc.identifier(), pos, None)?;
|
let doc_id = doc.identifier();
|
||||||
|
let completion_request = language_server.completion(doc_id, pos, None).unwrap();
|
||||||
|
|
||||||
Some(async move {
|
async move {
|
||||||
let json = completion_request.await?;
|
let json = completion_request.await?;
|
||||||
let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?;
|
let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?;
|
||||||
|
|
||||||
|
@ -4277,7 +4268,7 @@ pub fn completion(cx: &mut Context) {
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
anyhow::Ok(items)
|
anyhow::Ok(items)
|
||||||
})
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,28 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Gets the first language server that is attached to a document which supports a specific feature.
|
||||||
|
/// If there is no configured language server that supports the feature, this displays a status message.
|
||||||
|
/// Using this macro in a context where the editor automatically queries the LSP
|
||||||
|
/// (instead of when the user explicitly does so via a keybind like `gd`)
|
||||||
|
/// will spam the "No configured language server supports <feature>" status message confusingly.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! language_server_with_feature {
|
||||||
|
($editor:expr, $doc:expr, $feature:expr) => {{
|
||||||
|
let language_server = $doc.language_servers_with_feature($feature).next();
|
||||||
|
match language_server {
|
||||||
|
Some(language_server) => language_server,
|
||||||
|
None => {
|
||||||
|
$editor.set_status(format!(
|
||||||
|
"No configured language server supports {}",
|
||||||
|
$feature
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
impl ui::menu::Item for lsp::Location {
|
impl ui::menu::Item for lsp::Location {
|
||||||
/// Current working directory.
|
/// Current working directory.
|
||||||
type Data = PathBuf;
|
type Data = PathBuf;
|
||||||
|
@ -361,36 +383,38 @@ pub fn symbol_picker(cx: &mut Context) {
|
||||||
let mut futures: FuturesUnordered<_> = doc
|
let mut futures: FuturesUnordered<_> = doc
|
||||||
.language_servers_with_feature(LanguageServerFeature::DocumentSymbols)
|
.language_servers_with_feature(LanguageServerFeature::DocumentSymbols)
|
||||||
.filter(|ls| seen_language_servers.insert(ls.id()))
|
.filter(|ls| seen_language_servers.insert(ls.id()))
|
||||||
.filter_map(|ls| {
|
.map(|language_server| {
|
||||||
let request = ls.document_symbols(doc.identifier())?;
|
let request = language_server.document_symbols(doc.identifier()).unwrap();
|
||||||
Some((request, ls.offset_encoding(), doc.identifier()))
|
let offset_encoding = language_server.offset_encoding();
|
||||||
})
|
let doc_id = doc.identifier();
|
||||||
.map(|(request, offset_encoding, doc_id)| async move {
|
|
||||||
let json = request.await?;
|
async move {
|
||||||
let response: Option<lsp::DocumentSymbolResponse> = serde_json::from_value(json)?;
|
let json = request.await?;
|
||||||
let symbols = match response {
|
let response: Option<lsp::DocumentSymbolResponse> = serde_json::from_value(json)?;
|
||||||
Some(symbols) => symbols,
|
let symbols = match response {
|
||||||
None => return anyhow::Ok(vec![]),
|
Some(symbols) => symbols,
|
||||||
};
|
None => return anyhow::Ok(vec![]),
|
||||||
// lsp has two ways to represent symbols (flat/nested)
|
};
|
||||||
// convert the nested variant to flat, so that we have a homogeneous list
|
// lsp has two ways to represent symbols (flat/nested)
|
||||||
let symbols = match symbols {
|
// convert the nested variant to flat, so that we have a homogeneous list
|
||||||
lsp::DocumentSymbolResponse::Flat(symbols) => symbols
|
let symbols = match symbols {
|
||||||
.into_iter()
|
lsp::DocumentSymbolResponse::Flat(symbols) => symbols
|
||||||
.map(|symbol| SymbolInformationItem {
|
.into_iter()
|
||||||
symbol,
|
.map(|symbol| SymbolInformationItem {
|
||||||
offset_encoding,
|
symbol,
|
||||||
})
|
offset_encoding,
|
||||||
.collect(),
|
})
|
||||||
lsp::DocumentSymbolResponse::Nested(symbols) => {
|
.collect(),
|
||||||
let mut flat_symbols = Vec::new();
|
lsp::DocumentSymbolResponse::Nested(symbols) => {
|
||||||
for symbol in symbols {
|
let mut flat_symbols = Vec::new();
|
||||||
nested_to_flat(&mut flat_symbols, &doc_id, symbol, offset_encoding)
|
for symbol in symbols {
|
||||||
|
nested_to_flat(&mut flat_symbols, &doc_id, symbol, offset_encoding)
|
||||||
|
}
|
||||||
|
flat_symbols
|
||||||
}
|
}
|
||||||
flat_symbols
|
};
|
||||||
}
|
Ok(symbols)
|
||||||
};
|
}
|
||||||
Ok(symbols)
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let current_url = doc.url();
|
let current_url = doc.url();
|
||||||
|
@ -425,20 +449,24 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
|
||||||
let mut futures: FuturesUnordered<_> = doc
|
let mut futures: FuturesUnordered<_> = doc
|
||||||
.language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
|
.language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
|
||||||
.filter(|ls| seen_language_servers.insert(ls.id()))
|
.filter(|ls| seen_language_servers.insert(ls.id()))
|
||||||
.filter_map(|ls| Some((ls.workspace_symbols(pattern.clone())?, ls.offset_encoding())))
|
.map(|language_server| {
|
||||||
.map(|(request, offset_encoding)| async move {
|
let request = language_server.workspace_symbols(pattern.clone()).unwrap();
|
||||||
let json = request.await?;
|
let offset_encoding = language_server.offset_encoding();
|
||||||
|
async move {
|
||||||
|
let json = request.await?;
|
||||||
|
|
||||||
let response = serde_json::from_value::<Option<Vec<lsp::SymbolInformation>>>(json)?
|
let response =
|
||||||
.unwrap_or_default()
|
serde_json::from_value::<Option<Vec<lsp::SymbolInformation>>>(json)?
|
||||||
.into_iter()
|
.unwrap_or_default()
|
||||||
.map(|symbol| SymbolInformationItem {
|
.into_iter()
|
||||||
symbol,
|
.map(|symbol| SymbolInformationItem {
|
||||||
offset_encoding,
|
symbol,
|
||||||
})
|
offset_encoding,
|
||||||
.collect();
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
anyhow::Ok(response)
|
anyhow::Ok(response)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
@ -1043,22 +1071,19 @@ where
|
||||||
F: Future<Output = helix_lsp::Result<serde_json::Value>> + 'static + Send,
|
F: Future<Output = helix_lsp::Result<serde_json::Value>> + 'static + Send,
|
||||||
{
|
{
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
if let Some((future, offset_encoding)) =
|
|
||||||
doc.run_on_first_supported_language_server(view.id, feature, |ls, encoding, pos, doc_id| {
|
let language_server = language_server_with_feature!(cx.editor, doc, feature);
|
||||||
Some((request_provider(ls, pos, doc_id)?, encoding))
|
let offset_encoding = language_server.offset_encoding();
|
||||||
})
|
let pos = doc.position(view.id, offset_encoding);
|
||||||
{
|
let future = request_provider(language_server, pos, doc.identifier()).unwrap();
|
||||||
cx.callback(
|
|
||||||
future,
|
cx.callback(
|
||||||
move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
|
future,
|
||||||
let items = to_locations(response);
|
move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
|
||||||
goto_impl(editor, compositor, items, offset_encoding);
|
let items = to_locations(response);
|
||||||
},
|
goto_impl(editor, compositor, items, offset_encoding);
|
||||||
);
|
},
|
||||||
} else {
|
);
|
||||||
cx.editor
|
|
||||||
.set_error("No configured language server supports {feature}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn goto_declaration(cx: &mut Context) {
|
pub fn goto_declaration(cx: &mut Context) {
|
||||||
|
@ -1096,32 +1121,29 @@ pub fn goto_implementation(cx: &mut Context) {
|
||||||
pub fn goto_reference(cx: &mut Context) {
|
pub fn goto_reference(cx: &mut Context) {
|
||||||
let config = cx.editor.config();
|
let config = cx.editor.config();
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
if let Some((future, offset_encoding)) = doc.run_on_first_supported_language_server(
|
|
||||||
view.id,
|
// TODO could probably support multiple language servers,
|
||||||
LanguageServerFeature::GotoReference,
|
// not sure if there's a real practical use case for this though
|
||||||
|ls, encoding, pos, doc_id| {
|
let language_server =
|
||||||
Some((
|
language_server_with_feature!(cx.editor, doc, LanguageServerFeature::GotoReference);
|
||||||
ls.goto_reference(
|
let offset_encoding = language_server.offset_encoding();
|
||||||
doc_id,
|
let pos = doc.position(view.id, offset_encoding);
|
||||||
pos,
|
let future = language_server
|
||||||
config.lsp.goto_reference_include_declaration,
|
.goto_reference(
|
||||||
None,
|
doc.identifier(),
|
||||||
)?,
|
pos,
|
||||||
encoding,
|
config.lsp.goto_reference_include_declaration,
|
||||||
))
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx.callback(
|
||||||
|
future,
|
||||||
|
move |editor, compositor, response: Option<Vec<lsp::Location>>| {
|
||||||
|
let items = response.unwrap_or_default();
|
||||||
|
goto_impl(editor, compositor, items, offset_encoding);
|
||||||
},
|
},
|
||||||
) {
|
);
|
||||||
cx.callback(
|
|
||||||
future,
|
|
||||||
move |editor, compositor, response: Option<Vec<lsp::Location>>| {
|
|
||||||
let items = response.unwrap_or_default();
|
|
||||||
goto_impl(editor, compositor, items, offset_encoding);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
cx.editor
|
|
||||||
.set_error("No configured language server supports goto-reference");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||||
|
@ -1145,19 +1167,15 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
|
||||||
language_server.text_document_signature_help(doc.identifier(), pos, None)
|
language_server.text_document_signature_help(doc.identifier(), pos, None)
|
||||||
});
|
});
|
||||||
|
|
||||||
let future = match future {
|
let Some(future) = future else {
|
||||||
Some(future) => future.boxed(),
|
// Do not show the message if signature help was invoked
|
||||||
None => {
|
// automatically on backspace, trigger characters, etc.
|
||||||
// Do not show the message if signature help was invoked
|
if invoked == SignatureHelpInvoked::Manual {
|
||||||
// automatically on backspace, trigger characters, etc.
|
cx.editor.set_error("No configured language server supports signature-help");
|
||||||
if invoked == SignatureHelpInvoked::Manual {
|
|
||||||
cx.editor
|
|
||||||
.set_error("No configured language server supports signature-help");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
};
|
};
|
||||||
signature_help_impl_with_future(cx, future, invoked);
|
signature_help_impl_with_future(cx, future.boxed(), invoked);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn signature_help_impl_with_future(
|
pub fn signature_help_impl_with_future(
|
||||||
|
@ -1272,22 +1290,14 @@ pub fn signature_help_impl_with_future(
|
||||||
pub fn hover(cx: &mut Context) {
|
pub fn hover(cx: &mut Context) {
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
|
|
||||||
|
// TODO support multiple language servers (merge UI somehow)
|
||||||
|
let language_server =
|
||||||
|
language_server_with_feature!(cx.editor, doc, LanguageServerFeature::Hover);
|
||||||
// TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier
|
// TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier
|
||||||
let request = doc
|
let pos = doc.position(view.id, language_server.offset_encoding());
|
||||||
.language_servers_with_feature(LanguageServerFeature::Hover)
|
let future = language_server
|
||||||
.find_map(|language_server| {
|
.text_document_hover(doc.identifier(), pos, None)
|
||||||
let pos = doc.position(view.id, language_server.offset_encoding());
|
.unwrap();
|
||||||
language_server.text_document_hover(doc.identifier(), pos, None)
|
|
||||||
});
|
|
||||||
|
|
||||||
let future = match request {
|
|
||||||
Some(future) => future,
|
|
||||||
None => {
|
|
||||||
cx.editor
|
|
||||||
.set_error("No configured language server supports hover");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.callback(
|
cx.callback(
|
||||||
future,
|
future,
|
||||||
|
@ -1381,34 +1391,26 @@ pub fn rename_symbol(cx: &mut Context) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
let request = doc
|
|
||||||
.language_servers_with_feature(LanguageServerFeature::RenameSymbol)
|
|
||||||
.find_map(|language_server| {
|
|
||||||
if let Some(language_server_id) = language_server_id {
|
|
||||||
if language_server.id() != language_server_id {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let offset_encoding = language_server.offset_encoding();
|
|
||||||
let pos = doc.position(view.id, offset_encoding);
|
|
||||||
let future = language_server.rename_symbol(
|
|
||||||
doc.identifier(),
|
|
||||||
pos,
|
|
||||||
input.to_string(),
|
|
||||||
)?;
|
|
||||||
Some((future, offset_encoding))
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some((future, offset_encoding)) = request {
|
let Some(language_server) = doc
|
||||||
match block_on(future) {
|
.language_servers_with_feature(LanguageServerFeature::RenameSymbol)
|
||||||
Ok(edits) => {
|
.find(|ls| language_server_id.is_none() || Some(ls.id()) == language_server_id)
|
||||||
let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits);
|
else {
|
||||||
}
|
cx.editor.set_error("No configured language server supports symbol renaming");
|
||||||
Err(err) => cx.editor.set_error(err.to_string()),
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let offset_encoding = language_server.offset_encoding();
|
||||||
|
let pos = doc.position(view.id, offset_encoding);
|
||||||
|
let future = language_server
|
||||||
|
.rename_symbol(doc.identifier(), pos, input.to_string())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match block_on(future) {
|
||||||
|
Ok(edits) => {
|
||||||
|
let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits);
|
||||||
}
|
}
|
||||||
} else {
|
Err(err) => cx.editor.set_error(err.to_string()),
|
||||||
cx.editor
|
|
||||||
.set_error("No configured language server supports symbol renaming");
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1417,20 +1419,28 @@ pub fn rename_symbol(cx: &mut Context) {
|
||||||
Box::new(prompt)
|
Box::new(prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current_ref!(cx.editor);
|
||||||
|
|
||||||
let prepare_rename_request = doc
|
let language_server_with_prepare_rename_support = doc
|
||||||
.language_servers_with_feature(LanguageServerFeature::RenameSymbol)
|
.language_servers_with_feature(LanguageServerFeature::RenameSymbol)
|
||||||
.find_map(|language_server| {
|
.find(|ls| {
|
||||||
let offset_encoding = language_server.offset_encoding();
|
matches!(
|
||||||
let pos = doc.position(view.id, offset_encoding);
|
ls.capabilities().rename_provider,
|
||||||
let future = language_server.prepare_rename(doc.identifier(), pos)?;
|
Some(lsp::OneOf::Right(lsp::RenameOptions {
|
||||||
Some((future, offset_encoding, language_server.id()))
|
prepare_provider: Some(true),
|
||||||
|
..
|
||||||
|
}))
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
match prepare_rename_request {
|
if let Some(language_server) = language_server_with_prepare_rename_support {
|
||||||
// Language server supports textDocument/prepareRename, use it.
|
let ls_id = language_server.id();
|
||||||
Some((future, offset_encoding, ls_id)) => cx.callback(
|
let offset_encoding = language_server.offset_encoding();
|
||||||
|
let pos = doc.position(view.id, offset_encoding);
|
||||||
|
let future = language_server
|
||||||
|
.prepare_rename(doc.identifier(), pos)
|
||||||
|
.unwrap();
|
||||||
|
cx.callback(
|
||||||
future,
|
future,
|
||||||
move |editor, compositor, response: Option<lsp::PrepareRenameResponse>| {
|
move |editor, compositor, response: Option<lsp::PrepareRenameResponse>| {
|
||||||
let prefill = match get_prefill_from_lsp_response(editor, offset_encoding, response)
|
let prefill = match get_prefill_from_lsp_response(editor, offset_encoding, response)
|
||||||
|
@ -1446,38 +1456,23 @@ pub fn rename_symbol(cx: &mut Context) {
|
||||||
|
|
||||||
compositor.push(prompt);
|
compositor.push(prompt);
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
// Language server does not support textDocument/prepareRename, fall back
|
} else {
|
||||||
// to word boundary selection.
|
let prefill = get_prefill_from_word_boundary(cx.editor);
|
||||||
None => {
|
let prompt = create_rename_prompt(cx.editor, prefill, None);
|
||||||
let prefill = get_prefill_from_word_boundary(cx.editor);
|
cx.push_layer(prompt);
|
||||||
|
}
|
||||||
let prompt = create_rename_prompt(cx.editor, prefill, None);
|
|
||||||
|
|
||||||
cx.push_layer(prompt);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
|
pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
let future_offset_encoding = doc
|
let language_server =
|
||||||
.language_servers_with_feature(LanguageServerFeature::DocumentHighlight)
|
language_server_with_feature!(cx.editor, doc, LanguageServerFeature::DocumentHighlight);
|
||||||
.find_map(|language_server| {
|
let offset_encoding = language_server.offset_encoding();
|
||||||
let offset_encoding = language_server.offset_encoding();
|
let pos = doc.position(view.id, offset_encoding);
|
||||||
let pos = doc.position(view.id, offset_encoding);
|
let future = language_server
|
||||||
let future =
|
.text_document_document_highlight(doc.identifier(), pos, None)
|
||||||
language_server.text_document_document_highlight(doc.identifier(), pos, None)?;
|
.unwrap();
|
||||||
Some((future, offset_encoding))
|
|
||||||
});
|
|
||||||
let (future, offset_encoding) = match future_offset_encoding {
|
|
||||||
Some(future_offset_encoding) => future_offset_encoding,
|
|
||||||
None => {
|
|
||||||
cx.editor
|
|
||||||
.set_error("No configured language server supports document-highlight");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.callback(
|
cx.callback(
|
||||||
future,
|
future,
|
||||||
|
@ -1532,16 +1527,9 @@ fn compute_inlay_hints_for_view(
|
||||||
let view_id = view.id;
|
let view_id = view.id;
|
||||||
let doc_id = view.doc;
|
let doc_id = view.doc;
|
||||||
|
|
||||||
let mut language_servers = doc.language_servers_with_feature(LanguageServerFeature::InlayHints);
|
let language_server = doc
|
||||||
let language_server = language_servers.find(|language_server| {
|
.language_servers_with_feature(LanguageServerFeature::InlayHints)
|
||||||
matches!(
|
.next()?;
|
||||||
language_server.capabilities().inlay_hint_provider,
|
|
||||||
Some(
|
|
||||||
lsp::OneOf::Left(true)
|
|
||||||
| lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let doc_text = doc.text();
|
let doc_text = doc.text();
|
||||||
let len_lines = doc_text.len_lines();
|
let len_lines = doc_text.len_lines();
|
||||||
|
|
|
@ -394,14 +394,11 @@ pub mod completers {
|
||||||
pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
|
pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
|
||||||
let matcher = Matcher::default();
|
let matcher = Matcher::default();
|
||||||
|
|
||||||
let options = match doc!(editor)
|
let Some(options) = doc!(editor)
|
||||||
.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
|
.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
|
||||||
.find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
|
.find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
|
||||||
{
|
else {
|
||||||
Some(options) => options,
|
return vec![];
|
||||||
None => {
|
|
||||||
return vec![];
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut matches: Vec<_> = options
|
let mut matches: Vec<_> = options
|
||||||
|
|
|
@ -580,7 +580,7 @@ where
|
||||||
*mut_ref = f(mem::take(mut_ref));
|
*mut_ref = f(mem::take(mut_ref));
|
||||||
}
|
}
|
||||||
|
|
||||||
use helix_lsp::{lsp, Client, LanguageServerName, OffsetEncoding};
|
use helix_lsp::{lsp, Client, LanguageServerName};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
impl Document {
|
impl Document {
|
||||||
|
@ -732,21 +732,19 @@ impl Document {
|
||||||
|
|
||||||
let text = self.text.clone();
|
let text = self.text.clone();
|
||||||
// finds first language server that supports formatting and then formats
|
// finds first language server that supports formatting and then formats
|
||||||
let (offset_encoding, request) = self
|
let language_server = self
|
||||||
.language_servers_with_feature(LanguageServerFeature::Format)
|
.language_servers_with_feature(LanguageServerFeature::Format)
|
||||||
.find_map(|language_server| {
|
.next()?;
|
||||||
let offset_encoding = language_server.offset_encoding();
|
let offset_encoding = language_server.offset_encoding();
|
||||||
let request = language_server.text_document_formatting(
|
let request = language_server.text_document_formatting(
|
||||||
self.identifier(),
|
self.identifier(),
|
||||||
lsp::FormattingOptions {
|
lsp::FormattingOptions {
|
||||||
tab_size: self.tab_width() as u32,
|
tab_size: self.tab_width() as u32,
|
||||||
insert_spaces: matches!(self.indent_style, IndentStyle::Spaces(_)),
|
insert_spaces: matches!(self.indent_style, IndentStyle::Spaces(_)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
Some((offset_encoding, request))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let fut = async move {
|
let fut = async move {
|
||||||
let edits = request.await.unwrap_or_else(|e| {
|
let edits = request.await.unwrap_or_else(|e| {
|
||||||
|
@ -1445,7 +1443,6 @@ impl Document {
|
||||||
self.language_servers.remove(name)
|
self.language_servers.remove(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO filter also based on LSP capabilities?
|
|
||||||
pub fn language_servers_with_feature(
|
pub fn language_servers_with_feature(
|
||||||
&self,
|
&self,
|
||||||
feature: LanguageServerFeature,
|
feature: LanguageServerFeature,
|
||||||
|
@ -1453,7 +1450,10 @@ impl Document {
|
||||||
self.language_config().into_iter().flat_map(move |config| {
|
self.language_config().into_iter().flat_map(move |config| {
|
||||||
config.language_servers.iter().filter_map(move |features| {
|
config.language_servers.iter().filter_map(move |features| {
|
||||||
let ls = &**self.language_servers.get(&features.name)?;
|
let ls = &**self.language_servers.get(&features.name)?;
|
||||||
if ls.is_initialized() && features.has_feature(feature) {
|
if ls.is_initialized()
|
||||||
|
&& ls.supports_feature(feature)
|
||||||
|
&& features.has_feature(feature)
|
||||||
|
{
|
||||||
Some(ls)
|
Some(ls)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -1466,23 +1466,6 @@ impl Document {
|
||||||
self.language_servers().any(|l| l.id() == id)
|
self.language_servers().any(|l| l.id() == id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_on_first_supported_language_server<T, P>(
|
|
||||||
&self,
|
|
||||||
view_id: ViewId,
|
|
||||||
feature: LanguageServerFeature,
|
|
||||||
request_provider: P,
|
|
||||||
) -> Option<T>
|
|
||||||
where
|
|
||||||
P: Fn(&Client, OffsetEncoding, lsp::Position, lsp::TextDocumentIdentifier) -> Option<T>,
|
|
||||||
{
|
|
||||||
self.language_servers_with_feature(feature)
|
|
||||||
.find_map(|language_server| {
|
|
||||||
let offset_encoding = language_server.offset_encoding();
|
|
||||||
let pos = self.position(view_id, offset_encoding);
|
|
||||||
request_provider(language_server, offset_encoding, pos, self.identifier())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn diff_handle(&self) -> Option<&DiffHandle> {
|
pub fn diff_handle(&self) -> Option<&DiffHandle> {
|
||||||
self.diff_handle.as_ref()
|
self.diff_handle.as_ref()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue