Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/commands/lsp.rs')
-rw-r--r--helix-term/src/commands/lsp.rs679
1 files changed, 323 insertions, 356 deletions
diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs
index 0494db3e..3b9efb43 100644
--- a/helix-term/src/commands/lsp.rs
+++ b/helix-term/src/commands/lsp.rs
@@ -14,8 +14,7 @@ use tui::{text::Span, widgets::Row};
use super::{align_view, push_jump, Align, Context, Editor};
use helix_core::{
- diagnostic::DiagnosticProvider, syntax::config::LanguageServerFeature,
- text_annotations::InlineAnnotation, Selection, Uri,
+ syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection, Uri,
};
use helix_stdx::path;
use helix_view::{
@@ -32,7 +31,13 @@ use crate::{
ui::{self, overlay::overlaid, FileLocation, Picker, Popup, PromptEvent},
};
-use std::{cmp::Ordering, collections::HashSet, fmt::Display, future::Future, path::Path};
+use std::{
+ cmp::Ordering,
+ collections::{BTreeMap, HashSet},
+ fmt::Write,
+ future::Future,
+ path::Path,
+};
/// 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.
@@ -46,7 +51,7 @@ macro_rules! language_server_with_feature {
match language_server {
Some(language_server) => language_server,
None => {
- $editor.set_error(format!(
+ $editor.set_status(format!(
"No configured language server supports {}",
$feature
));
@@ -56,36 +61,10 @@ macro_rules! language_server_with_feature {
}};
}
-/// A wrapper around `lsp::Location` that swaps out the LSP URI for `helix_core::Uri` and adds
-/// the server's offset encoding.
-#[derive(Debug, Clone, PartialEq, Eq)]
-struct Location {
- uri: Uri,
- range: lsp::Range,
- offset_encoding: OffsetEncoding,
-}
-
-fn lsp_location_to_location(
- location: lsp::Location,
- offset_encoding: OffsetEncoding,
-) -> Option<Location> {
- let uri = match location.uri.try_into() {
- Ok(uri) => uri,
- Err(err) => {
- log::warn!("discarding invalid or unsupported URI: {err}");
- return None;
- }
- };
- Some(Location {
- uri,
- range: location.range,
- offset_encoding,
- })
-}
-
struct SymbolInformationItem {
- location: Location,
symbol: lsp::SymbolInformation,
+ offset_encoding: OffsetEncoding,
+ uri: Uri,
}
struct DiagnosticStyles {
@@ -96,35 +75,35 @@ struct DiagnosticStyles {
}
struct PickerDiagnostic {
- location: Location,
+ uri: Uri,
diag: lsp::Diagnostic,
+ offset_encoding: OffsetEncoding,
}
-fn location_to_file_location(location: &Location) -> Option<FileLocation<'_>> {
- let path = location.uri.as_path()?;
- let line = Some((
- location.range.start.line as usize,
- location.range.end.line as usize,
- ));
+fn uri_to_file_location<'a>(uri: &'a Uri, range: &lsp::Range) -> Option<FileLocation<'a>> {
+ let path = uri.as_path()?;
+ let line = Some((range.start.line as usize, range.end.line as usize));
Some((path.into(), line))
}
-fn jump_to_location(editor: &mut Editor, location: &Location, action: Action) {
+fn jump_to_location(
+ editor: &mut Editor,
+ location: &lsp::Location,
+ offset_encoding: OffsetEncoding,
+ action: Action,
+) {
let (view, doc) = current!(editor);
push_jump(view, doc);
- let Some(path) = location.uri.as_path() else {
- let err = format!("unable to convert URI to filepath: {:?}", location.uri);
- editor.set_error(err);
- return;
+ let path = match location.uri.to_file_path() {
+ Ok(path) => path,
+ Err(_) => {
+ let err = format!("unable to convert URI to filepath: {}", location.uri);
+ editor.set_error(err);
+ return;
+ }
};
- jump_to_position(
- editor,
- path,
- location.range,
- location.offset_encoding,
- action,
- );
+ jump_to_position(editor, &path, location.range, offset_encoding, action);
}
fn jump_to_position(
@@ -204,7 +183,7 @@ type DiagnosticsPicker = Picker<PickerDiagnostic, DiagnosticStyles>;
fn diag_picker(
cx: &Context,
- diagnostics: impl IntoIterator<Item = (Uri, Vec<(lsp::Diagnostic, DiagnosticProvider)>)>,
+ diagnostics: BTreeMap<Uri, Vec<(lsp::Diagnostic, LanguageServerId)>>,
format: DiagnosticsFormat,
) -> DiagnosticsPicker {
// TODO: drop current_path comparison and instead use workspace: bool flag?
@@ -214,30 +193,17 @@ fn diag_picker(
for (uri, diags) in diagnostics {
flat_diag.reserve(diags.len());
- for (diag, provider) in diags {
- if let Some(ls) = provider
- .language_server_id()
- .and_then(|id| cx.editor.language_server_by_id(id))
- {
+ for (diag, ls) in diags {
+ if let Some(ls) = cx.editor.language_server_by_id(ls) {
flat_diag.push(PickerDiagnostic {
- location: Location {
- uri: uri.clone(),
- range: diag.range,
- offset_encoding: ls.offset_encoding(),
- },
+ uri: uri.clone(),
diag,
+ offset_encoding: ls.offset_encoding(),
});
}
}
}
- flat_diag.sort_by(|a, b| {
- a.diag
- .severity
- .unwrap_or(lsp::DiagnosticSeverity::HINT)
- .cmp(&b.diag.severity.unwrap_or(lsp::DiagnosticSeverity::HINT))
- });
-
let styles = DiagnosticStyles {
hint: cx.editor.theme.get("hint"),
info: cx.editor.theme.get("info"),
@@ -259,9 +225,6 @@ fn diag_picker(
.into()
},
),
- ui::PickerColumn::new("source", |item: &PickerDiagnostic, _| {
- item.diag.source.as_deref().unwrap_or("").into()
- }),
ui::PickerColumn::new("code", |item: &PickerDiagnostic, _| {
match item.diag.code.as_ref() {
Some(NumberOrString::Number(n)) => n.to_string().into(),
@@ -273,14 +236,14 @@ fn diag_picker(
item.diag.message.as_str().into()
}),
];
- let mut primary_column = 3; // message
+ let mut primary_column = 2; // message
if format == DiagnosticsFormat::ShowSourcePath {
columns.insert(
// between message code and message
- 3,
+ 2,
ui::PickerColumn::new("path", |item: &PickerDiagnostic, _| {
- if let Some(path) = item.location.uri.as_path() {
+ if let Some(path) = item.uri.as_path() {
path::get_truncated_path(path)
.to_string_lossy()
.to_string()
@@ -298,14 +261,26 @@ fn diag_picker(
primary_column,
flat_diag,
styles,
- move |cx, diag, action| {
- jump_to_location(cx.editor, &diag.location, action);
+ move |cx,
+ PickerDiagnostic {
+ uri,
+ diag,
+ offset_encoding,
+ },
+ action| {
+ let Some(path) = uri.as_path() else {
+ return;
+ };
+ jump_to_position(cx.editor, path, diag.range, *offset_encoding, action);
let (view, doc) = current!(cx.editor);
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
},
)
- .with_preview(move |_editor, diag| location_to_file_location(&diag.location))
+ .with_preview(move |_editor, PickerDiagnostic { uri, diag, .. }| {
+ let line = Some((diag.range.start.line as usize, diag.range.end.line as usize));
+ Some((uri.as_path()?.into(), line))
+ })
.truncate_start(false)
}
@@ -327,11 +302,8 @@ pub fn symbol_picker(cx: &mut Context) {
location: lsp::Location::new(file.uri.clone(), symbol.selection_range),
container_name: None,
},
- location: Location {
- uri: uri.clone(),
- range: symbol.selection_range,
- offset_encoding,
- },
+ offset_encoding,
+ uri: uri.clone(),
});
for child in symbol.children.into_iter().flatten() {
nested_to_flat(list, file, uri, child, offset_encoding);
@@ -353,7 +325,9 @@ pub fn symbol_picker(cx: &mut Context) {
.expect("docs with active language servers must be backed by paths");
async move {
- let symbols = match request.await? {
+ 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![]),
};
@@ -363,12 +337,9 @@ pub fn symbol_picker(cx: &mut Context) {
lsp::DocumentSymbolResponse::Flat(symbols) => symbols
.into_iter()
.map(|symbol| SymbolInformationItem {
- location: Location {
- uri: doc_uri.clone(),
- range: symbol.location.range,
- offset_encoding,
- },
+ uri: doc_uri.clone(),
symbol,
+ offset_encoding,
})
.collect(),
lsp::DocumentSymbolResponse::Nested(symbols) => {
@@ -398,11 +369,9 @@ pub fn symbol_picker(cx: &mut Context) {
cx.jobs.callback(async move {
let mut symbols = Vec::new();
- while let Some(response) = futures.next().await {
- match response {
- Ok(mut items) => symbols.append(&mut items),
- Err(err) => log::error!("Error requesting document symbols: {err}"),
- }
+ // 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| {
let columns = [
@@ -415,13 +384,6 @@ pub fn symbol_picker(cx: &mut Context) {
ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| {
item.symbol.name.as_str().into()
}),
- ui::PickerColumn::new("container", |item: &SymbolInformationItem, _| {
- item.symbol
- .container_name
- .as_deref()
- .unwrap_or_default()
- .into()
- }),
];
let picker = Picker::new(
@@ -430,10 +392,17 @@ pub fn symbol_picker(cx: &mut Context) {
symbols,
(),
move |cx, item, action| {
- jump_to_location(cx.editor, &item.location, action);
+ jump_to_location(
+ cx.editor,
+ &item.symbol.location,
+ item.offset_encoding,
+ action,
+ );
},
)
- .with_preview(move |_editor, item| location_to_file_location(&item.location))
+ .with_preview(move |_editor, item| {
+ uri_to_file_location(&item.uri, &item.symbol.location.range)
+ })
.truncate_start(false);
compositor.push(Box::new(overlaid(picker)))
@@ -469,34 +438,27 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
.unwrap();
let offset_encoding = language_server.offset_encoding();
async move {
- let symbols = request
- .await?
- .and_then(|resp| match resp {
- lsp::WorkspaceSymbolResponse::Flat(symbols) => Some(symbols),
- lsp::WorkspaceSymbolResponse::Nested(_) => None,
- })
- .unwrap_or_default();
-
- let response: Vec<_> = symbols
- .into_iter()
- .filter_map(|symbol| {
- let uri = match Uri::try_from(&symbol.location.uri) {
- Ok(uri) => uri,
- Err(err) => {
- log::warn!("discarding symbol with invalid URI: {err}");
- return None;
- }
- };
- Some(SymbolInformationItem {
- location: Location {
+ let json = request.await?;
+
+ let response: Vec<_> =
+ serde_json::from_value::<Option<Vec<lsp::SymbolInformation>>>(json)?
+ .unwrap_or_default()
+ .into_iter()
+ .filter_map(|symbol| {
+ let uri = match Uri::try_from(&symbol.location.uri) {
+ Ok(uri) => uri,
+ Err(err) => {
+ log::warn!("discarding symbol with invalid URI: {err}");
+ return None;
+ }
+ };
+ Some(SymbolInformationItem {
+ symbol,
uri,
- range: symbol.location.range,
offset_encoding,
- },
- symbol,
+ })
})
- })
- .collect();
+ .collect();
anyhow::Ok(response)
}
@@ -509,14 +471,10 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
let injector = injector.clone();
async move {
- while let Some(response) = futures.next().await {
- match response {
- Ok(items) => {
- for item in items {
- injector.push(item)?;
- }
- }
- Err(err) => log::error!("Error requesting workspace symbols: {err}"),
+ // TODO if one symbol request errors, all other requests are discarded (even if they're valid)
+ while let Some(lsp_items) = futures.try_next().await? {
+ for item in lsp_items {
+ injector.push(item)?;
}
}
Ok(())
@@ -531,15 +489,8 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
item.symbol.name.as_str().into()
})
.without_filtering(),
- ui::PickerColumn::new("container", |item: &SymbolInformationItem, _| {
- item.symbol
- .container_name
- .as_deref()
- .unwrap_or_default()
- .into()
- }),
ui::PickerColumn::new("path", |item: &SymbolInformationItem, _| {
- if let Some(path) = item.location.uri.as_path() {
+ if let Some(path) = item.uri.as_path() {
path::get_relative_path(path)
.to_string_lossy()
.to_string()
@@ -556,10 +507,15 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
[],
(),
move |cx, item, action| {
- jump_to_location(cx.editor, &item.location, action);
+ jump_to_location(
+ cx.editor,
+ &item.symbol.location,
+ item.offset_encoding,
+ action,
+ );
},
)
- .with_preview(|_editor, item| location_to_file_location(&item.location))
+ .with_preview(|_editor, item| uri_to_file_location(&item.uri, &item.symbol.location.range))
.with_dynamic_query(get_symbols, None)
.truncate_start(false);
@@ -570,7 +526,11 @@ pub fn diagnostics_picker(cx: &mut Context) {
let doc = doc!(cx.editor);
if let Some(uri) = doc.uri() {
let diagnostics = cx.editor.diagnostics.get(&uri).cloned().unwrap_or_default();
- let picker = diag_picker(cx, [(uri, diagnostics)], DiagnosticsFormat::HideSourcePath);
+ let picker = diag_picker(
+ cx,
+ [(uri, diagnostics)].into(),
+ DiagnosticsFormat::HideSourcePath,
+ );
cx.push_layer(Box::new(overlaid(picker)));
}
}
@@ -589,7 +549,7 @@ struct CodeActionOrCommandItem {
impl ui::menu::Item for CodeActionOrCommandItem {
type Data = ();
- fn format(&self, _data: &Self::Data) -> Row<'_> {
+ fn format(&self, _data: &Self::Data) -> Row {
match &self.lsp_item {
lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(),
lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
@@ -688,8 +648,11 @@ pub fn code_action(cx: &mut Context) {
Some((code_action_request, language_server_id))
})
.map(|(request, ls_id)| async move {
- let Some(mut actions) = request.await? else {
- return anyhow::Ok(Vec::new());
+ let json = request.await?;
+ let response: Option<lsp::CodeActionResponse> = serde_json::from_value(json)?;
+ let mut actions = match response {
+ Some(a) => a,
+ None => return anyhow::Ok(Vec::new()),
};
// remove disabled code actions
@@ -754,12 +717,9 @@ pub fn code_action(cx: &mut Context) {
cx.jobs.callback(async move {
let mut actions = Vec::new();
-
- while let Some(output) = futures.next().await {
- match output {
- Ok(mut lsp_items) => actions.append(&mut lsp_items),
- Err(err) => log::error!("while gathering code actions: {err}"),
- }
+ // 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| {
@@ -784,16 +744,22 @@ pub fn code_action(cx: &mut Context) {
match &action.lsp_item {
lsp::CodeActionOrCommand::Command(command) => {
log::debug!("code action command: {:?}", command);
- editor.execute_lsp_command(command.clone(), action.language_server_id);
+ execute_lsp_command(editor, action.language_server_id, command.clone());
}
lsp::CodeActionOrCommand::CodeAction(code_action) => {
log::debug!("code action: {:?}", code_action);
// we support lsp "codeAction/resolve" for `edit` and `command` fields
let mut resolved_code_action = None;
if code_action.edit.is_none() || code_action.command.is_none() {
- if let Some(future) = language_server.resolve_code_action(code_action) {
- if let Ok(code_action) = helix_lsp::block_on(future) {
- resolved_code_action = Some(code_action);
+ if let Some(future) =
+ language_server.resolve_code_action(code_action.clone())
+ {
+ if let Ok(response) = helix_lsp::block_on(future) {
+ if let Ok(code_action) =
+ serde_json::from_value::<CodeAction>(response)
+ {
+ resolved_code_action = Some(code_action);
+ }
}
}
}
@@ -807,16 +773,14 @@ 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 {
- editor.execute_lsp_command(command.clone(), action.language_server_id);
+ execute_lsp_command(editor, action.language_server_id, command.clone());
}
}
}
});
picker.move_down(); // pre-select the first item
- let popup = Popup::new("code-action", picker)
- .with_scrollbar(false)
- .auto_close(true);
+ let popup = Popup::new("code-action", picker).with_scrollbar(false);
compositor.replace_or_push("code-action", popup);
};
@@ -825,6 +789,33 @@ pub fn code_action(cx: &mut Context) {
});
}
+pub fn execute_lsp_command(
+ editor: &mut Editor,
+ language_server_id: LanguageServerId,
+ cmd: lsp::Command,
+) {
+ // the command is executed on the server and communicated back
+ // to the client asynchronously using workspace edits
+ let future = match editor
+ .language_server_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");
+ return;
+ }
+ };
+
+ tokio::spawn(async move {
+ let res = future.await;
+
+ if let Err(e) = res {
+ log::error!("execute LSP command: {}", e);
+ }
+ });
+}
+
#[derive(Debug)]
pub struct ApplyEditError {
pub kind: ApplyEditErrorKind,
@@ -841,113 +832,136 @@ pub enum ApplyEditErrorKind {
// InvalidEdit,
}
-impl Display for ApplyEditErrorKind {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+impl ToString for ApplyEditErrorKind {
+ fn to_string(&self) -> String {
match self {
- ApplyEditErrorKind::DocumentChanged => f.write_str("document has changed"),
- ApplyEditErrorKind::FileNotFound => f.write_str("file not found"),
- ApplyEditErrorKind::UnknownURISchema => f.write_str("URI schema not supported"),
- ApplyEditErrorKind::IoError(err) => f.write_str(&format!("{err}")),
+ ApplyEditErrorKind::DocumentChanged => "document has changed".to_string(),
+ ApplyEditErrorKind::FileNotFound => "file not found".to_string(),
+ ApplyEditErrorKind::UnknownURISchema => "URI schema not supported".to_string(),
+ ApplyEditErrorKind::IoError(err) => err.to_string(),
}
}
}
/// Precondition: `locations` should be non-empty.
-fn goto_impl(editor: &mut Editor, compositor: &mut Compositor, locations: Vec<Location>) {
+fn goto_impl(
+ editor: &mut Editor,
+ compositor: &mut Compositor,
+ locations: Vec<lsp::Location>,
+ offset_encoding: OffsetEncoding,
+) {
let cwdir = helix_stdx::env::current_working_dir();
match locations.as_slice() {
[location] => {
- jump_to_location(editor, location, Action::Replace);
+ jump_to_location(editor, location, offset_encoding, Action::Replace);
}
[] => unreachable!("`locations` should be non-empty for `goto_impl`"),
_locations => {
let columns = [ui::PickerColumn::new(
"location",
- |item: &Location, cwdir: &std::path::PathBuf| {
- let path = if let Some(path) = item.uri.as_path() {
- path.strip_prefix(cwdir).unwrap_or(path).to_string_lossy()
+ |item: &lsp::Location, cwdir: &std::path::PathBuf| {
+ // The preallocation here will overallocate a few characters since it will account for the
+ // URL's scheme, which is not used most of the time since that scheme will be "file://".
+ // Those extra chars will be used to avoid allocating when writing the line number (in the
+ // common case where it has 5 digits or less, which should be enough for a cast majority
+ // of usages).
+ let mut res = String::with_capacity(item.uri.as_str().len());
+
+ if item.uri.scheme() == "file" {
+ // With the preallocation above and UTF-8 paths already, this closure will do one (1)
+ // allocation, for `to_file_path`, else there will be two (2), with `to_string_lossy`.
+ if let Ok(path) = item.uri.to_file_path() {
+ // We don't convert to a `helix_core::Uri` here because we've already checked the scheme.
+ // This path won't be normalized but it's only used for display.
+ res.push_str(
+ &path.strip_prefix(cwdir).unwrap_or(&path).to_string_lossy(),
+ );
+ }
} else {
- item.uri.to_string().into()
- };
+ // Never allocates since we declared the string with this capacity already.
+ res.push_str(item.uri.as_str());
+ }
- format!("{path}:{}", item.range.start.line + 1).into()
+ // Most commonly, this will not allocate, especially on Unix systems where the root prefix
+ // is a simple `/` and not `C:\` (with whatever drive letter)
+ write!(&mut res, ":{}", item.range.start.line + 1)
+ .expect("Will only failed if allocating fail");
+ res.into()
},
)];
- let picker = Picker::new(columns, 0, locations, cwdir, |cx, location, action| {
- jump_to_location(cx.editor, location, action)
+ let picker = Picker::new(columns, 0, locations, cwdir, move |cx, location, action| {
+ jump_to_location(cx.editor, location, offset_encoding, action)
})
- .with_preview(|_editor, location| location_to_file_location(location));
+ .with_preview(move |_editor, location| {
+ use crate::ui::picker::PathOrId;
+
+ let lines = Some((
+ location.range.start.line as usize,
+ location.range.end.line as usize,
+ ));
+
+ // TODO: we should avoid allocating by doing the Uri conversion ahead of time.
+ //
+ // To do this, introduce a `Location` type in `helix-core` that reuses the core
+ // `Uri` type instead of the LSP `Url` type and replaces the LSP `Range` type.
+ // Refactor the callers of `goto_impl` to pass iterators that translate the
+ // LSP location type to the custom one in core, or have them collect and pass
+ // `Vec<Location>`s. Replace the `uri_to_file_location` function with
+ // `location_to_file_location` that takes only `&helix_core::Location` as
+ // parameters.
+ //
+ // By doing this we can also eliminate the duplicated URI info in the
+ // `SymbolInformationItem` type and introduce a custom Symbol type in `helix-core`
+ // which will be reused in the future for tree-sitter based symbol pickers.
+ let path = Uri::try_from(&location.uri).ok()?.as_path_buf()?;
+ #[allow(deprecated)]
+ Some((PathOrId::from_path_buf(path), lines))
+ });
compositor.push(Box::new(overlaid(picker)));
}
}
}
+fn to_locations(definitions: Option<lsp::GotoDefinitionResponse>) -> Vec<lsp::Location> {
+ match definitions {
+ Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location],
+ Some(lsp::GotoDefinitionResponse::Array(locations)) => locations,
+ Some(lsp::GotoDefinitionResponse::Link(locations)) => locations
+ .into_iter()
+ .map(|location_link| lsp::Location {
+ uri: location_link.target_uri,
+ range: location_link.target_range,
+ })
+ .collect(),
+ None => Vec::new(),
+ }
+}
+
fn goto_single_impl<P, F>(cx: &mut Context, feature: LanguageServerFeature, request_provider: P)
where
P: Fn(&Client, lsp::Position, lsp::TextDocumentIdentifier) -> Option<F>,
- F: Future<Output = helix_lsp::Result<Option<lsp::GotoDefinitionResponse>>> + 'static + Send,
+ F: Future<Output = helix_lsp::Result<serde_json::Value>> + 'static + Send,
{
- let (view, doc) = current_ref!(cx.editor);
- let mut futures: FuturesOrdered<_> = doc
- .language_servers_with_feature(feature)
- .map(|language_server| {
- 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();
- async move { anyhow::Ok((future.await?, offset_encoding)) }
- })
- .collect();
+ let (view, doc) = current!(cx.editor);
- cx.jobs.callback(async move {
- let mut locations = Vec::new();
- while let Some(response) = futures.next().await {
- match response {
- Ok((response, offset_encoding)) => match response {
- Some(lsp::GotoDefinitionResponse::Scalar(lsp_location)) => {
- locations.extend(lsp_location_to_location(lsp_location, offset_encoding));
- }
- Some(lsp::GotoDefinitionResponse::Array(lsp_locations)) => {
- locations.extend(lsp_locations.into_iter().flat_map(|location| {
- lsp_location_to_location(location, offset_encoding)
- }));
- }
- Some(lsp::GotoDefinitionResponse::Link(lsp_locations)) => {
- locations.extend(
- lsp_locations
- .into_iter()
- .map(|location_link| {
- lsp::Location::new(
- location_link.target_uri,
- location_link.target_range,
- )
- })
- .flat_map(|location| {
- lsp_location_to_location(location, offset_encoding)
- }),
- );
- }
- None => (),
- },
- Err(err) => log::error!("Error requesting locations: {err}"),
- }
- }
- let call = move |editor: &mut Editor, compositor: &mut Compositor| {
- if locations.is_empty() {
- editor.set_error(match feature {
- LanguageServerFeature::GotoDeclaration => "No declaration found.",
- LanguageServerFeature::GotoDefinition => "No definition found.",
- LanguageServerFeature::GotoTypeDefinition => "No type definition found.",
- LanguageServerFeature::GotoImplementation => "No implementation found.",
- _ => "No location found.",
- });
+ let language_server = language_server_with_feature!(cx.editor, doc, feature);
+ 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,
+ move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
+ let items = to_locations(response);
+ if items.is_empty() {
+ editor.set_error("No definition found.");
} else {
- goto_impl(editor, compositor, locations);
+ goto_impl(editor, compositor, items, offset_encoding);
}
- };
- Ok(Callback::EditorCompositor(Box::new(call)))
- });
+ },
+ );
}
pub fn goto_declaration(cx: &mut Context) {
@@ -984,47 +998,34 @@ pub fn goto_implementation(cx: &mut Context) {
pub fn goto_reference(cx: &mut Context) {
let config = cx.editor.config();
- let (view, doc) = current_ref!(cx.editor);
+ let (view, doc) = current!(cx.editor);
- let mut futures: FuturesOrdered<_> = doc
- .language_servers_with_feature(LanguageServerFeature::GotoReference)
- .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,
- )
- .unwrap();
- async move { anyhow::Ok((future.await?, offset_encoding)) }
- })
- .collect();
+ // TODO could probably support multiple language servers,
+ // not sure if there's a real practical use case for this though
+ let language_server =
+ language_server_with_feature!(cx.editor, doc, LanguageServerFeature::GotoReference);
+ 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,
+ )
+ .unwrap();
- cx.jobs.callback(async move {
- let mut locations = Vec::new();
- while let Some(response) = futures.next().await {
- match response {
- Ok((lsp_locations, offset_encoding)) => locations.extend(
- lsp_locations
- .into_iter()
- .flatten()
- .flat_map(|location| lsp_location_to_location(location, offset_encoding)),
- ),
- Err(err) => log::error!("Error requesting references: {err}"),
- }
- }
- let call = move |editor: &mut Editor, compositor: &mut Compositor| {
- if locations.is_empty() {
+ cx.callback(
+ future,
+ move |editor, compositor, response: Option<Vec<lsp::Location>>| {
+ let items = response.unwrap_or_default();
+ if items.is_empty() {
editor.set_error("No references found.");
} else {
- goto_impl(editor, compositor, locations);
+ goto_impl(editor, compositor, items, offset_encoding);
}
- };
- Ok(Callback::EditorCompositor(Box::new(call)))
- });
+ },
+ );
}
pub fn signature_help(cx: &mut Context) {
@@ -1034,59 +1035,54 @@ pub fn signature_help(cx: &mut Context) {
}
pub fn hover(cx: &mut Context) {
- use ui::lsp::hover::Hover;
-
let (view, doc) = current!(cx.editor);
- if doc
- .language_servers_with_feature(LanguageServerFeature::Hover)
- .count()
- == 0
- {
- cx.editor
- .set_error("No configured language server supports hover");
- return;
- }
- let mut seen_language_servers = HashSet::new();
- let mut futures: FuturesOrdered<_> = doc
- .language_servers_with_feature(LanguageServerFeature::Hover)
- .filter(|ls| seen_language_servers.insert(ls.id()))
- .map(|language_server| {
- let server_name = language_server.name().to_string();
- // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier
- let pos = doc.position(view.id, language_server.offset_encoding());
- let request = language_server
- .text_document_hover(doc.identifier(), pos, None)
- .unwrap();
-
- async move { anyhow::Ok((server_name, request.await?)) }
- })
- .collect();
+ // 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
+ let pos = doc.position(view.id, language_server.offset_encoding());
+ let future = language_server
+ .text_document_hover(doc.identifier(), pos, None)
+ .unwrap();
- cx.jobs.callback(async move {
- let mut hovers: Vec<(String, lsp::Hover)> = Vec::new();
+ cx.callback(
+ future,
+ move |editor, compositor, response: Option<lsp::Hover>| {
+ if let Some(hover) = response {
+ // hover.contents / .range <- used for visualizing
+
+ fn marked_string_to_markdown(contents: lsp::MarkedString) -> String {
+ match contents {
+ lsp::MarkedString::String(contents) => contents,
+ lsp::MarkedString::LanguageString(string) => {
+ if string.language == "markdown" {
+ string.value
+ } else {
+ format!("```{}\n{}\n```", string.language, string.value)
+ }
+ }
+ }
+ }
- while let Some(response) = futures.next().await {
- match response {
- Ok((server_name, Some(hover))) => hovers.push((server_name, hover)),
- Ok(_) => (),
- Err(err) => log::error!("Error requesting hover: {err}"),
- }
- }
+ let contents = match hover.contents {
+ lsp::HoverContents::Scalar(contents) => marked_string_to_markdown(contents),
+ lsp::HoverContents::Array(contents) => contents
+ .into_iter()
+ .map(marked_string_to_markdown)
+ .collect::<Vec<_>>()
+ .join("\n\n"),
+ lsp::HoverContents::Markup(contents) => contents.value,
+ };
- let call = move |editor: &mut Editor, compositor: &mut Compositor| {
- if hovers.is_empty() {
- editor.set_status("No hover results available.");
- return;
- }
+ // skip if contents empty
- // create new popup
- let contents = Hover::new(hovers, editor.syn_loader.clone());
- let popup = Popup::new(Hover::ID, contents).auto_close(true);
- compositor.replace_or_push(Hover::ID, popup);
- };
- Ok(Callback::EditorCompositor(Box::new(call)))
- });
+ let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
+ let popup = Popup::new("hover", contents).auto_close(true);
+ compositor.replace_or_push("hover", popup);
+ }
+ },
+ );
}
pub fn rename_symbol(cx: &mut Context) {
@@ -1146,7 +1142,7 @@ pub fn rename_symbol(cx: &mut Context) {
let Some(language_server) = doc
.language_servers_with_feature(LanguageServerFeature::RenameSymbol)
- .find(|ls| language_server_id.is_none_or(|id| id == ls.id()))
+ .find(|ls| language_server_id.map_or(true, |id| id == ls.id()))
else {
cx.editor
.set_error("No configured language server supports symbol renaming");
@@ -1161,9 +1157,7 @@ pub fn rename_symbol(cx: &mut Context) {
match block_on(future) {
Ok(edits) => {
- let _ = cx
- .editor
- .apply_workspace_edit(offset_encoding, &edits.unwrap_or_default());
+ let _ = cx.editor.apply_workspace_edit(offset_encoding, &edits);
}
Err(err) => cx.editor.set_error(err.to_string()),
}
@@ -1305,8 +1299,7 @@ fn compute_inlay_hints_for_view(
// 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(doc.view_offset(view_id).anchor.min(doc_text.len_chars()));
+ 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))
@@ -1320,7 +1313,7 @@ fn compute_inlay_hints_for_view(
if !doc.inlay_hints_oudated
&& doc
.inlay_hints(view_id)
- .is_some_and(|dih| dih.id == new_doc_inlay_hints_id)
+ .map_or(false, |dih| dih.id == new_doc_inlay_hints_id)
{
return None;
}
@@ -1366,7 +1359,7 @@ fn compute_inlay_hints_for_view(
// Most language servers will already send them sorted but ensure this is the case to
// avoid errors on our end.
- hints.sort_by_key(|inlay_hint| inlay_hint.position);
+ hints.sort_unstable_by_key(|inlay_hint| inlay_hint.position);
let mut padding_before_inlay_hints = Vec::new();
let mut type_inlay_hints = Vec::new();
@@ -1375,7 +1368,6 @@ fn compute_inlay_hints_for_view(
let mut padding_after_inlay_hints = Vec::new();
let doc_text = doc.text();
- let inlay_hints_length_limit = doc.config.load().lsp.inlay_hints_length_limit;
for hint in hints {
let char_idx =
@@ -1386,7 +1378,7 @@ fn compute_inlay_hints_for_view(
None => continue,
};
- let mut label = match hint.label {
+ let label = match hint.label {
lsp::InlayHintLabel::String(s) => s,
lsp::InlayHintLabel::LabelParts(parts) => parts
.into_iter()
@@ -1394,31 +1386,6 @@ fn compute_inlay_hints_for_view(
.collect::<Vec<_>>()
.join(""),
};
- // Truncate the hint if too long
- if let Some(limit) = inlay_hints_length_limit {
- // Limit on displayed width
- use helix_core::unicode::{
- segmentation::UnicodeSegmentation, width::UnicodeWidthStr,
- };
-
- let width = label.width();
- let limit = limit.get().into();
- if width > limit {
- let mut floor_boundary = 0;
- let mut acc = 0;
- for (i, grapheme_cluster) in label.grapheme_indices(true) {
- acc += grapheme_cluster.width();
-
- if acc > limit {
- floor_boundary = i;
- break;
- }
- }
-
- label.truncate(floor_boundary);
- label.push('…');
- }
- }
let inlay_hints_vec = match hint.kind {
Some(lsp::InlayHintKind::TYPE) => &mut type_inlay_hints,