aboutsummaryrefslogtreecommitdiff
path: root/helix-term
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term')
-rw-r--r--helix-term/src/application.rs126
-rw-r--r--helix-term/src/commands.rs273
-rw-r--r--helix-term/src/commands/lsp.rs799
-rw-r--r--helix-term/src/commands/typed.rs106
-rw-r--r--helix-term/src/health.rs27
-rw-r--r--helix-term/src/ui/completion.rs85
-rw-r--r--helix-term/src/ui/editor.rs17
-rw-r--r--helix-term/src/ui/mod.rs21
-rw-r--r--helix-term/src/ui/statusline.rs12
9 files changed, 808 insertions, 658 deletions
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index b54d6835..45f99e48 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -30,6 +30,7 @@ use crate::{
use log::{debug, error, warn};
use std::{
+ collections::btree_map::Entry,
io::{stdin, stdout},
path::Path,
sync::Arc,
@@ -564,7 +565,7 @@ impl Application {
let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
let id = doc.id();
doc.detect_language(loader);
- let _ = self.editor.refresh_language_server(id);
+ self.editor.refresh_language_servers(id);
}
// TODO: fix being overwritten by lsp
@@ -662,6 +663,18 @@ impl Application {
) {
use helix_lsp::{Call, MethodCall, Notification};
+ macro_rules! language_server {
+ () => {
+ match self.editor.language_servers.get_by_id(server_id) {
+ Some(language_server) => language_server,
+ None => {
+ warn!("can't find language server with id `{}`", server_id);
+ return;
+ }
+ }
+ };
+ }
+
match call {
Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
let notification = match Notification::parse(&method, params) {
@@ -677,14 +690,7 @@ impl Application {
match notification {
Notification::Initialized => {
- let language_server =
- match self.editor.language_servers.get_by_id(server_id) {
- Some(language_server) => language_server,
- None => {
- warn!("can't find language server with id `{}`", server_id);
- return;
- }
- };
+ let language_server = language_server!();
// Trigger a workspace/didChangeConfiguration notification after initialization.
// This might not be required by the spec but Neovim does this as well, so it's
@@ -694,7 +700,7 @@ impl Application {
}
let docs = self.editor.documents().filter(|doc| {
- doc.language_server().map(|server| server.id()) == Some(server_id)
+ doc.language_servers().iter().any(|l| l.id() == server_id)
});
// trigger textDocument/didOpen for docs that are already open
@@ -723,6 +729,7 @@ impl Application {
return;
}
};
+ let offset_encoding = language_server!().offset_encoding();
let doc = self.editor.document_by_path_mut(&path).filter(|doc| {
if let Some(version) = params.version {
if version != doc.version() {
@@ -745,18 +752,11 @@ impl Application {
use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
use lsp::DiagnosticSeverity;
- let language_server = if let Some(language_server) = doc.language_server() {
- language_server
- } else {
- log::warn!("Discarding diagnostic because language server is not initialized: {:?}", diagnostic);
- return None;
- };
-
// TODO: convert inside server
let start = if let Some(start) = lsp_pos_to_pos(
text,
diagnostic.range.start,
- language_server.offset_encoding(),
+ offset_encoding,
) {
start
} else {
@@ -764,11 +764,9 @@ impl Application {
return None;
};
- let end = if let Some(end) = lsp_pos_to_pos(
- text,
- diagnostic.range.end,
- language_server.offset_encoding(),
- ) {
+ let end = if let Some(end) =
+ lsp_pos_to_pos(text, diagnostic.range.end, offset_encoding)
+ {
end
} else {
log::warn!("lsp position out of bounds - {:?}", diagnostic);
@@ -807,14 +805,19 @@ impl Application {
None => None,
};
- let tags = if let Some(ref tags) = diagnostic.tags {
- let new_tags = tags.iter().filter_map(|tag| {
- match *tag {
- lsp::DiagnosticTag::DEPRECATED => Some(DiagnosticTag::Deprecated),
- lsp::DiagnosticTag::UNNECESSARY => Some(DiagnosticTag::Unnecessary),
- _ => None
- }
- }).collect();
+ let tags = if let Some(tags) = &diagnostic.tags {
+ let new_tags = tags
+ .iter()
+ .filter_map(|tag| match *tag {
+ lsp::DiagnosticTag::DEPRECATED => {
+ Some(DiagnosticTag::Deprecated)
+ }
+ lsp::DiagnosticTag::UNNECESSARY => {
+ Some(DiagnosticTag::Unnecessary)
+ }
+ _ => None,
+ })
+ .collect();
new_tags
} else {
@@ -830,11 +833,12 @@ impl Application {
tags,
source: diagnostic.source.clone(),
data: diagnostic.data.clone(),
+ language_server_id: server_id,
})
})
.collect();
- doc.set_diagnostics(diagnostics);
+ doc.replace_diagnostics(diagnostics, server_id);
}
// Sort diagnostics first by severity and then by line numbers.
@@ -842,13 +846,26 @@ impl Application {
params
.diagnostics
.sort_unstable_by_key(|d| (d.severity, d.range.start));
+ let diagnostics = params
+ .diagnostics
+ .into_iter()
+ .map(|d| (d, server_id, offset_encoding))
+ .collect();
// Insert the original lsp::Diagnostics here because we may have no open document
// for diagnosic message and so we can't calculate the exact position.
// When using them later in the diagnostics picker, we calculate them on-demand.
- self.editor
- .diagnostics
- .insert(params.uri, params.diagnostics);
+ match self.editor.diagnostics.entry(params.uri) {
+ Entry::Occupied(o) => {
+ let current_diagnostics = o.into_mut();
+ // there may entries of other language servers, which is why we can't overwrite the whole entry
+ current_diagnostics.retain(|(_, lsp_id, _)| *lsp_id != server_id);
+ current_diagnostics.extend(diagnostics);
+ }
+ Entry::Vacant(v) => {
+ v.insert(diagnostics);
+ }
+ };
}
Notification::ShowMessage(params) => {
log::warn!("unhandled window/showMessage: {:?}", params);
@@ -950,10 +967,12 @@ impl Application {
.editor
.documents_mut()
.filter_map(|doc| {
- if doc.language_server().map(|server| server.id())
- == Some(server_id)
+ if doc
+ .language_servers()
+ .iter()
+ .any(|server| server.id() == server_id)
{
- doc.set_diagnostics(Vec::new());
+ doc.clear_diagnostics(server_id);
doc.url()
} else {
None
@@ -1029,28 +1048,15 @@ impl Application {
}))
}
Ok(MethodCall::WorkspaceFolders) => {
- let language_server =
- self.editor.language_servers.get_by_id(server_id).unwrap();
-
- Ok(json!(&*language_server.workspace_folders().await))
+ Ok(json!(&*language_server!().workspace_folders().await))
}
Ok(MethodCall::WorkspaceConfiguration(params)) => {
+ let language_server = language_server!();
let result: Vec<_> = params
.items
.iter()
- .map(|item| {
- let mut config = match &item.scope_uri {
- Some(scope) => {
- let path = scope.to_file_path().ok()?;
- let doc = self.editor.document_by_path(path)?;
- doc.language_config()?.config.as_ref()?
- }
- None => self
- .editor
- .language_servers
- .get_by_id(server_id)?
- .config()?,
- };
+ .filter_map(|item| {
+ let mut config = language_server.config()?;
if let Some(section) = item.section.as_ref() {
for part in section.split('.') {
config = config.get(part)?;
@@ -1074,15 +1080,7 @@ impl Application {
}
};
- let language_server = match self.editor.language_servers.get_by_id(server_id) {
- Some(language_server) => language_server,
- None => {
- warn!("can't find language server with id `{}`", server_id);
- return;
- }
- };
-
- tokio::spawn(language_server.reply(id, reply));
+ tokio::spawn(language_server!().reply(id, reply));
}
Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id),
}
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 5a844e35..c7d28e19 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -23,6 +23,7 @@ use helix_core::{
regex::{self, Regex, RegexBuilder},
search::{self, CharMatcher},
selection, shellwords, surround,
+ syntax::LanguageServerFeature,
text_annotations::TextAnnotations,
textobject,
tree_sitter::Node,
@@ -54,13 +55,13 @@ use crate::{
job::Callback,
keymap::ReverseKeymap,
ui::{
- self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, FilePicker, Picker,
- Popup, Prompt, PromptEvent,
+ self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem,
+ FilePicker, Picker, Popup, Prompt, PromptEvent,
},
};
use crate::job::{self, Jobs};
-use futures_util::StreamExt;
+use futures_util::{stream::FuturesUnordered, StreamExt, TryStreamExt};
use std::{collections::HashMap, fmt, future::Future};
use std::{collections::HashSet, num::NonZeroUsize};
@@ -3029,7 +3030,7 @@ fn exit_select_mode(cx: &mut Context) {
fn goto_first_diag(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- let selection = match doc.diagnostics().first() {
+ let selection = match doc.shown_diagnostics().next() {
Some(diag) => Selection::single(diag.range.start, diag.range.end),
None => return,
};
@@ -3038,7 +3039,7 @@ fn goto_first_diag(cx: &mut Context) {
fn goto_last_diag(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- let selection = match doc.diagnostics().last() {
+ let selection = match doc.shown_diagnostics().last() {
Some(diag) => Selection::single(diag.range.start, diag.range.end),
None => return,
};
@@ -3054,10 +3055,9 @@ fn goto_next_diag(cx: &mut Context) {
.cursor(doc.text().slice(..));
let diag = doc
- .diagnostics()
- .iter()
+ .shown_diagnostics()
.find(|diag| diag.range.start > cursor_pos)
- .or_else(|| doc.diagnostics().first());
+ .or_else(|| doc.shown_diagnostics().next());
let selection = match diag {
Some(diag) => Selection::single(diag.range.start, diag.range.end),
@@ -3075,11 +3075,12 @@ fn goto_prev_diag(cx: &mut Context) {
.cursor(doc.text().slice(..));
let diag = doc
- .diagnostics()
- .iter()
+ .shown_diagnostics()
+ .collect::<Vec<_>>()
+ .into_iter()
.rev()
.find(|diag| diag.range.start < cursor_pos)
- .or_else(|| doc.diagnostics().last());
+ .or_else(|| doc.shown_diagnostics().last());
let selection = match diag {
// NOTE: the selection is reversed because we're jumping to the
@@ -3234,60 +3235,72 @@ pub mod insert {
use helix_lsp::lsp;
// if ch matches completion char, trigger completion
let doc = doc_mut!(cx.editor);
- let language_server = match doc.language_server() {
- Some(language_server) => language_server,
- None => return,
- };
+ let trigger_completion = doc
+ .language_servers_with_feature(LanguageServerFeature::Completion)
+ .iter()
+ .any(|ls| {
+ let capabilities = ls.capabilities();
- let capabilities = language_server.capabilities();
+ // TODO: what if trigger is multiple chars long
+ matches!(&capabilities.completion_provider, Some(lsp::CompletionOptions {
+ trigger_characters: Some(triggers),
+ ..
+ }) if triggers.iter().any(|trigger| trigger.contains(ch)))
+ });
- if let Some(lsp::CompletionOptions {
- trigger_characters: Some(triggers),
- ..
- }) = &capabilities.completion_provider
- {
- // TODO: what if trigger is multiple chars long
- if triggers.iter().any(|trigger| trigger.contains(ch)) {
- cx.editor.clear_idle_timer();
- super::completion(cx);
- }
+ if trigger_completion {
+ cx.editor.clear_idle_timer();
+ super::completion(cx);
}
}
fn signature_help(cx: &mut Context, ch: char) {
+ use futures_util::FutureExt;
use helix_lsp::lsp;
// if ch matches signature_help char, trigger
- let doc = doc_mut!(cx.editor);
- // The language_server!() macro is not used here since it will
- // print an "LSP not active for current buffer" message on
- // every keypress.
- let language_server = match doc.language_server() {
- Some(language_server) => language_server,
- None => return,
- };
-
- let capabilities = language_server.capabilities();
+ let (view, doc) = current!(cx.editor);
+ // 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 = &[')', ';', '.'];
+ // TODO support multiple language servers (not just the first that is found)
+ let future = doc
+ .language_servers_with_feature(LanguageServerFeature::SignatureHelp)
+ .iter()
+ .find_map(|ls| {
+ let capabilities = ls.capabilities();
+
+ match 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 lsp::ServerCapabilities {
- signature_help_provider:
- Some(lsp::SignatureHelpOptions {
- trigger_characters: Some(triggers),
- // 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);
- }
+ if let Some(future) = future {
+ super::signature_help_impl_with_future(
+ cx,
+ future.boxed(),
+ SignatureHelpInvoked::Automatic,
+ )
}
}
@@ -3301,7 +3314,7 @@ pub mod insert {
Some(transaction)
}
- use helix_core::auto_pairs;
+ use helix_core::{auto_pairs, syntax::LanguageServerFeature};
pub fn insert_char(cx: &mut Context, c: char) {
let (view, doc) = current_ref!(cx.editor);
@@ -4046,55 +4059,55 @@ fn format_selections(cx: &mut Context) {
use helix_lsp::{lsp, util::range_to_lsp_range};
let (view, doc) = current!(cx.editor);
+ let view_id = view.id;
// via lsp if available
// TODO: else via tree-sitter indentation calculations
- let language_server = match doc.language_server() {
- Some(language_server) => language_server,
- None => return,
- };
-
- let ranges: Vec<lsp::Range> = doc
- .selection(view.id)
- .iter()
- .map(|range| range_to_lsp_range(doc.text(), *range, language_server.offset_encoding()))
- .collect();
-
- if ranges.len() != 1 {
+ if doc.selection(view_id).len() != 1 {
cx.editor
.set_error("format_selections only supports a single selection for now");
return;
}
- // TODO: handle fails
- // TODO: concurrent map over all ranges
-
- let range = ranges[0];
-
- let request = match language_server.text_document_range_formatting(
- doc.identifier(),
- range,
- lsp::FormattingOptions::default(),
- None,
- ) {
- Some(future) => future,
+ let (future, offset_encoding) = match doc
+ .language_servers_with_feature(LanguageServerFeature::Format)
+ .iter()
+ .find_map(|language_server| {
+ 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,
+ )?;
+ Some((future, offset_encoding))
+ }) {
+ Some(future_offset_encoding) => future_offset_encoding,
None => {
cx.editor
- .set_error("Language server does not support range formatting");
+ .set_error("No language server supports range formatting");
return;
}
};
- let edits = tokio::task::block_in_place(|| helix_lsp::block_on(request)).unwrap_or_default();
+ let edits = tokio::task::block_in_place(|| helix_lsp::block_on(future)).unwrap_or_default();
- let transaction = helix_lsp::util::generate_transaction_from_edits(
- doc.text(),
- edits,
- language_server.offset_encoding(),
- );
+ let transaction =
+ helix_lsp::util::generate_transaction_from_edits(doc.text(), edits, offset_encoding);
- doc.apply(&transaction, view.id);
+ doc.apply(&transaction, view_id);
}
fn join_selections_impl(cx: &mut Context, select_space: bool) {
@@ -4231,21 +4244,45 @@ pub fn completion(cx: &mut Context) {
doc.savepoint(view)
};
- let language_server = match doc.language_server() {
- Some(language_server) => language_server,
- None => return,
- };
-
- let offset_encoding = language_server.offset_encoding();
let text = savepoint.text.clone();
let cursor = savepoint.cursor();
- let pos = pos_to_lsp_pos(&text, cursor, offset_encoding);
+ let mut futures: FuturesUnordered<_> = doc
+ .language_servers_with_feature(LanguageServerFeature::Completion)
+ .iter()
+ // TODO this should probably already been filtered in something like "language_servers_with_feature"
+ .filter_map(|language_server| {
+ let language_server_id = language_server.id();
+ let offset_encoding = language_server.offset_encoding();
+ let pos = pos_to_lsp_pos(doc.text(), cursor, helix_lsp::OffsetEncoding::Utf8);
+ let completion_request = language_server.completion(doc.identifier(), pos, None)?;
+
+ Some(async move {
+ let json = completion_request.await?;
+ let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?;
+
+ let items = match response {
+ Some(lsp::CompletionResponse::Array(items)) => items,
+ // TODO: do something with is_incomplete
+ Some(lsp::CompletionResponse::List(lsp::CompletionList {
+ is_incomplete: _is_incomplete,
+ items,
+ })) => items,
+ None => Vec::new(),
+ }
+ .into_iter()
+ .map(|item| CompletionItem {
+ item,
+ language_server_id,
+ offset_encoding,
+ resolved: false,
+ })
+ .collect();
- let future = match language_server.completion(doc.identifier(), pos, None) {
- Some(future) => future,
- None => return,
- };
+ anyhow::Ok(items)
+ })
+ })
+ .collect();
// setup a channel that allows the request to be canceled
let (tx, rx) = oneshot::channel();
@@ -4254,12 +4291,20 @@ pub fn completion(cx: &mut Context) {
// and the associated request is automatically dropped
cx.editor.completion_request_handle = Some(tx);
let future = async move {
+ let items_future = async move {
+ let mut items = Vec::new();
+ // TODO if one completion request errors, all other completion requests are discarded (even if they're valid)
+ while let Some(mut lsp_items) = futures.try_next().await? {
+ items.append(&mut lsp_items);
+ }
+ anyhow::Ok(items)
+ };
tokio::select! {
biased;
_ = rx => {
- Ok(serde_json::Value::Null)
+ Ok(Vec::new())
}
- res = future => {
+ res = items_future => {
res
}
}
@@ -4293,9 +4338,9 @@ pub fn completion(cx: &mut Context) {
},
));
- cx.callback(
- future,
- move |editor, compositor, response: Option<lsp::CompletionResponse>| {
+ cx.jobs.callback(async move {
+ let items = future.await?;
+ let call = move |editor: &mut Editor, compositor: &mut Compositor| {
let (view, doc) = current_ref!(editor);
// check if the completion request is stale.
//
@@ -4306,16 +4351,6 @@ pub fn completion(cx: &mut Context) {
return;
}
- let items = match response {
- Some(lsp::CompletionResponse::Array(items)) => items,
- // TODO: do something with is_incomplete
- Some(lsp::CompletionResponse::List(lsp::CompletionList {
- is_incomplete: _is_incomplete,
- items,
- })) => items,
- None => Vec::new(),
- };
-
if items.is_empty() {
// editor.set_error("No completion available");
return;
@@ -4326,7 +4361,6 @@ pub fn completion(cx: &mut Context) {
editor,
savepoint,
items,
- offset_encoding,
start_offset,
trigger_offset,
size,
@@ -4340,8 +4374,9 @@ pub fn completion(cx: &mut Context) {
{
compositor.remove(SignatureHelp::ID);
}
- },
- );
+ };
+ Ok(Callback::EditorCompositor(Box::new(call)))
+ });
}
// comments
@@ -5141,7 +5176,7 @@ async fn shell_impl_async(
helix_view::document::to_writer(&mut stdin, (encoding::UTF_8, false), &input)
.await?;
}
- Ok::<_, anyhow::Error>(())
+ anyhow::Ok(())
});
let (output, _) = tokio::join! {
process.wait_with_output(),
diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs
index 0ad6fb7e..efef1211 100644
--- a/helix-term/src/commands/lsp.rs
+++ b/helix-term/src/commands/lsp.rs
@@ -1,4 +1,4 @@
-use futures_util::FutureExt;
+use futures_util::{future::BoxFuture, stream::FuturesUnordered, FutureExt};
use helix_lsp::{
block_on,
lsp::{
@@ -8,6 +8,8 @@ use helix_lsp::{
util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
OffsetEncoding,
};
+use serde_json::Value;
+use tokio_stream::StreamExt;
use tui::{
text::{Span, Spans},
widgets::Row,
@@ -15,7 +17,9 @@ use tui::{
use super::{align_view, push_jump, Align, Context, Editor, Open};
-use helix_core::{path, text_annotations::InlineAnnotation, Selection};
+use helix_core::{
+ path, syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection,
+};
use helix_view::{
document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
editor::Action,
@@ -25,6 +29,7 @@ use helix_view::{
use crate::{
compositor::{self, Compositor},
+ job::Callback,
ui::{
self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, FilePicker,
Popup, PromptEvent,
@@ -35,24 +40,6 @@ use std::{
cmp::Ordering, collections::BTreeMap, fmt::Write, future::Future, path::PathBuf, sync::Arc,
};
-/// Gets the language server that is attached to a document, and
-/// if it's not active 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 "LSP inactive" status message confusingly.
-#[macro_export]
-macro_rules! language_server {
- ($editor:expr, $doc:expr) => {
- match $doc.language_server() {
- Some(language_server) => language_server,
- None => {
- $editor.set_status("Language server not active for current buffer");
- return;
- }
- }
- };
-}
-
impl ui::menu::Item for lsp::Location {
/// Current working directory.
type Data = PathBuf;
@@ -87,20 +74,30 @@ impl ui::menu::Item for lsp::Location {
}
}
-impl ui::menu::Item for lsp::SymbolInformation {
+struct SymbolInformationItem {
+ symbol: lsp::SymbolInformation,
+ offset_encoding: OffsetEncoding,
+}
+
+impl ui::menu::Item for SymbolInformationItem {
/// Path to currently focussed document
type Data = Option<lsp::Url>;
fn format(&self, current_doc_path: &Self::Data) -> Row {
- if current_doc_path.as_ref() == Some(&self.location.uri) {
- self.name.as_str().into()
+ if current_doc_path.as_ref() == Some(&self.symbol.location.uri) {
+ self.symbol.name.as_str().into()
} else {
- match self.location.uri.to_file_path() {
+ match self.symbol.location.uri.to_file_path() {
Ok(path) => {
let get_relative_path = path::get_relative_path(path.as_path());
- format!("{} ({})", &self.name, get_relative_path.to_string_lossy()).into()
+ format!(
+ "{} ({})",
+ &self.symbol.name,
+ get_relative_path.to_string_lossy()
+ )
+ .into()
}
- Err(_) => format!("{} ({})", &self.name, &self.location.uri).into(),
+ Err(_) => format!("{} ({})", &self.symbol.name, &self.symbol.location.uri).into(),
}
}
}
@@ -116,6 +113,7 @@ struct DiagnosticStyles {
struct PickerDiagnostic {
url: lsp::Url,
diag: lsp::Diagnostic,
+ offset_encoding: OffsetEncoding,
}
impl ui::menu::Item for PickerDiagnostic {
@@ -211,21 +209,19 @@ fn jump_to_location(
align_view(doc, view, Align::Center);
}
-fn sym_picker(
- symbols: Vec<lsp::SymbolInformation>,
- current_path: Option<lsp::Url>,
- offset_encoding: OffsetEncoding,
-) -> FilePicker<lsp::SymbolInformation> {
+type SymbolPicker = FilePicker<SymbolInformationItem>;
+
+fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
// TODO: drop current_path comparison and instead use workspace: bool flag?
FilePicker::new(
symbols,
current_path.clone(),
- move |cx, symbol, action| {
+ move |cx, item, action| {
let (view, doc) = current!(cx.editor);
push_jump(view, doc);
- if current_path.as_ref() != Some(&symbol.location.uri) {
- let uri = &symbol.location.uri;
+ if current_path.as_ref() != Some(&item.symbol.location.uri) {
+ let uri = &item.symbol.location.uri;
let path = match uri.to_file_path() {
Ok(path) => path,
Err(_) => {
@@ -245,7 +241,7 @@ fn sym_picker(
let (view, doc) = current!(cx.editor);
if let Some(range) =
- lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding)
+ lsp_range_to_range(doc.text(), item.symbol.location.range, item.offset_encoding)
{
// we flip the range so that the cursor sits on the start of the symbol
// (for example start of the function).
@@ -253,7 +249,7 @@ fn sym_picker(
align_view(doc, view, Align::Center);
}
},
- move |_editor, symbol| Some(location_to_file_location(&symbol.location)),
+ move |_editor, item| Some(location_to_file_location(&item.symbol.location)),
)
.truncate_start(false)
}
@@ -266,10 +262,9 @@ enum DiagnosticsFormat {
fn diag_picker(
cx: &Context,
- diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
+ diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize, OffsetEncoding)>>,
current_path: Option<lsp::Url>,
format: DiagnosticsFormat,
- offset_encoding: OffsetEncoding,
) -> FilePicker<PickerDiagnostic> {
// TODO: drop current_path comparison and instead use workspace: bool flag?
@@ -277,10 +272,11 @@ fn diag_picker(
let mut flat_diag = Vec::new();
for (url, diags) in diagnostics {
flat_diag.reserve(diags.len());
- for diag in diags {
+ for (diag, _, offset_encoding) in diags {
flat_diag.push(PickerDiagnostic {
url: url.clone(),
diag,
+ offset_encoding,
});
}
}
@@ -295,7 +291,13 @@ fn diag_picker(
FilePicker::new(
flat_diag,
(styles, format),
- move |cx, PickerDiagnostic { url, diag }, action| {
+ move |cx,
+ PickerDiagnostic {
+ url,
+ diag,
+ offset_encoding,
+ },
+ action| {
if current_path.as_ref() == Some(url) {
let (view, doc) = current!(cx.editor);
push_jump(view, doc);
@@ -306,14 +308,14 @@ fn diag_picker(
let (view, doc) = current!(cx.editor);
- if let Some(range) = lsp_range_to_range(doc.text(), diag.range, offset_encoding) {
+ if let Some(range) = lsp_range_to_range(doc.text(), diag.range, *offset_encoding) {
// 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(range.head, range.anchor));
align_view(doc, view, Align::Center);
}
},
- move |_editor, PickerDiagnostic { url, diag }| {
+ move |_editor, PickerDiagnostic { url, diag, .. }| {
let location = lsp::Location::new(url.clone(), diag.range);
Some(location_to_file_location(&location))
},
@@ -323,126 +325,149 @@ fn diag_picker(
pub fn symbol_picker(cx: &mut Context) {
fn nested_to_flat(
- list: &mut Vec<lsp::SymbolInformation>,
+ list: &mut Vec<SymbolInformationItem>,
file: &lsp::TextDocumentIdentifier,
symbol: lsp::DocumentSymbol,
+ offset_encoding: OffsetEncoding,
) {
#[allow(deprecated)]
- list.push(lsp::SymbolInformation {
- name: symbol.name,
- kind: symbol.kind,
- tags: symbol.tags,
- deprecated: symbol.deprecated,
- location: lsp::Location::new(file.uri.clone(), symbol.selection_range),
- container_name: None,
+ list.push(SymbolInformationItem {
+ symbol: lsp::SymbolInformation {
+ name: symbol.name,
+ kind: symbol.kind,
+ tags: symbol.tags,
+ deprecated: symbol.deprecated,
+ location: lsp::Location::new(file.uri.clone(), symbol.selection_range),
+ container_name: None,
+ },
+ offset_encoding,
});
for child in symbol.children.into_iter().flatten() {
- nested_to_flat(list, file, child);
+ nested_to_flat(list, file, child, offset_encoding);
}
}
let doc = doc!(cx.editor);
- let language_server = language_server!(cx.editor, doc);
+ let mut futures: FuturesUnordered<_> = doc
+ .language_servers_with_feature(LanguageServerFeature::DocumentSymbols)
+ .iter()
+ .filter_map(|ls| {
+ let request = ls.document_symbols(doc.identifier())?;
+ Some((request, ls.offset_encoding(), doc.identifier()))
+ })
+ .map(|(request, offset_encoding, doc_id)| async move {
+ let json = request.await?;
+ let response: Option<lsp::DocumentSymbolResponse> = serde_json::from_value(json)?;
+ let symbols = match response {
+ 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
+ let symbols = match symbols {
+ lsp::DocumentSymbolResponse::Flat(symbols) => symbols
+ .into_iter()
+ .map(|symbol| SymbolInformationItem {
+ symbol,
+ offset_encoding,
+ })
+ .collect(),
+ lsp::DocumentSymbolResponse::Nested(symbols) => {
+ let mut flat_symbols = Vec::new();
+ for symbol in symbols {
+ nested_to_flat(&mut flat_symbols, &doc_id, symbol, offset_encoding)
+ }
+ flat_symbols
+ }
+ };
+ Ok(symbols)
+ })
+ .collect();
let current_url = doc.url();
- let offset_encoding = language_server.offset_encoding();
- let future = match language_server.document_symbols(doc.identifier()) {
- Some(future) => future,
- None => {
- cx.editor
- .set_error("Language server does not support document symbols");
- return;
- }
- };
-
- cx.callback(
- future,
- move |editor, compositor, response: Option<lsp::DocumentSymbolResponse>| {
- if let Some(symbols) = response {
- // lsp has two ways to represent symbols (flat/nested)
- // convert the nested variant to flat, so that we have a homogeneous list
- let symbols = match symbols {
- lsp::DocumentSymbolResponse::Flat(symbols) => symbols,
- lsp::DocumentSymbolResponse::Nested(symbols) => {
- let doc = doc!(editor);
- let mut flat_symbols = Vec::new();
- for symbol in symbols {
- nested_to_flat(&mut flat_symbols, &doc.identifier(), symbol)
- }
- flat_symbols
- }
- };
+ if futures.is_empty() {
+ cx.editor
+ .set_error("No Language server does support document symbols");
+ return;
+ }
- let picker = sym_picker(symbols, current_url, offset_encoding);
- compositor.push(Box::new(overlaid(picker)))
+ cx.jobs.callback(async move {
+ let mut symbols = Vec::new();
+ // TODO if one symbol request errors, all other requests are discarded (even if they're valid)
+ while let Some(mut lsp_items) = futures.try_next().await? {
+ symbols.append(&mut lsp_items);
+ }
+ let call = move |editor: &mut Editor, compositor: &mut Compositor| {
+ if symbols.is_empty() {
+ editor.set_error("No symbols available");
+ return;
}
- },
- )
+ let picker = sym_picker(symbols, current_url);
+ compositor.push(Box::new(overlaid(picker)))
+ };
+
+ Ok(Callback::EditorCompositor(Box::new(call)))
+ });
}
pub fn workspace_symbol_picker(cx: &mut Context) {
let doc = doc!(cx.editor);
- let current_url = doc.url();
- let language_server = language_server!(cx.editor, doc);
- let offset_encoding = language_server.offset_encoding();
- let future = match language_server.workspace_symbols("".to_string()) {
- Some(future) => future,
- None => {
- cx.editor
- .set_error("Language server does not support workspace symbols");
- return;
+
+ let get_symbols = move |pattern: String, editor: &mut Editor| {
+ let doc = doc!(editor);
+ let mut futures: FuturesUnordered<_> = doc
+ .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
+ .iter()
+ .filter_map(|ls| Some((ls.workspace_symbols(pattern.clone())?, ls.offset_encoding())))
+ .map(|(request, offset_encoding)| async move {
+ let json = request.await?;
+
+ let response = serde_json::from_value::<Option<Vec<lsp::SymbolInformation>>>(json)?
+ .unwrap_or_default()
+ .into_iter()
+ .map(|symbol| SymbolInformationItem {
+ symbol,
+ offset_encoding,
+ })
+ .collect();
+
+ anyhow::Ok(response)
+ })
+ .collect();
+
+ if futures.is_empty() {
+ editor.set_error("No Language server does support workspace symbols");
}
- };
- cx.callback(
- future,
- move |_editor, compositor, response: Option<Vec<lsp::SymbolInformation>>| {
- let symbols = response.unwrap_or_default();
- let picker = sym_picker(symbols, current_url, offset_encoding);
- let get_symbols = |query: String, editor: &mut Editor| {
- let doc = doc!(editor);
- let language_server = match doc.language_server() {
- Some(s) => s,
- None => {
- // This should not generally happen since the picker will not
- // even open in the first place if there is no server.
- return async move { Err(anyhow::anyhow!("LSP not active")) }.boxed();
- }
- };
- let symbol_request = match language_server.workspace_symbols(query) {
- Some(future) => future,
- None => {
- // This should also not happen since the language server must have
- // supported workspace symbols before to reach this block.
- return async move {
- Err(anyhow::anyhow!(
- "Language server does not support workspace symbols"
- ))
- }
- .boxed();
- }
- };
+ async move {
+ let mut symbols = Vec::new();
+ // TODO if one symbol request errors, all other requests are discarded (even if they're valid)
+ while let Some(mut lsp_items) = futures.try_next().await? {
+ symbols.append(&mut lsp_items);
+ }
+ anyhow::Ok(symbols)
+ }
+ .boxed()
+ };
- let future = async move {
- let json = symbol_request.await?;
- let response: Option<Vec<lsp::SymbolInformation>> =
- serde_json::from_value(json)?;
+ let current_url = doc.url();
+ let initial_symbols = get_symbols("".to_owned(), cx.editor);
- Ok(response.unwrap_or_default())
- };
- future.boxed()
- };
+ cx.jobs.callback(async move {
+ let symbols = initial_symbols.await?;
+ let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
+ let picker = sym_picker(symbols, current_url);
let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols));
compositor.push(Box::new(overlaid(dyn_picker)))
- },
- )
+ };
+
+ Ok(Callback::EditorCompositor(Box::new(call)))
+ });
}
pub fn diagnostics_picker(cx: &mut Context) {
let doc = doc!(cx.editor);
- let language_server = language_server!(cx.editor, doc);
if let Some(current_url) = doc.url() {
- let offset_encoding = language_server.offset_encoding();
let diagnostics = cx
.editor
.diagnostics
@@ -454,7 +479,6 @@ pub fn diagnostics_picker(cx: &mut Context) {
[(current_url.clone(), diagnostics)].into(),
Some(current_url),
DiagnosticsFormat::HideSourcePath,
- offset_encoding,
);
cx.push_layer(Box::new(overlaid(picker)));
}
@@ -462,24 +486,28 @@ pub fn diagnostics_picker(cx: &mut Context) {
pub fn workspace_diagnostics_picker(cx: &mut Context) {
let doc = doc!(cx.editor);
- let language_server = language_server!(cx.editor, doc);
let current_url = doc.url();
- let offset_encoding = language_server.offset_encoding();
+ // TODO not yet filtered by LanguageServerFeature, need to do something similar as Document::shown_diagnostics here for all open documents
let diagnostics = cx.editor.diagnostics.clone();
let picker = diag_picker(
cx,
diagnostics,
current_url,
DiagnosticsFormat::ShowSourcePath,
- offset_encoding,
);
cx.push_layer(Box::new(overlaid(picker)));
}
-impl ui::menu::Item for lsp::CodeActionOrCommand {
+struct CodeActionOrCommandItem {
+ lsp_item: lsp::CodeActionOrCommand,
+ offset_encoding: OffsetEncoding,
+ language_server_id: usize,
+}
+
+impl ui::menu::Item for CodeActionOrCommandItem {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
- match self {
+ match &self.lsp_item {
lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(),
lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
}
@@ -546,45 +574,40 @@ fn action_fixes_diagnostics(action: &CodeActionOrCommand) -> bool {
pub fn code_action(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- let language_server = language_server!(cx.editor, doc);
-
let selection_range = doc.selection(view.id).primary();
- let offset_encoding = language_server.offset_encoding();
-
- let range = range_to_lsp_range(doc.text(), selection_range, offset_encoding);
- let future = match language_server.code_actions(
- doc.identifier(),
- range,
- // Filter and convert overlapping diagnostics
- lsp::CodeActionContext {
- diagnostics: doc
- .diagnostics()
- .iter()
- .filter(|&diag| {
- selection_range
- .overlaps(&helix_core::Range::new(diag.range.start, diag.range.end))
- })
- .map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding))
- .collect(),
- only: None,
- trigger_kind: Some(CodeActionTriggerKind::INVOKED),
- },
- ) {
- Some(future) => future,
- None => {
- cx.editor
- .set_error("Language server does not support code actions");
- return;
- }
- };
-
- cx.callback(
- future,
- move |editor, compositor, response: Option<lsp::CodeActionResponse>| {
+ let mut futures: FuturesUnordered<_> = doc
+ .language_servers_with_feature(LanguageServerFeature::CodeAction)
+ .iter()
+ // TODO this should probably already been filtered in something like "language_servers_with_feature"
+ .filter_map(|language_server| {
+ let offset_encoding = language_server.offset_encoding();
+ let language_server_id = language_server.id();
+ let range = range_to_lsp_range(doc.text(), selection_range, offset_encoding);
+ // Filter and convert overlapping diagnostics
+ let code_action_context = lsp::CodeActionContext {
+ diagnostics: doc
+ .diagnostics()
+ .iter()
+ .filter(|&diag| {
+ selection_range
+ .overlaps(&helix_core::Range::new(diag.range.start, diag.range.end))
+ })
+ .map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding))
+ .collect(),
+ only: None,
+ trigger_kind: Some(CodeActionTriggerKind::INVOKED),
+ };
+ let code_action_request =
+ language_server.code_actions(doc.identifier(), range, code_action_context)?;
+ Some((code_action_request, offset_encoding, language_server_id))
+ })
+ .map(|(request, offset_encoding, ls_id)| async move {
+ let json = request.await?;
+ let response: Option<lsp::CodeActionResponse> = serde_json::from_value(json)?;
let mut actions = match response {
Some(a) => a,
- None => return,
+ None => return anyhow::Ok(Vec::new()),
};
// remove disabled code actions
@@ -596,11 +619,6 @@ pub fn code_action(cx: &mut Context) {
)
});
- if actions.is_empty() {
- editor.set_status("No code actions available");
- return;
- }
-
// Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec.
// Many details are modeled after vscode because language servers are usually tested against it.
// VScode sorts the codeaction two times:
@@ -636,18 +654,48 @@ pub fn code_action(cx: &mut Context) {
.reverse()
});
- let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| {
+ Ok(actions
+ .into_iter()
+ .map(|lsp_item| CodeActionOrCommandItem {
+ lsp_item,
+ offset_encoding,
+ language_server_id: ls_id,
+ })
+ .collect())
+ })
+ .collect();
+
+ if futures.is_empty() {
+ cx.editor
+ .set_error("No Language server does support code actions");
+ return;
+ }
+
+ cx.jobs.callback(async move {
+ let mut actions = Vec::new();
+ // TODO if one code action request errors, all other requests are ignored (even if they're valid)
+ while let Some(mut lsp_items) = futures.try_next().await? {
+ actions.append(&mut lsp_items);
+ }
+
+ let call = move |editor: &mut Editor, compositor: &mut Compositor| {
+ if actions.is_empty() {
+ editor.set_error("No code actions available");
+ return;
+ }
+ let mut picker = ui::Menu::new(actions, (), move |editor, action, event| {
if event != PromptEvent::Validate {
return;
}
// always present here
- let code_action = code_action.unwrap();
+ let action = action.unwrap();
+ let offset_encoding = action.offset_encoding;
- match code_action {
+ match &action.lsp_item {
lsp::CodeActionOrCommand::Command(command) => {
log::debug!("code action command: {:?}", command);
- execute_lsp_command(editor, command.clone());
+ execute_lsp_command(editor, action.language_server_id, command.clone());
}
lsp::CodeActionOrCommand::CodeAction(code_action) => {
log::debug!("code action: {:?}", code_action);
@@ -659,7 +707,7 @@ pub fn code_action(cx: &mut Context) {
// if code action provides both edit and command first the edit
// should be applied and then the command
if let Some(command) = &code_action.command {
- execute_lsp_command(editor, command.clone());
+ execute_lsp_command(editor, action.language_server_id, command.clone());
}
}
}
@@ -668,8 +716,10 @@ pub fn code_action(cx: &mut Context) {
let popup = Popup::new("code-action", picker).with_scrollbar(false);
compositor.replace_or_push("code-action", popup);
- },
- )
+ };
+
+ Ok(Callback::EditorCompositor(Box::new(call)))
+ });
}
impl ui::menu::Item for lsp::Command {
@@ -679,13 +729,14 @@ impl ui::menu::Item for lsp::Command {
}
}
-pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
- let doc = doc!(editor);
- let language_server = language_server!(editor, doc);
-
+pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd: lsp::Command) {
// the command is executed on the server and communicated back
// to the client asynchronously using workspace edits
- let future = match language_server.command(cmd) {
+ let future = match editor
+ .language_servers
+ .get_by_id(language_server_id)
+ .and_then(|language_server| language_server.command(cmd))
+ {
Some(future) => future,
None => {
editor.set_error("Language server does not support executing commands");
@@ -977,18 +1028,22 @@ fn to_locations(definitions: Option<lsp::GotoDefinitionResponse>) -> Vec<lsp::Lo
}
}
+// TODO find a way to reduce boilerplate of all the goto functions, without unnecessary complexity...
pub fn goto_declaration(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- let language_server = language_server!(cx.editor, doc);
- let offset_encoding = language_server.offset_encoding();
-
- let pos = doc.position(view.id, offset_encoding);
-
- let future = match language_server.goto_declaration(doc.identifier(), pos, None) {
- Some(future) => future,
+ let (future, offset_encoding) = match doc
+ .language_servers_with_feature(LanguageServerFeature::GotoDeclaration)
+ .iter()
+ .find_map(|language_server| {
+ let offset_encoding = language_server.offset_encoding();
+ let pos = doc.position(view.id, offset_encoding);
+ let future = language_server.goto_declaration(doc.identifier(), pos, None)?;
+ Some((future, offset_encoding))
+ }) {
+ Some(future_offset_encoding) => future_offset_encoding,
None => {
cx.editor
- .set_error("Language server does not support goto-declaration");
+ .set_error("No language server supports goto-declaration");
return;
}
};
@@ -1004,16 +1059,19 @@ pub fn goto_declaration(cx: &mut Context) {
pub fn goto_definition(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- let language_server = language_server!(cx.editor, doc);
- let offset_encoding = language_server.offset_encoding();
-
- let pos = doc.position(view.id, offset_encoding);
-
- let future = match language_server.goto_definition(doc.identifier(), pos, None) {
- Some(future) => future,
+ let (future, offset_encoding) = match doc
+ .language_servers_with_feature(LanguageServerFeature::GotoDefinition)
+ .iter()
+ .find_map(|language_server| {
+ let offset_encoding = language_server.offset_encoding();
+ let pos = doc.position(view.id, offset_encoding);
+ let future = language_server.goto_definition(doc.identifier(), pos, None)?;
+ Some((future, offset_encoding))
+ }) {
+ Some(future_offset_encoding) => future_offset_encoding,
None => {
cx.editor
- .set_error("Language server does not support goto-definition");
+ .set_error("No language server supports goto-definition");
return;
}
};
@@ -1029,16 +1087,19 @@ pub fn goto_definition(cx: &mut Context) {
pub fn goto_type_definition(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- let language_server = language_server!(cx.editor, doc);
- let offset_encoding = language_server.offset_encoding();
-
- let pos = doc.position(view.id, offset_encoding);
-
- let future = match language_server.goto_type_definition(doc.identifier(), pos, None) {
- Some(future) => future,
+ let (future, offset_encoding) = match doc
+ .language_servers_with_feature(LanguageServerFeature::GotoTypeDefinition)
+ .iter()
+ .find_map(|language_server| {
+ let offset_encoding = language_server.offset_encoding();
+ let pos = doc.position(view.id, offset_encoding);
+ let future = language_server.goto_type_definition(doc.identifier(), pos, None)?;
+ Some((future, offset_encoding))
+ }) {
+ Some(future_offset_encoding) => future_offset_encoding,
None => {
cx.editor
- .set_error("Language server does not support goto-type-definition");
+ .set_error("No language server supports goto-type-definition");
return;
}
};
@@ -1054,16 +1115,19 @@ pub fn goto_type_definition(cx: &mut Context) {
pub fn goto_implementation(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- let language_server = language_server!(cx.editor, doc);
- let offset_encoding = language_server.offset_encoding();
-
- let pos = doc.position(view.id, offset_encoding);
-
- let future = match language_server.goto_implementation(doc.identifier(), pos, None) {
- Some(future) => future,
+ let (future, offset_encoding) = match doc
+ .language_servers_with_feature(LanguageServerFeature::GotoImplementation)
+ .iter()
+ .find_map(|language_server| {
+ let offset_encoding = language_server.offset_encoding();
+ let pos = doc.position(view.id, offset_encoding);
+ let future = language_server.goto_implementation(doc.identifier(), pos, None)?;
+ Some((future, offset_encoding))
+ }) {
+ Some(future_offset_encoding) => future_offset_encoding,
None => {
cx.editor
- .set_error("Language server does not support goto-implementation");
+ .set_error("no language server supports goto-implementation");
return;
}
};
@@ -1080,21 +1144,24 @@ pub fn goto_implementation(cx: &mut Context) {
pub fn goto_reference(cx: &mut Context) {
let config = cx.editor.config();
let (view, doc) = current!(cx.editor);
- let language_server = language_server!(cx.editor, doc);
- let offset_encoding = language_server.offset_encoding();
-
- let pos = doc.position(view.id, offset_encoding);
-
- let future = match language_server.goto_reference(
- doc.identifier(),
- pos,
- config.lsp.goto_reference_include_declaration,
- None,
- ) {
- Some(future) => future,
+ let (future, offset_encoding) = match doc
+ .language_servers_with_feature(LanguageServerFeature::GotoReference)
+ .iter()
+ .find_map(|language_server| {
+ let offset_encoding = language_server.offset_encoding();
+ let pos = doc.position(view.id, offset_encoding);
+ let future = language_server.goto_reference(
+ doc.identifier(),
+ pos,
+ config.lsp.goto_reference_include_declaration,
+ None,
+ )?;
+ Some((future, offset_encoding))
+ }) {
+ Some(future_offset_encoding) => future_offset_encoding,
None => {
cx.editor
- .set_error("Language server does not support goto-reference");
+ .set_error("language server supports goto-reference");
return;
}
};
@@ -1108,7 +1175,7 @@ pub fn goto_reference(cx: &mut Context) {
);
}
-#[derive(PartialEq, Eq)]
+#[derive(PartialEq, Eq, Clone, Copy)]
pub enum SignatureHelpInvoked {
Manual,
Automatic,
@@ -1120,35 +1187,34 @@ pub fn signature_help(cx: &mut Context) {
pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
let (view, doc) = current!(cx.editor);
- let was_manually_invoked = invoked == SignatureHelpInvoked::Manual;
- let language_server = match doc.language_server() {
- Some(language_server) => language_server,
+ // TODO merge multiple language server signature help into one instead of just taking the first language server that supports it
+ let future = match doc
+ .language_servers_with_feature(LanguageServerFeature::SignatureHelp)
+ .iter()
+ .find_map(|language_server| {
+ let pos = doc.position(view.id, language_server.offset_encoding());
+ language_server.text_document_signature_help(doc.identifier(), pos, None)
+ }) {
+ Some(future) => future.boxed(),
None => {
// Do not show the message if signature help was invoked
// automatically on backspace, trigger characters, etc.
- if was_manually_invoked {
+ if invoked == SignatureHelpInvoked::Manual {
cx.editor
- .set_status("Language server not active for current buffer");
- }
- return;
- }
- };
- let offset_encoding = language_server.offset_encoding();
-
- let pos = doc.position(view.id, offset_encoding);
-
- let future = match language_server.text_document_signature_help(doc.identifier(), pos, None) {
- Some(f) => f,
- None => {
- if was_manually_invoked {
- cx.editor
- .set_error("Language server does not support signature-help");
+ .set_error("No language server supports signature-help");
}
return;
}
};
+ signature_help_impl_with_future(cx, future, invoked);
+}
+pub fn signature_help_impl_with_future(
+ cx: &mut Context,
+ future: BoxFuture<'static, helix_lsp::Result<Value>>,
+ invoked: SignatureHelpInvoked,
+) {
cx.callback(
future,
move |editor, compositor, response: Option<lsp::SignatureHelp>| {
@@ -1156,7 +1222,7 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
if !(config.lsp.auto_signature_help
|| SignatureHelp::visible_popup(compositor).is_some()
- || was_manually_invoked)
+ || invoked == SignatureHelpInvoked::Manual)
{
return;
}
@@ -1165,7 +1231,7 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
// it very probably means the server was a little slow to respond and the user has
// already moved on to something else, making a signature help popup will just be an
// annoyance, see https://github.com/helix-editor/helix/issues/3112
- if !was_manually_invoked && editor.mode != Mode::Insert {
+ if invoked == SignatureHelpInvoked::Automatic && editor.mode != Mode::Insert {
return;
}
@@ -1255,18 +1321,20 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
pub fn hover(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- let language_server = language_server!(cx.editor, doc);
- let offset_encoding = language_server.offset_encoding();
// TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier
+ let request = doc
+ .language_servers_with_feature(LanguageServerFeature::Hover)
+ .iter()
+ .find_map(|language_server| {
+ let pos = doc.position(view.id, language_server.offset_encoding());
+ language_server.text_document_hover(doc.identifier(), pos, None)
+ });
- let pos = doc.position(view.id, offset_encoding);
-
- let future = match language_server.text_document_hover(doc.identifier(), pos, None) {
+ let future = match request {
Some(future) => future,
None => {
- cx.editor
- .set_error("Language server does not support hover");
+ cx.editor.set_error("No language server supports hover");
return;
}
};
@@ -1349,7 +1417,11 @@ pub fn rename_symbol(cx: &mut Context) {
}
}
- fn create_rename_prompt(editor: &Editor, prefill: String) -> Box<ui::Prompt> {
+ fn create_rename_prompt(
+ editor: &Editor,
+ prefill: String,
+ language_server_id: Option<usize>,
+ ) -> Box<ui::Prompt> {
let prompt = ui::Prompt::new(
"rename-to:".into(),
None,
@@ -1358,27 +1430,36 @@ pub fn rename_symbol(cx: &mut Context) {
if event != PromptEvent::Validate {
return;
}
-
let (view, doc) = current!(cx.editor);
- let language_server = language_server!(cx.editor, doc);
- let offset_encoding = language_server.offset_encoding();
-
- let pos = doc.position(view.id, offset_encoding);
-
- let future =
- match language_server.rename_symbol(doc.identifier(), pos, input.to_string()) {
- Some(future) => future,
- None => {
- cx.editor
- .set_error("Language server does not support symbol renaming");
- return;
+ let request = doc
+ .language_servers_with_feature(LanguageServerFeature::RenameSymbol)
+ .iter()
+ .find_map(|language_server| {
+ if let Some(language_server_id) = language_server_id {
+ if language_server.id() != language_server_id {
+ return None;
+ }
}
- };
- match block_on(future) {
- Ok(edits) => {
- let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits);
+ 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 {
+ match block_on(future) {
+ Ok(edits) => {
+ let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits);
+ }
+ Err(err) => cx.editor.set_error(err.to_string()),
}
- Err(err) => cx.editor.set_error(err.to_string()),
+ } else {
+ cx.editor
+ .set_error("No language server supports symbol renaming");
}
},
)
@@ -1388,20 +1469,20 @@ pub fn rename_symbol(cx: &mut Context) {
}
let (view, doc) = current!(cx.editor);
- let language_server = language_server!(cx.editor, doc);
- let offset_encoding = language_server.offset_encoding();
-
- if !language_server.supports_rename() {
- cx.editor
- .set_error("Language server does not support symbol renaming");
- return;
- }
- let pos = doc.position(view.id, offset_encoding);
+ let prepare_rename_request = doc
+ .language_servers_with_feature(LanguageServerFeature::RenameSymbol)
+ .iter()
+ .find_map(|language_server| {
+ let offset_encoding = language_server.offset_encoding();
+ let pos = doc.position(view.id, offset_encoding);
+ let future = language_server.prepare_rename(doc.identifier(), pos)?;
+ Some((future, offset_encoding, language_server.id()))
+ });
- match language_server.prepare_rename(doc.identifier(), pos) {
+ match prepare_rename_request {
// Language server supports textDocument/prepareRename, use it.
- Some(future) => cx.callback(
+ Some((future, offset_encoding, ls_id)) => cx.callback(
future,
move |editor, compositor, response: Option<lsp::PrepareRenameResponse>| {
let prefill = match get_prefill_from_lsp_response(editor, offset_encoding, response)
@@ -1413,7 +1494,7 @@ pub fn rename_symbol(cx: &mut Context) {
}
};
- let prompt = create_rename_prompt(editor, prefill);
+ let prompt = create_rename_prompt(editor, prefill, Some(ls_id));
compositor.push(prompt);
},
@@ -1423,7 +1504,7 @@ pub fn rename_symbol(cx: &mut Context) {
None => {
let prefill = get_prefill_from_word_boundary(cx.editor);
- let prompt = create_rename_prompt(cx.editor, prefill);
+ let prompt = create_rename_prompt(cx.editor, prefill, None);
cx.push_layer(prompt);
}
@@ -1432,17 +1513,20 @@ pub fn rename_symbol(cx: &mut Context) {
pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- let language_server = language_server!(cx.editor, doc);
- let offset_encoding = language_server.offset_encoding();
-
- let pos = doc.position(view.id, offset_encoding);
-
- let future = match language_server.text_document_document_highlight(doc.identifier(), pos, None)
- {
+ let (future, offset_encoding) = match doc
+ .language_servers_with_feature(LanguageServerFeature::DocumentHighlight)
+ .iter()
+ .find_map(|language_server| {
+ let offset_encoding = language_server.offset_encoding();
+ let pos = doc.position(view.id, offset_encoding);
+ let future =
+ language_server.text_document_document_highlight(doc.identifier(), pos, None)?;
+ Some((future, offset_encoding))
+ }) {
Some(future) => future,
None => {
cx.editor
- .set_error("Language server does not support document highlight");
+ .set_error("No language server supports document-highlight");
return;
}
};
@@ -1455,8 +1539,6 @@ pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
_ => return,
};
let (view, doc) = current!(editor);
- let language_server = language_server!(editor, doc);
- let offset_encoding = language_server.offset_encoding();
let text = doc.text();
let pos = doc.selection(view.id).primary().head;
@@ -1502,63 +1584,58 @@ fn compute_inlay_hints_for_view(
let view_id = view.id;
let doc_id = view.doc;
- let language_server = doc.language_server()?;
-
- let capabilities = language_server.capabilities();
-
- let (future, new_doc_inlay_hints_id) = match capabilities.inlay_hint_provider {
- Some(
- lsp::OneOf::Left(true)
- | lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_)),
- ) => {
- let doc_text = doc.text();
- let len_lines = doc_text.len_lines();
-
- // Compute ~3 times the current view height of inlay hints, that way some scrolling
- // will not show half the view with hints and half without while still being faster
- // than computing all the hints for the full file (which could be dozens of time
- // longer than the view is).
- let view_height = view.inner_height();
- let first_visible_line =
- doc_text.char_to_line(view.offset.anchor.min(doc_text.len_chars()));
- let first_line = first_visible_line.saturating_sub(view_height);
- let last_line = first_visible_line
- .saturating_add(view_height.saturating_mul(2))
- .min(len_lines);
-
- let new_doc_inlay_hint_id = DocumentInlayHintsId {
- first_line,
- last_line,
- };
- // Don't recompute the annotations in case nothing has changed about the view
- if !doc.inlay_hints_oudated
- && doc
- .inlay_hints(view_id)
- .map_or(false, |dih| dih.id == new_doc_inlay_hint_id)
- {
- return None;
- }
+ let language_servers = doc.language_servers_with_feature(LanguageServerFeature::InlayHints);
+ let language_server = language_servers.iter().find(|language_server| {
+ matches!(
+ language_server.capabilities().inlay_hint_provider,
+ Some(
+ lsp::OneOf::Left(true)
+ | lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_))
+ )
+ )
+ })?;
+
+ let doc_text = doc.text();
+ let len_lines = doc_text.len_lines();
+
+ // Compute ~3 times the current view height of inlay hints, that way some scrolling
+ // will not show half the view with hints and half without while still being faster
+ // than computing all the hints for the full file (which could be dozens of time
+ // longer than the view is).
+ let view_height = view.inner_height();
+ let first_visible_line = doc_text.char_to_line(view.offset.anchor.min(doc_text.len_chars()));
+ let first_line = first_visible_line.saturating_sub(view_height);
+ let last_line = first_visible_line
+ .saturating_add(view_height.saturating_mul(2))
+ .min(len_lines);
+
+ let new_doc_inlay_hints_id = DocumentInlayHintsId {
+ first_line,
+ last_line,
+ };
+ // Don't recompute the annotations in case nothing has changed about the view
+ if !doc.inlay_hints_oudated
+ && doc
+ .inlay_hints(view_id)
+ .map_or(false, |dih| dih.id == new_doc_inlay_hints_id)
+ {
+ return None;
+ }
- let doc_slice = doc_text.slice(..);
- let first_char_in_range = doc_slice.line_to_char(first_line);
- let last_char_in_range = doc_slice.line_to_char(last_line);
+ let doc_slice = doc_text.slice(..);
+ let first_char_in_range = doc_slice.line_to_char(first_line);
+ let last_char_in_range = doc_slice.line_to_char(last_line);
- let range = helix_lsp::util::range_to_lsp_range(
- doc_text,
- helix_core::Range::new(first_char_in_range, last_char_in_range),
- language_server.offset_encoding(),
- );
+ let range = helix_lsp::util::range_to_lsp_range(
+ doc_text,
+ helix_core::Range::new(first_char_in_range, last_char_in_range),
+ language_server.offset_encoding(),
+ );
- (
- language_server.text_document_range_inlay_hints(doc.identifier(), range, None),
- new_doc_inlay_hint_id,
- )
- }
- _ => return None,
- };
+ let offset_encoding = language_server.offset_encoding();
let callback = super::make_job_callback(
- future?,
+ language_server.text_document_range_inlay_hints(doc.identifier(), range, None)?,
move |editor, _compositor, response: Option<Vec<lsp::InlayHint>>| {
// The config was modified or the window was closed while the request was in flight
if !editor.config().lsp.display_inlay_hints || editor.tree.try_get(view_id).is_none() {
@@ -1572,8 +1649,8 @@ fn compute_inlay_hints_for_view(
};
// If we have neither hints nor an LSP, empty the inlay hints since they're now oudated
- let (mut hints, offset_encoding) = match (response, doc.language_server()) {
- (Some(h), Some(ls)) if !h.is_empty() => (h, ls.offset_encoding()),
+ let mut hints = match response {
+ Some(hints) if !hints.is_empty() => hints,
_ => {
doc.set_inlay_hints(
view_id,
diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs
index 81a24059..b78de772 100644
--- a/helix-term/src/commands/typed.rs
+++ b/helix-term/src/commands/typed.rs
@@ -1329,23 +1329,20 @@ fn lsp_workspace_command(
if event != PromptEvent::Validate {
return Ok(());
}
-
- let (_, doc) = current!(cx.editor);
-
- let language_server = match doc.language_server() {
- Some(language_server) => language_server,
- None => {
- cx.editor
- .set_status("Language server not active for current buffer");
- return Ok(());
- }
- };
-
- let options = match &language_server.capabilities().execute_command_provider {
- Some(options) => options,
+ let doc = doc!(cx.editor);
+ let language_servers =
+ doc.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand);
+ let (language_server_id, options) = match language_servers.iter().find_map(|ls| {
+ ls.capabilities()
+ .execute_command_provider
+ .as_ref()
+ .map(|options| (ls.id(), options))
+ }) {
+ Some(id_options) => id_options,
None => {
- cx.editor
- .set_status("Workspace commands are not supported for this language server");
+ cx.editor.set_status(
+ "No active language servers for this document support workspace commands",
+ );
return Ok(());
}
};
@@ -1362,8 +1359,8 @@ fn lsp_workspace_command(
let callback = async move {
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |_editor: &mut Editor, compositor: &mut Compositor| {
- let picker = ui::Picker::new(commands, (), |cx, command, _action| {
- execute_lsp_command(cx.editor, command.clone());
+ let picker = ui::Picker::new(commands, (), move |cx, command, _action| {
+ execute_lsp_command(cx.editor, language_server_id, command.clone());
});
compositor.push(Box::new(overlaid(picker)))
},
@@ -1376,6 +1373,7 @@ fn lsp_workspace_command(
if options.commands.iter().any(|c| c == &command) {
execute_lsp_command(
cx.editor,
+ language_server_id,
helix_lsp::lsp::Command {
title: command.clone(),
arguments: None,
@@ -1426,7 +1424,7 @@ fn lsp_restart(
.collect();
for document_id in document_ids_to_refresh {
- cx.editor.refresh_language_server(document_id);
+ cx.editor.refresh_language_servers(document_id);
}
Ok(())
@@ -1443,21 +1441,63 @@ fn lsp_stop(
let doc = doc!(cx.editor);
- let ls_id = doc
- .language_server()
- .map(|ls| ls.id())
- .context("LSP not running for the current document")?;
+ // TODO this stops language servers which may be used in another doc/language type that uses the same language servers
+ // I'm not sure if this is really what we want
+ let ls_shutdown_names = doc
+ .language_servers()
+ .iter()
+ .map(|ls| ls.name())
+ .collect::<Vec<_>>();
- let config = doc
- .language_config()
- .context("LSP not defined for the current document")?;
- cx.editor.language_servers.stop(config);
+ for ls_name in &ls_shutdown_names {
+ cx.editor.language_servers.stop(ls_name);
+ }
+
+ let doc_ids_active_clients: Vec<_> = cx
+ .editor
+ .documents()
+ .filter_map(|doc| {
+ let doc_active_ls_ids: Vec<_> = doc
+ .language_servers()
+ .iter()
+ .filter(|ls| !ls_shutdown_names.contains(&ls.name()))
+ .map(|ls| ls.id())
+ .collect();
+
+ let active_clients: Vec<_> = cx
+ .editor
+ .language_servers
+ .iter_clients()
+ .filter(|client| doc_active_ls_ids.contains(&client.id()))
+ .map(Clone::clone)
+ .collect();
+
+ if active_clients.len() != doc.language_servers().len() {
+ Some((doc.id(), active_clients))
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ for (doc_id, active_clients) in doc_ids_active_clients {
+ let doc = cx.editor.documents.get_mut(&doc_id).unwrap();
+
+ let stopped_clients: Vec<_> = doc
+ .language_servers()
+ .iter()
+ .filter(|ls| {
+ !active_clients
+ .iter()
+ .any(|active_ls| active_ls.id() == ls.id())
+ })
+ .map(|ls| ls.id())
+ .collect(); // is necessary because of borrow-checking
- for doc in cx.editor.documents_mut() {
- if doc.language_server().map_or(false, |ls| ls.id() == ls_id) {
- doc.set_language_server(None);
- doc.set_diagnostics(Default::default());
+ for client_id in stopped_clients {
+ doc.clear_diagnostics(client_id)
}
+ doc.set_language_servers(active_clients);
}
Ok(())
@@ -1850,7 +1890,7 @@ fn language(
doc.detect_indent_and_line_ending();
let id = doc.id();
- cx.editor.refresh_language_server(id);
+ cx.editor.refresh_language_servers(id);
Ok(())
}
@@ -2588,7 +2628,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "lsp-restart",
aliases: &[],
- doc: "Restarts the Language Server that is in use by the current doc",
+ doc: "Restarts the language servers used by the current doc",
fun: lsp_restart,
signature: CommandSignature::none(),
},
diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs
index 480c2c67..031f982c 100644
--- a/helix-term/src/health.rs
+++ b/helix-term/src/health.rs
@@ -2,7 +2,10 @@ use crossterm::{
style::{Color, Print, Stylize},
tty::IsTty,
};
-use helix_core::config::{default_syntax_loader, user_syntax_loader};
+use helix_core::{
+ config::{default_syntax_loader, user_syntax_loader},
+ syntax::LanguageServerFeatureConfiguration,
+};
use helix_loader::grammar::load_runtime_file;
use helix_view::clipboard::get_clipboard_provider;
use std::io::Write;
@@ -192,10 +195,14 @@ pub fn languages_all() -> std::io::Result<()> {
for lang in &syn_loader_conf.language {
column(&lang.language_id, Color::Reset);
- let lsp = lang
- .language_server
- .as_ref()
- .map(|lsp| lsp.command.to_string());
+ // TODO multiple language servers (check binary for each supported language server, not just the first)
+
+ let lsp = lang.language_servers.first().and_then(|lsp| {
+ syn_loader_conf
+ .language_server
+ .get(lsp.name())
+ .map(|config| config.command.clone())
+ });
check_binary(lsp);
let dap = lang.debugger.as_ref().map(|dap| dap.command.to_string());
@@ -264,11 +271,15 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
}
};
+ // TODO multiple language servers
probe_protocol(
"language server",
- lang.language_server
- .as_ref()
- .map(|lsp| lsp.command.to_string()),
+ lang.language_servers.first().and_then(|lsp| {
+ syn_loader_conf
+ .language_server
+ .get(lsp.name())
+ .map(|config| config.command.clone())
+ }),
)?;
probe_protocol(
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index c5c40580..859403a7 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -15,7 +15,7 @@ use helix_view::{graphics::Rect, Document, Editor};
use crate::commands;
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
-use helix_lsp::{lsp, util};
+use helix_lsp::{lsp, util, OffsetEncoding};
impl menu::Item for CompletionItem {
type Data = ();
@@ -38,6 +38,7 @@ impl menu::Item for CompletionItem {
|| self.item.tags.as_ref().map_or(false, |tags| {
tags.contains(&lsp::CompletionItemTag::DEPRECATED)
});
+
menu::Row::new(vec![
menu::Cell::from(Span::styled(
self.item.label.as_str(),
@@ -79,19 +80,16 @@ impl menu::Item for CompletionItem {
}
None => "",
}),
- // self.detail.as_deref().unwrap_or("")
- // self.label_details
- // .as_ref()
- // .or(self.detail())
- // .as_str(),
])
}
}
#[derive(Debug, PartialEq, Default, Clone)]
-struct CompletionItem {
- item: lsp::CompletionItem,
- resolved: bool,
+pub struct CompletionItem {
+ pub item: lsp::CompletionItem,
+ pub language_server_id: usize,
+ pub offset_encoding: OffsetEncoding,
+ pub resolved: bool,
}
/// Wraps a Menu.
@@ -109,21 +107,13 @@ impl Completion {
pub fn new(
editor: &Editor,
savepoint: Arc<SavePoint>,
- mut items: Vec<lsp::CompletionItem>,
- offset_encoding: helix_lsp::OffsetEncoding,
+ mut items: Vec<CompletionItem>,
start_offset: usize,
trigger_offset: usize,
) -> Self {
let replace_mode = editor.config().completion_replace;
// Sort completion items according to their preselect status (given by the LSP server)
- items.sort_by_key(|item| !item.preselect.unwrap_or(false));
- let items = items
- .into_iter()
- .map(|item| CompletionItem {
- item,
- resolved: false,
- })
- .collect();
+ items.sort_by_key(|item| !item.item.preselect.unwrap_or(false));
// Then create the menu
let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
@@ -131,7 +121,6 @@ impl Completion {
doc: &Document,
view_id: ViewId,
item: &CompletionItem,
- offset_encoding: helix_lsp::OffsetEncoding,
trigger_offset: usize,
include_placeholder: bool,
replace_mode: bool,
@@ -154,6 +143,8 @@ impl Completion {
}
};
+ let offset_encoding = item.offset_encoding;
+
let Some(range) = util::lsp_range_to_range(doc.text(), edit.range, offset_encoding) else{
return Transaction::new(doc.text());
};
@@ -247,15 +238,8 @@ impl Completion {
// always present here
let item = item.unwrap();
- let transaction = item_to_transaction(
- doc,
- view.id,
- item,
- offset_encoding,
- trigger_offset,
- true,
- replace_mode,
- );
+ let transaction =
+ item_to_transaction(doc, view.id, item, trigger_offset, true, replace_mode);
doc.apply_temporary(&transaction, view.id);
}
PromptEvent::Validate => {
@@ -267,10 +251,15 @@ impl Completion {
// always present here
let mut item = item.unwrap().clone();
+ let language_server = editor
+ .language_servers
+ .get_by_id(item.language_server_id)
+ .unwrap();
+
// resolve item if not yet resolved
if !item.resolved {
if let Some(resolved) =
- Self::resolve_completion_item(doc, item.item.clone())
+ Self::resolve_completion_item(language_server, item.item.clone())
{
item.item = resolved;
}
@@ -281,7 +270,6 @@ impl Completion {
doc,
view.id,
&item,
- offset_encoding,
trigger_offset,
false,
replace_mode,
@@ -299,7 +287,7 @@ impl Completion {
let transaction = util::generate_transaction_from_edits(
doc.text(),
additional_edits,
- offset_encoding, // TODO: should probably transcode in Client
+ item.offset_encoding, // TODO: should probably transcode in Client
);
doc.apply(&transaction, view.id);
}
@@ -323,10 +311,17 @@ impl Completion {
}
fn resolve_completion_item(
- doc: &Document,
+ language_server: &helix_lsp::Client,
completion_item: lsp::CompletionItem,
) -> Option<lsp::CompletionItem> {
- let language_server = doc.language_server()?;
+ let completion_resolve_provider = language_server
+ .capabilities()
+ .completion_provider
+ .as_ref()?
+ .resolve_provider;
+ if completion_resolve_provider != Some(true) {
+ return None;
+ }
let future = language_server.resolve_completion_item(completion_item)?;
let response = helix_lsp::block_on(future);
@@ -397,8 +392,11 @@ impl Completion {
Some(item) if !item.resolved => item.clone(),
_ => return false,
};
-
- let language_server = match doc!(cx.editor).language_server() {
+ let language_server = match cx
+ .editor
+ .language_servers
+ .get_by_id(current_item.language_server_id)
+ {
Some(language_server) => language_server,
None => return false,
};
@@ -422,13 +420,14 @@ impl Completion {
.unwrap()
.completion
{
- completion.replace_item(
- current_item,
- CompletionItem {
- item: resolved_item,
- resolved: true,
- },
- );
+ let resolved_item = CompletionItem {
+ item: resolved_item,
+ language_server_id: current_item.language_server_id,
+ offset_encoding: current_item.offset_encoding,
+ resolved: true,
+ };
+
+ completion.replace_item(current_item, resolved_item);
}
},
);
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index f0989fa8..43b5d1af 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -33,7 +33,7 @@ use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
use tui::{buffer::Buffer as Surface, text::Span};
-use super::statusline;
+use super::{completion::CompletionItem, statusline};
use super::{document::LineDecoration, lsp::SignatureHelp};
pub struct EditorView {
@@ -650,7 +650,7 @@ impl EditorView {
.primary()
.cursor(doc.text().slice(..));
- let diagnostics = doc.diagnostics().iter().filter(|diagnostic| {
+ let diagnostics = doc.shown_diagnostics().filter(|diagnostic| {
diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
});
@@ -953,20 +953,13 @@ impl EditorView {
&mut self,
editor: &mut Editor,
savepoint: Arc<SavePoint>,
- items: Vec<helix_lsp::lsp::CompletionItem>,
- offset_encoding: helix_lsp::OffsetEncoding,
+ items: Vec<CompletionItem>,
start_offset: usize,
trigger_offset: usize,
size: Rect,
) -> Option<Rect> {
- let mut completion = Completion::new(
- editor,
- savepoint,
- items,
- offset_encoding,
- start_offset,
- trigger_offset,
- );
+ let mut completion =
+ Completion::new(editor, savepoint, items, start_offset, trigger_offset);
if completion.is_empty() {
// skip if we got no completion results
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 3e9a14b0..118836c0 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -17,7 +17,7 @@ mod text;
use crate::compositor::{Component, Compositor};
use crate::filter_picker_entry;
use crate::job::{self, Callback};
-pub use completion::Completion;
+pub use completion::{Completion, CompletionItem};
pub use editor::EditorView;
pub use markdown::Markdown;
pub use menu::Menu;
@@ -238,6 +238,7 @@ pub mod completers {
use crate::ui::prompt::Completion;
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
+ use helix_core::syntax::LanguageServerFeature;
use helix_view::document::SCRATCH_BUFFER_NAME;
use helix_view::theme;
use helix_view::{editor::Config, Editor};
@@ -393,17 +394,13 @@ pub mod completers {
pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
let matcher = Matcher::default();
- let (_, doc) = current_ref!(editor);
-
- let language_server = match doc.language_server() {
- Some(language_server) => language_server,
- None => {
- return vec![];
- }
- };
-
- let options = match &language_server.capabilities().execute_command_provider {
- Some(options) => options,
+ let language_servers =
+ doc!(editor).language_servers_with_feature(LanguageServerFeature::WorkspaceCommand);
+ let options = match language_servers
+ .into_iter()
+ .find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
+ {
+ Some(id_options) => id_options,
None => {
return vec![];
}
diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs
index 88786351..b10e8076 100644
--- a/helix-term/src/ui/statusline.rs
+++ b/helix-term/src/ui/statusline.rs
@@ -197,15 +197,16 @@ where
);
}
+// TODO think about handling multiple language servers
fn render_lsp_spinner<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
+ let language_servers = context.doc.language_servers();
write(
context,
- context
- .doc
- .language_server()
+ language_servers
+ .first()
.and_then(|srv| {
context
.spinners
@@ -225,8 +226,7 @@ where
{
let (warnings, errors) = context
.doc
- .diagnostics()
- .iter()
+ .shown_diagnostics()
.fold((0, 0), |mut counts, diag| {
use helix_core::diagnostic::Severity;
match diag.severity {
@@ -266,7 +266,7 @@ where
.diagnostics
.values()
.flatten()
- .fold((0, 0), |mut counts, diag| {
+ .fold((0, 0), |mut counts, (diag, _, _)| {
match diag.severity {
Some(DiagnosticSeverity::WARNING) => counts.0 += 1,
Some(DiagnosticSeverity::ERROR) | None => counts.1 += 1,