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.rs1970
1 files changed, 885 insertions, 1085 deletions
diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs
index 0494db3e..810e3adf 100644
--- a/helix-term/src/commands/lsp.rs
+++ b/helix-term/src/commands/lsp.rs
@@ -1,91 +1,96 @@
-use futures_util::{stream::FuturesOrdered, FutureExt};
use helix_lsp::{
block_on,
- lsp::{
- self, CodeAction, CodeActionOrCommand, CodeActionTriggerKind, DiagnosticSeverity,
- NumberOrString,
- },
- util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
- Client, LanguageServerId, OffsetEncoding,
+ lsp::{self, CodeAction, CodeActionOrCommand, DiagnosticSeverity, NumberOrString},
+ util::{diagnostic_to_lsp_diagnostic, lsp_pos_to_pos, lsp_range_to_range, range_to_lsp_range},
+ OffsetEncoding,
};
-use tokio_stream::StreamExt;
-use tui::{text::Span, widgets::Row};
+use tui::text::{Span, Spans};
-use super::{align_view, push_jump, Align, Context, Editor};
+use super::{align_view, push_jump, Align, Context, Editor, Open};
-use helix_core::{
- diagnostic::DiagnosticProvider, syntax::config::LanguageServerFeature,
- text_annotations::InlineAnnotation, Selection, Uri,
-};
-use helix_stdx::path;
-use helix_view::{
- document::{DocumentInlayHints, DocumentInlayHintsId},
- editor::Action,
- handlers::lsp::SignatureHelpInvoked,
- theme::Style,
- Document, View,
-};
+use helix_core::{path, Selection};
+use helix_view::{apply_transaction, document::Mode, editor::Action, theme::Style};
use crate::{
compositor::{self, Compositor},
- job::Callback,
- ui::{self, overlay::overlaid, FileLocation, Picker, Popup, PromptEvent},
+ ui::{
+ self, lsp::SignatureHelp, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent,
+ },
};
-use std::{cmp::Ordering, collections::HashSet, fmt::Display, future::Future, path::Path};
+use std::{
+ borrow::Cow, cmp::Ordering, collections::BTreeMap, fmt::Write, path::PathBuf, sync::Arc,
+};
-/// Gets the first language server that is attached to a document which supports a specific feature.
-/// If there is no configured language server that supports the feature, this displays a status message.
-/// Using this macro in a context where the editor automatically queries the LSP
-/// (instead of when the user explicitly does so via a keybind like `gd`)
-/// will spam the "No configured language server supports \<feature>" status message confusingly.
+/// 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_with_feature {
- ($editor:expr, $doc:expr, $feature:expr) => {{
- let language_server = $doc.language_servers_with_feature($feature).next();
- match language_server {
+macro_rules! language_server {
+ ($editor:expr, $doc:expr) => {
+ match $doc.language_server() {
Some(language_server) => language_server,
None => {
- $editor.set_error(format!(
- "No configured language server supports {}",
- $feature
- ));
+ $editor.set_status("Language server not active for current buffer");
return;
}
}
- }};
-}
-
-/// 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;
+impl ui::menu::Item for lsp::Location {
+ /// Current working directory.
+ type Data = PathBuf;
+
+ fn label(&self, cwdir: &Self::Data) -> Spans {
+ // 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(self.uri.as_str().len());
+
+ if self.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`.
+ let mut write_path_to_res = || -> Option<()> {
+ let path = self.uri.to_file_path().ok()?;
+ res.push_str(&path.strip_prefix(cwdir).unwrap_or(&path).to_string_lossy());
+ Some(())
+ };
+ write_path_to_res();
+ } else {
+ // Never allocates since we declared the string with this capacity already.
+ res.push_str(self.uri.as_str());
}
- };
- Some(Location {
- uri,
- range: location.range,
- offset_encoding,
- })
+
+ // 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, ":{}", self.range.start.line)
+ .expect("Will only failed if allocating fail");
+ res.into()
+ }
}
-struct SymbolInformationItem {
- location: Location,
- symbol: lsp::SymbolInformation,
+impl ui::menu::Item for lsp::SymbolInformation {
+ /// Path to currently focussed document
+ type Data = Option<lsp::Url>;
+
+ fn label(&self, current_doc_path: &Self::Data) -> Spans {
+ if current_doc_path.as_ref() == Some(&self.location.uri) {
+ self.name.as_str().into()
+ } else {
+ match self.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()
+ }
+ Err(_) => format!("{} ({})", &self.name, &self.location.uri).into(),
+ }
+ }
+ }
}
struct DiagnosticStyles {
@@ -96,102 +101,148 @@ struct DiagnosticStyles {
}
struct PickerDiagnostic {
- location: Location,
+ url: lsp::Url,
diag: lsp::Diagnostic,
}
-fn location_to_file_location(location: &Location) -> Option<FileLocation<'_>> {
- let path = location.uri.as_path()?;
+impl ui::menu::Item for PickerDiagnostic {
+ type Data = (DiagnosticStyles, DiagnosticsFormat);
+
+ fn label(&self, (styles, format): &Self::Data) -> Spans {
+ let mut style = self
+ .diag
+ .severity
+ .map(|s| match s {
+ DiagnosticSeverity::HINT => styles.hint,
+ DiagnosticSeverity::INFORMATION => styles.info,
+ DiagnosticSeverity::WARNING => styles.warning,
+ DiagnosticSeverity::ERROR => styles.error,
+ _ => Style::default(),
+ })
+ .unwrap_or_default();
+
+ // remove background as it is distracting in the picker list
+ style.bg = None;
+
+ let code: Cow<'_, str> = self
+ .diag
+ .code
+ .as_ref()
+ .map(|c| match c {
+ NumberOrString::Number(n) => n.to_string().into(),
+ NumberOrString::String(s) => s.as_str().into(),
+ })
+ .unwrap_or_default();
+
+ let path = match format {
+ DiagnosticsFormat::HideSourcePath => String::new(),
+ DiagnosticsFormat::ShowSourcePath => {
+ let path = path::get_truncated_path(self.url.path());
+ format!("{}: ", path.to_string_lossy())
+ }
+ };
+
+ Spans::from(vec![
+ Span::raw(path),
+ Span::styled(&self.diag.message, style),
+ Span::styled(code, style),
+ ])
+ }
+}
+
+fn location_to_file_location(location: &lsp::Location) -> FileLocation {
+ let path = location.uri.to_file_path().unwrap();
let line = Some((
location.range.start.line as usize,
location.range.end.line as usize,
));
- Some((path.into(), line))
-}
-
-fn jump_to_location(editor: &mut Editor, location: &Location, 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;
- };
- jump_to_position(
- editor,
- path,
- location.range,
- location.offset_encoding,
- action,
- );
+ (path.into(), line)
}
-fn jump_to_position(
+// TODO: share with symbol picker(symbol.location)
+fn jump_to_location(
editor: &mut Editor,
- path: &Path,
- range: lsp::Range,
+ location: &lsp::Location,
offset_encoding: OffsetEncoding,
action: Action,
) {
- let doc = match editor.open(path, action) {
- Ok(id) => doc_mut!(editor, &id),
- Err(err) => {
- let err = format!("failed to open path: {:?}: {:?}", path, err);
+ let (view, doc) = current!(editor);
+ push_jump(view, doc);
+
+ 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;
}
};
- let view = view_mut!(editor);
+ match editor.open(&path, action) {
+ Ok(_) => (),
+ Err(err) => {
+ let err = format!("failed to open path: {:?}: {:?}", location.uri, err);
+ editor.set_error(err);
+ return;
+ }
+ }
+ let (view, doc) = current!(editor);
+ let definition_pos = location.range.start;
// TODO: convert inside server
- let new_range = if let Some(new_range) = lsp_range_to_range(doc.text(), range, offset_encoding)
+ let new_pos = if let Some(new_pos) = lsp_pos_to_pos(doc.text(), definition_pos, offset_encoding)
{
- new_range
+ new_pos
} else {
- log::warn!("lsp position out of bounds - {:?}", range);
return;
};
- // we flip the range so that the cursor sits on the start of the symbol
- // (for example start of the function).
- doc.set_selection(view.id, Selection::single(new_range.head, new_range.anchor));
- if action.align_view(view, doc.id()) {
- align_view(doc, view, Align::Center);
- }
+ doc.set_selection(view.id, Selection::point(new_pos));
+ align_view(doc, view, Align::Center);
}
-fn display_symbol_kind(kind: lsp::SymbolKind) -> &'static str {
- match kind {
- lsp::SymbolKind::FILE => "file",
- lsp::SymbolKind::MODULE => "module",
- lsp::SymbolKind::NAMESPACE => "namespace",
- lsp::SymbolKind::PACKAGE => "package",
- lsp::SymbolKind::CLASS => "class",
- lsp::SymbolKind::METHOD => "method",
- lsp::SymbolKind::PROPERTY => "property",
- lsp::SymbolKind::FIELD => "field",
- lsp::SymbolKind::CONSTRUCTOR => "construct",
- lsp::SymbolKind::ENUM => "enum",
- lsp::SymbolKind::INTERFACE => "interface",
- lsp::SymbolKind::FUNCTION => "function",
- lsp::SymbolKind::VARIABLE => "variable",
- lsp::SymbolKind::CONSTANT => "constant",
- lsp::SymbolKind::STRING => "string",
- lsp::SymbolKind::NUMBER => "number",
- lsp::SymbolKind::BOOLEAN => "boolean",
- lsp::SymbolKind::ARRAY => "array",
- lsp::SymbolKind::OBJECT => "object",
- lsp::SymbolKind::KEY => "key",
- lsp::SymbolKind::NULL => "null",
- lsp::SymbolKind::ENUM_MEMBER => "enummem",
- lsp::SymbolKind::STRUCT => "struct",
- lsp::SymbolKind::EVENT => "event",
- lsp::SymbolKind::OPERATOR => "operator",
- lsp::SymbolKind::TYPE_PARAMETER => "typeparam",
- _ => {
- log::warn!("Unknown symbol kind: {:?}", kind);
- ""
- }
- }
+fn sym_picker(
+ symbols: Vec<lsp::SymbolInformation>,
+ current_path: Option<lsp::Url>,
+ offset_encoding: OffsetEncoding,
+) -> FilePicker<lsp::SymbolInformation> {
+ // TODO: drop current_path comparison and instead use workspace: bool flag?
+ FilePicker::new(
+ symbols,
+ current_path.clone(),
+ move |cx, symbol, 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;
+ let path = match uri.to_file_path() {
+ Ok(path) => path,
+ Err(_) => {
+ let err = format!("unable to convert URI to filepath: {}", uri);
+ cx.editor.set_error(err);
+ return;
+ }
+ };
+ if let Err(err) = cx.editor.open(&path, action) {
+ let err = format!("failed to open document: {}: {}", uri, err);
+ log::error!("{}", err);
+ cx.editor.set_error(err);
+ return;
+ }
+ }
+
+ let (view, doc) = current!(cx.editor);
+
+ if let Some(range) =
+ lsp_range_to_range(doc.text(), symbol.location.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, symbol| Some(location_to_file_location(&symbol.location)),
+ )
+ .truncate_start(false)
}
#[derive(Copy, Clone, PartialEq)]
@@ -200,44 +251,27 @@ enum DiagnosticsFormat {
HideSourcePath,
}
-type DiagnosticsPicker = Picker<PickerDiagnostic, DiagnosticStyles>;
-
fn diag_picker(
cx: &Context,
- diagnostics: impl IntoIterator<Item = (Uri, Vec<(lsp::Diagnostic, DiagnosticProvider)>)>,
+ diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
+ current_path: Option<lsp::Url>,
format: DiagnosticsFormat,
-) -> DiagnosticsPicker {
+ offset_encoding: OffsetEncoding,
+) -> FilePicker<PickerDiagnostic> {
// TODO: drop current_path comparison and instead use workspace: bool flag?
// flatten the map to a vec of (url, diag) pairs
let mut flat_diag = Vec::new();
- for (uri, diags) in diagnostics {
+ for (url, 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))
- {
- flat_diag.push(PickerDiagnostic {
- location: Location {
- uri: uri.clone(),
- range: diag.range,
- offset_encoding: ls.offset_encoding(),
- },
- diag,
- });
- }
+ for diag in diags {
+ flat_diag.push(PickerDiagnostic {
+ url: url.clone(),
+ diag,
+ });
}
}
- 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"),
@@ -245,352 +279,161 @@ fn diag_picker(
error: cx.editor.theme.get("error"),
};
- let mut columns = vec![
- ui::PickerColumn::new(
- "severity",
- |item: &PickerDiagnostic, styles: &DiagnosticStyles| {
- match item.diag.severity {
- Some(DiagnosticSeverity::HINT) => Span::styled("HINT", styles.hint),
- Some(DiagnosticSeverity::INFORMATION) => Span::styled("INFO", styles.info),
- Some(DiagnosticSeverity::WARNING) => Span::styled("WARN", styles.warning),
- Some(DiagnosticSeverity::ERROR) => Span::styled("ERROR", styles.error),
- _ => Span::raw(""),
- }
- .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(),
- Some(NumberOrString::String(s)) => s.as_str().into(),
- None => "".into(),
+ FilePicker::new(
+ flat_diag,
+ (styles, format),
+ move |cx, PickerDiagnostic { url, diag }, action| {
+ if current_path.as_ref() == Some(url) {
+ let (view, doc) = current!(cx.editor);
+ push_jump(view, doc);
+ } else {
+ let path = url.to_file_path().unwrap();
+ cx.editor.open(&path, action).expect("editor.open failed");
}
- }),
- ui::PickerColumn::new("message", |item: &PickerDiagnostic, _| {
- item.diag.message.as_str().into()
- }),
- ];
- let mut primary_column = 3; // message
-
- if format == DiagnosticsFormat::ShowSourcePath {
- columns.insert(
- // between message code and message
- 3,
- ui::PickerColumn::new("path", |item: &PickerDiagnostic, _| {
- if let Some(path) = item.location.uri.as_path() {
- path::get_truncated_path(path)
- .to_string_lossy()
- .to_string()
- .into()
- } else {
- Default::default()
- }
- }),
- );
- primary_column += 1;
- }
- Picker::new(
- columns,
- primary_column,
- flat_diag,
- styles,
- move |cx, diag, action| {
- jump_to_location(cx.editor, &diag.location, action);
let (view, doc) = current!(cx.editor);
- view.diagnostics_handler
- .immediately_show_diagnostic(doc, view.id);
+
+ 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 }| {
+ let location = lsp::Location::new(url.clone(), diag.range);
+ Some(location_to_file_location(&location))
},
)
- .with_preview(move |_editor, diag| location_to_file_location(&diag.location))
.truncate_start(false)
}
pub fn symbol_picker(cx: &mut Context) {
fn nested_to_flat(
- list: &mut Vec<SymbolInformationItem>,
+ list: &mut Vec<lsp::SymbolInformation>,
file: &lsp::TextDocumentIdentifier,
- uri: &Uri,
symbol: lsp::DocumentSymbol,
- offset_encoding: OffsetEncoding,
) {
#[allow(deprecated)]
- 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,
- },
- location: Location {
- uri: uri.clone(),
- range: symbol.selection_range,
- offset_encoding,
- },
+ 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,
});
for child in symbol.children.into_iter().flatten() {
- nested_to_flat(list, file, uri, child, offset_encoding);
+ nested_to_flat(list, file, child);
}
}
let doc = doc!(cx.editor);
- let mut seen_language_servers = HashSet::new();
+ let language_server = language_server!(cx.editor, doc);
+ let current_url = doc.url();
+ let offset_encoding = language_server.offset_encoding();
- let mut futures: FuturesOrdered<_> = doc
- .language_servers_with_feature(LanguageServerFeature::DocumentSymbols)
- .filter(|ls| seen_language_servers.insert(ls.id()))
- .map(|language_server| {
- let request = language_server.document_symbols(doc.identifier()).unwrap();
- let offset_encoding = language_server.offset_encoding();
- let doc_id = doc.identifier();
- let doc_uri = doc
- .uri()
- .expect("docs with active language servers must be backed by paths");
-
- async move {
- let symbols = match request.await? {
- Some(symbols) => symbols,
- None => return anyhow::Ok(vec![]),
- };
+ 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
- .into_iter()
- .map(|symbol| SymbolInformationItem {
- location: Location {
- uri: doc_uri.clone(),
- range: symbol.location.range,
- offset_encoding,
- },
- symbol,
- })
- .collect(),
+ 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_id,
- &doc_uri,
- symbol,
- offset_encoding,
- )
+ nested_to_flat(&mut flat_symbols, &doc.identifier(), symbol)
}
flat_symbols
}
};
- Ok(symbols)
- }
- })
- .collect();
-
- if futures.is_empty() {
- cx.editor
- .set_error("No configured language server supports document symbols");
- return;
- }
- 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}"),
+ let picker = sym_picker(symbols, current_url, offset_encoding);
+ compositor.push(Box::new(overlayed(picker)))
}
- }
- let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
- let columns = [
- ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| {
- display_symbol_kind(item.symbol.kind).into()
- }),
- // Some symbols in the document symbol picker may have a URI that isn't
- // the current file. It should be rare though, so we concatenate that
- // URI in with the symbol name in this picker.
- 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(
- columns,
- 1, // name column
- symbols,
- (),
- move |cx, item, action| {
- jump_to_location(cx.editor, &item.location, action);
- },
- )
- .with_preview(move |_editor, item| location_to_file_location(&item.location))
- .truncate_start(false);
-
- compositor.push(Box::new(overlaid(picker)))
- };
-
- Ok(Callback::EditorCompositor(Box::new(call)))
- });
+ },
+ )
}
pub fn workspace_symbol_picker(cx: &mut Context) {
- use crate::ui::picker::Injector;
-
let doc = doc!(cx.editor);
- if doc
- .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
- .count()
- == 0
- {
- cx.editor
- .set_error("No configured language server supports workspace symbols");
- return;
- }
-
- let get_symbols = |pattern: &str, editor: &mut Editor, _data, injector: &Injector<_, _>| {
- let doc = doc!(editor);
- let mut seen_language_servers = HashSet::new();
- let mut futures: FuturesOrdered<_> = doc
- .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
- .filter(|ls| seen_language_servers.insert(ls.id()))
- .map(|language_server| {
- let request = language_server
- .workspace_symbols(pattern.to_string())
- .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 {
- uri,
- range: symbol.location.range,
- offset_encoding,
- },
- symbol,
- })
- })
- .collect();
-
- anyhow::Ok(response)
- }
- })
- .collect();
-
- if futures.is_empty() {
- editor.set_error("No configured language server supports workspace symbols");
- }
-
- 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}"),
- }
- }
- Ok(())
+ 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;
}
- .boxed()
};
- let columns = [
- ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| {
- display_symbol_kind(item.symbol.kind).into()
- }),
- ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| {
- 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() {
- path::get_relative_path(path)
- .to_string_lossy()
- .to_string()
- .into()
- } else {
- item.symbol.location.uri.to_string().into()
+
+ cx.callback(
+ future,
+ move |_editor, compositor, response: Option<Vec<lsp::SymbolInformation>>| {
+ if let Some(symbols) = response {
+ let picker = sym_picker(symbols, current_url, offset_encoding);
+ compositor.push(Box::new(overlayed(picker)))
}
- }),
- ];
-
- let picker = Picker::new(
- columns,
- 1, // name column
- [],
- (),
- move |cx, item, action| {
- jump_to_location(cx.editor, &item.location, action);
},
)
- .with_preview(|_editor, item| location_to_file_location(&item.location))
- .with_dynamic_query(get_symbols, None)
- .truncate_start(false);
-
- cx.push_layer(Box::new(overlaid(picker)));
}
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);
- cx.push_layer(Box::new(overlaid(picker)));
+ 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
+ .get(&current_url)
+ .cloned()
+ .unwrap_or_default();
+ let picker = diag_picker(
+ cx,
+ [(current_url.clone(), diagnostics)].into(),
+ Some(current_url),
+ DiagnosticsFormat::HideSourcePath,
+ offset_encoding,
+ );
+ cx.push_layer(Box::new(overlayed(picker)));
}
}
pub fn workspace_diagnostics_picker(cx: &mut Context) {
- // TODO not yet filtered by LanguageServerFeature, need to do something similar as Document::shown_diagnostics here for all open documents
+ 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();
let diagnostics = cx.editor.diagnostics.clone();
- let picker = diag_picker(cx, diagnostics, DiagnosticsFormat::ShowSourcePath);
- cx.push_layer(Box::new(overlaid(picker)));
-}
-
-struct CodeActionOrCommandItem {
- lsp_item: lsp::CodeActionOrCommand,
- language_server_id: LanguageServerId,
+ let picker = diag_picker(
+ cx,
+ diagnostics,
+ current_url,
+ DiagnosticsFormat::ShowSourcePath,
+ offset_encoding,
+ );
+ cx.push_layer(Box::new(overlayed(picker)));
}
-impl ui::menu::Item for CodeActionOrCommandItem {
+impl ui::menu::Item for lsp::CodeActionOrCommand {
type Data = ();
- fn format(&self, _data: &Self::Data) -> Row<'_> {
- match &self.lsp_item {
+ fn label(&self, _data: &Self::Data) -> Spans {
+ match self {
lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(),
lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
}
@@ -604,7 +447,7 @@ impl ui::menu::Item for CodeActionOrCommandItem {
///
/// While the `kind` field is defined as open ended in the LSP spec (any value may be used)
/// in practice a closed set of common values (mostly suggested in the LSP spec) are used.
-/// VSCode displays each of these categories separately (separated by a heading in the codeactions picker)
+/// VSCode displays each of these categories seperatly (seperated by a heading in the codeactions picker)
/// to make them easier to navigate. Helix does not display these headings to the user.
/// However it does sort code actions by their categories to achieve the same order as the VScode picker,
/// just without the headings.
@@ -634,7 +477,7 @@ fn action_category(action: &CodeActionOrCommand) -> u32 {
}
}
-fn action_preferred(action: &CodeActionOrCommand) -> bool {
+fn action_prefered(action: &CodeActionOrCommand) -> bool {
matches!(
action,
CodeActionOrCommand::CodeAction(CodeAction {
@@ -657,39 +500,44 @@ 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 mut seen_language_servers = HashSet::new();
+ let range = range_to_lsp_range(doc.text(), selection_range, offset_encoding);
- let mut futures: FuturesOrdered<_> = doc
- .language_servers_with_feature(LanguageServerFeature::CodeAction)
- .filter(|ls| seen_language_servers.insert(ls.id()))
- // 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, language_server_id))
- })
- .map(|(request, ls_id)| async move {
- let Some(mut actions) = request.await? else {
- return anyhow::Ok(Vec::new());
+ 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,
+ },
+ ) {
+ 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 actions = match response {
+ Some(a) => a,
+ None => return,
};
// remove disabled code actions
@@ -701,13 +549,18 @@ 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.
+ // Many details are modeled after vscode because langauge servers are usually tested against it.
// VScode sorts the codeaction two times:
//
// First the codeactions that fix some diagnostics are moved to the front.
// If both codeactions fix some diagnostics (or both fix none) the codeaction
- // that is marked with `is_preferred` is shown first. The codeactions are then shown in separate
+ // that is marked with `is_preffered` is shown first. The codeactions are then shown in seperate
// submenus that only contain a certain category (see `action_category`) of actions.
//
// Below this done in in a single sorting step
@@ -729,516 +582,648 @@ pub fn code_action(cx: &mut Context) {
return order;
}
- // if one of the codeactions is marked as preferred show it first
+ // if one of the codeactions is marked as prefered show it first
// otherwise keep the original LSP sorting
- action_preferred(action1)
- .cmp(&action_preferred(action2))
+ action_prefered(action1)
+ .cmp(&action_prefered(action2))
.reverse()
});
- Ok(actions
- .into_iter()
- .map(|lsp_item| CodeActionOrCommandItem {
- lsp_item,
- language_server_id: ls_id,
- })
- .collect())
- })
- .collect();
-
- if futures.is_empty() {
- cx.editor
- .set_error("No configured language server supports code actions");
- return;
- }
-
- 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}"),
- }
- }
-
- 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| {
+ let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| {
if event != PromptEvent::Validate {
return;
}
// always present here
- let action = action.unwrap();
- let Some(language_server) = editor.language_server_by_id(action.language_server_id)
- else {
- editor.set_error("Language Server disappeared");
- return;
- };
- let offset_encoding = language_server.offset_encoding();
+ let code_action = code_action.unwrap();
- match &action.lsp_item {
+ match code_action {
lsp::CodeActionOrCommand::Command(command) => {
log::debug!("code action command: {:?}", command);
- editor.execute_lsp_command(command.clone(), action.language_server_id);
+ execute_lsp_command(editor, 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);
- }
- }
- }
- let resolved_code_action =
- resolved_code_action.as_ref().unwrap_or(code_action);
-
- if let Some(ref workspace_edit) = resolved_code_action.edit {
- let _ = editor.apply_workspace_edit(offset_encoding, workspace_edit);
+ if let Some(ref workspace_edit) = code_action.edit {
+ log::debug!("edit: {:?}", workspace_edit);
+ apply_workspace_edit(editor, offset_encoding, workspace_edit);
}
// 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, 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);
- };
-
- Ok(Callback::EditorCompositor(Box::new(call)))
- });
+ },
+ )
}
-#[derive(Debug)]
-pub struct ApplyEditError {
- pub kind: ApplyEditErrorKind,
- pub failed_change_idx: usize,
+impl ui::menu::Item for lsp::Command {
+ type Data = ();
+ fn label(&self, _data: &Self::Data) -> Spans {
+ self.title.as_str().into()
+ }
}
-#[derive(Debug)]
-pub enum ApplyEditErrorKind {
- DocumentChanged,
- FileNotFound,
- UnknownURISchema,
- IoError(std::io::Error),
- // TODO: check edits before applying and propagate failure
- // InvalidEdit,
+pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
+ let doc = doc!(editor);
+ let language_server = language_server!(editor, doc);
+
+ // the command is executed on the server and communicated back
+ // to the client asynchronously using workspace edits
+ let future = match 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);
+ }
+ });
}
-impl Display for ApplyEditErrorKind {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- 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}")),
+pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
+ use lsp::ResourceOp;
+ use std::fs;
+ match op {
+ ResourceOp::Create(op) => {
+ let path = op.uri.to_file_path().unwrap();
+ let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
+ !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
+ });
+ if ignore_if_exists && path.exists() {
+ Ok(())
+ } else {
+ // Create directory if it does not exist
+ if let Some(dir) = path.parent() {
+ if !dir.is_dir() {
+ fs::create_dir_all(dir)?;
+ }
+ }
+
+ fs::write(&path, [])
+ }
+ }
+ ResourceOp::Delete(op) => {
+ let path = op.uri.to_file_path().unwrap();
+ if path.is_dir() {
+ let recursive = op
+ .options
+ .as_ref()
+ .and_then(|options| options.recursive)
+ .unwrap_or(false);
+
+ if recursive {
+ fs::remove_dir_all(&path)
+ } else {
+ fs::remove_dir(&path)
+ }
+ } else if path.is_file() {
+ fs::remove_file(&path)
+ } else {
+ Ok(())
+ }
+ }
+ ResourceOp::Rename(op) => {
+ let from = op.old_uri.to_file_path().unwrap();
+ let to = op.new_uri.to_file_path().unwrap();
+ let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
+ !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
+ });
+ if ignore_if_exists && to.exists() {
+ Ok(())
+ } else {
+ fs::rename(from, &to)
+ }
}
}
}
-/// Precondition: `locations` should be non-empty.
-fn goto_impl(editor: &mut Editor, compositor: &mut Compositor, locations: Vec<Location>) {
- let cwdir = helix_stdx::env::current_working_dir();
+pub fn apply_workspace_edit(
+ editor: &mut Editor,
+ offset_encoding: OffsetEncoding,
+ workspace_edit: &lsp::WorkspaceEdit,
+) {
+ let mut apply_edits = |uri: &helix_lsp::Url, text_edits: Vec<lsp::TextEdit>| {
+ let path = match uri.to_file_path() {
+ Ok(path) => path,
+ Err(_) => {
+ let err = format!("unable to convert URI to filepath: {}", uri);
+ log::error!("{}", err);
+ editor.set_error(err);
+ return;
+ }
+ };
- match locations.as_slice() {
- [location] => {
- jump_to_location(editor, location, 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()
- } else {
- item.uri.to_string().into()
- };
-
- format!("{path}:{}", item.range.start.line + 1).into()
- },
- )];
+ let current_view_id = view!(editor).id;
+ let doc_id = match editor.open(&path, Action::Load) {
+ Ok(doc_id) => doc_id,
+ Err(err) => {
+ let err = format!("failed to open document: {}: {}", uri, err);
+ log::error!("{}", err);
+ editor.set_error(err);
+ return;
+ }
+ };
- let picker = Picker::new(columns, 0, locations, cwdir, |cx, location, action| {
- jump_to_location(cx.editor, location, action)
- })
- .with_preview(|_editor, location| location_to_file_location(location));
- compositor.push(Box::new(overlaid(picker)));
+ let doc = doc_mut!(editor, &doc_id);
+
+ // Need to determine a view for apply/append_changes_to_history
+ let selections = doc.selections();
+ let view_id = if selections.contains_key(&current_view_id) {
+ // use current if possible
+ current_view_id
+ } else {
+ // Hack: we take the first available view_id
+ selections
+ .keys()
+ .next()
+ .copied()
+ .expect("No view_id available")
+ };
+
+ let transaction = helix_lsp::util::generate_transaction_from_edits(
+ doc.text(),
+ text_edits,
+ offset_encoding,
+ );
+ let view = view_mut!(editor, view_id);
+ apply_transaction(&transaction, doc, view);
+ doc.append_changes_to_history(view);
+ };
+
+ if let Some(ref changes) = workspace_edit.changes {
+ log::debug!("workspace changes: {:?}", changes);
+ for (uri, text_edits) in changes {
+ let text_edits = text_edits.to_vec();
+ apply_edits(uri, text_edits)
}
+ return;
+ // Not sure if it works properly, it'll be safer to just panic here to avoid breaking some parts of code on which code actions will be used
+ // TODO: find some example that uses workspace changes, and test it
+ // for (url, edits) in changes.iter() {
+ // let file_path = url.origin().ascii_serialization();
+ // let file_path = std::path::PathBuf::from(file_path);
+ // let file = std::fs::File::open(file_path).unwrap();
+ // let mut text = Rope::from_reader(file).unwrap();
+ // let transaction = edits_to_changes(&text, edits);
+ // transaction.apply(&mut text);
+ // }
}
-}
-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,
-{
- 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();
-
- 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,
- )
+ if let Some(ref document_changes) = workspace_edit.document_changes {
+ match document_changes {
+ lsp::DocumentChanges::Edits(document_edits) => {
+ for document_edit in document_edits {
+ let edits = document_edit
+ .edits
+ .iter()
+ .map(|edit| match edit {
+ lsp::OneOf::Left(text_edit) => text_edit,
+ lsp::OneOf::Right(annotated_text_edit) => {
+ &annotated_text_edit.text_edit
+ }
+ })
+ .cloned()
+ .collect();
+ apply_edits(&document_edit.text_document.uri, edits);
+ }
+ }
+ lsp::DocumentChanges::Operations(operations) => {
+ log::debug!("document changes - operations: {:?}", operations);
+ for operation in operations {
+ match operation {
+ lsp::DocumentChangeOperation::Op(op) => {
+ apply_document_resource_op(op).unwrap();
+ }
+
+ lsp::DocumentChangeOperation::Edit(document_edit) => {
+ let edits = document_edit
+ .edits
+ .iter()
+ .map(|edit| match edit {
+ lsp::OneOf::Left(text_edit) => text_edit,
+ lsp::OneOf::Right(annotated_text_edit) => {
+ &annotated_text_edit.text_edit
+ }
})
- .flat_map(|location| {
- lsp_location_to_location(location, offset_encoding)
- }),
- );
+ .cloned()
+ .collect();
+ apply_edits(&document_edit.text_document.uri, edits);
+ }
}
- 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.",
- });
- } else {
- goto_impl(editor, compositor, locations);
- }
- };
- Ok(Callback::EditorCompositor(Box::new(call)))
- });
+ }
}
-pub fn goto_declaration(cx: &mut Context) {
- goto_single_impl(
- cx,
- LanguageServerFeature::GotoDeclaration,
- |ls, pos, doc_id| ls.goto_declaration(doc_id, pos, None),
- );
+fn goto_impl(
+ editor: &mut Editor,
+ compositor: &mut Compositor,
+ locations: Vec<lsp::Location>,
+ offset_encoding: OffsetEncoding,
+) {
+ let cwdir = std::env::current_dir().unwrap_or_default();
+
+ match locations.as_slice() {
+ [location] => {
+ jump_to_location(editor, location, offset_encoding, Action::Replace);
+ }
+ [] => {
+ editor.set_error("No definition found.");
+ }
+ _locations => {
+ let picker = FilePicker::new(
+ locations,
+ cwdir,
+ move |cx, location, action| {
+ jump_to_location(cx.editor, location, offset_encoding, action)
+ },
+ move |_editor, location| Some(location_to_file_location(location)),
+ );
+ compositor.push(Box::new(overlayed(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(),
+ }
}
pub fn goto_definition(cx: &mut Context) {
- goto_single_impl(
- cx,
- LanguageServerFeature::GotoDefinition,
- |ls, pos, doc_id| ls.goto_definition(doc_id, pos, None),
+ 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,
+ None => {
+ cx.editor
+ .set_error("Language server does not support goto-definition");
+ return;
+ }
+ };
+
+ cx.callback(
+ future,
+ move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
+ let items = to_locations(response);
+ goto_impl(editor, compositor, items, offset_encoding);
+ },
);
}
pub fn goto_type_definition(cx: &mut Context) {
- goto_single_impl(
- cx,
- LanguageServerFeature::GotoTypeDefinition,
- |ls, pos, doc_id| ls.goto_type_definition(doc_id, pos, None),
+ 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,
+ None => {
+ cx.editor
+ .set_error("Language server does not support goto-type-definition");
+ return;
+ }
+ };
+
+ cx.callback(
+ future,
+ move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
+ let items = to_locations(response);
+ goto_impl(editor, compositor, items, offset_encoding);
+ },
);
}
pub fn goto_implementation(cx: &mut Context) {
- goto_single_impl(
- cx,
- LanguageServerFeature::GotoImplementation,
- |ls, pos, doc_id| ls.goto_implementation(doc_id, pos, None),
+ 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,
+ None => {
+ cx.editor
+ .set_error("Language server does not support goto-implementation");
+ return;
+ }
+ };
+
+ cx.callback(
+ future,
+ move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
+ let items = to_locations(response);
+ goto_impl(editor, compositor, items, offset_encoding);
+ },
);
}
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 language_server = language_server!(cx.editor, doc);
+ let offset_encoding = language_server.offset_encoding();
- 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();
-
- 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 pos = doc.position(view.id, offset_encoding);
+
+ let future = match language_server.goto_reference(doc.identifier(), pos, None) {
+ Some(future) => future,
+ None => {
+ cx.editor
+ .set_error("Language server does not support goto-reference");
+ return;
}
- let call = move |editor: &mut Editor, compositor: &mut Compositor| {
- if locations.is_empty() {
- editor.set_error("No references found.");
- } else {
- goto_impl(editor, compositor, locations);
- }
- };
- Ok(Callback::EditorCompositor(Box::new(call)))
- });
+ };
+
+ cx.callback(
+ future,
+ move |editor, compositor, response: Option<Vec<lsp::Location>>| {
+ let items = response.unwrap_or_default();
+ goto_impl(editor, compositor, items, offset_encoding);
+ },
+ );
}
-pub fn signature_help(cx: &mut Context) {
- cx.editor
- .handlers
- .trigger_signature_help(SignatureHelpInvoked::Manual, cx.editor)
+#[derive(PartialEq, Eq)]
+pub enum SignatureHelpInvoked {
+ Manual,
+ Automatic,
}
-pub fn hover(cx: &mut Context) {
- use ui::lsp::hover::Hover;
+pub fn signature_help(cx: &mut Context) {
+ signature_help_impl(cx, SignatureHelpInvoked::Manual)
+}
+pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
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();
+ let was_manually_invoked = invoked == SignatureHelpInvoked::Manual;
+
+ let language_server = match doc.language_server() {
+ Some(language_server) => language_server,
+ None => {
+ // Do not show the message if signature help was invoked
+ // automatically on backspace, trigger characters, etc.
+ if was_manually_invoked {
+ cx.editor
+ .set_status("Language server not active for current buffer");
+ }
+ return;
+ }
+ };
+ let offset_encoding = language_server.offset_encoding();
- cx.jobs.callback(async move {
- let mut hovers: Vec<(String, lsp::Hover)> = Vec::new();
+ let pos = doc.position(view.id, offset_encoding);
- 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 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");
}
+ return;
}
+ };
- let call = move |editor: &mut Editor, compositor: &mut Compositor| {
- if hovers.is_empty() {
- editor.set_status("No hover results available.");
+ cx.callback(
+ future,
+ move |editor, compositor, response: Option<lsp::SignatureHelp>| {
+ let config = &editor.config();
+
+ if !(config.lsp.auto_signature_help
+ || SignatureHelp::visible_popup(compositor).is_some()
+ || was_manually_invoked)
+ {
return;
}
- // 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)))
- });
-}
-
-pub fn rename_symbol(cx: &mut Context) {
- fn get_prefill_from_word_boundary(editor: &Editor) -> String {
- let (view, doc) = current_ref!(editor);
- let text = doc.text().slice(..);
- let primary_selection = doc.selection(view.id).primary();
- if primary_selection.len() > 1 {
- primary_selection
- } else {
- use helix_core::textobject::{textobject_word, TextObject};
- textobject_word(text, primary_selection, TextObject::Inside, 1, false)
- }
- .fragment(text)
- .into()
- }
-
- fn get_prefill_from_lsp_response(
- editor: &Editor,
- offset_encoding: OffsetEncoding,
- response: Option<lsp::PrepareRenameResponse>,
- ) -> Result<String, &'static str> {
- match response {
- Some(lsp::PrepareRenameResponse::Range(range)) => {
- let text = doc!(editor).text();
-
- Ok(lsp_range_to_range(text, range, offset_encoding)
- .ok_or("lsp sent invalid selection range for rename")?
- .fragment(text.slice(..))
- .into())
- }
- Some(lsp::PrepareRenameResponse::RangeWithPlaceholder { placeholder, .. }) => {
- Ok(placeholder)
- }
- Some(lsp::PrepareRenameResponse::DefaultBehavior { .. }) => {
- Ok(get_prefill_from_word_boundary(editor))
+ // If the signature help invocation is automatic, don't show it outside of Insert Mode:
+ // 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 {
+ return;
}
- None => Err("lsp did not respond to prepare rename request"),
- }
- }
- fn create_rename_prompt(
- editor: &Editor,
- prefill: String,
- history_register: Option<char>,
- language_server_id: Option<LanguageServerId>,
- ) -> Box<ui::Prompt> {
- let prompt = ui::Prompt::new(
- "rename-to:".into(),
- history_register,
- ui::completers::none,
- move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
- if event != PromptEvent::Validate {
+ let response = match response {
+ // According to the spec the response should be None if there
+ // are no signatures, but some servers don't follow this.
+ Some(s) if !s.signatures.is_empty() => s,
+ _ => {
+ compositor.remove(SignatureHelp::ID);
return;
}
- let (view, doc) = current!(cx.editor);
+ };
+ let doc = doc!(editor);
+ let language = doc.language_name().unwrap_or("");
- let Some(language_server) = doc
- .language_servers_with_feature(LanguageServerFeature::RenameSymbol)
- .find(|ls| language_server_id.is_none_or(|id| id == ls.id()))
- else {
- cx.editor
- .set_error("No configured language server supports symbol renaming");
- return;
- };
+ let signature = match response
+ .signatures
+ .get(response.active_signature.unwrap_or(0) as usize)
+ {
+ Some(s) => s,
+ None => return,
+ };
+ let mut contents = SignatureHelp::new(
+ signature.label.clone(),
+ language.to_string(),
+ Arc::clone(&editor.syn_loader),
+ );
+
+ let signature_doc = if config.lsp.display_signature_help_docs {
+ signature.documentation.as_ref().map(|doc| match doc {
+ lsp::Documentation::String(s) => s.clone(),
+ lsp::Documentation::MarkupContent(markup) => markup.value.clone(),
+ })
+ } else {
+ None
+ };
- let offset_encoding = language_server.offset_encoding();
- let pos = doc.position(view.id, offset_encoding);
- let future = language_server
- .rename_symbol(doc.identifier(), pos, input.to_string())
- .unwrap();
-
- match block_on(future) {
- Ok(edits) => {
- let _ = cx
- .editor
- .apply_workspace_edit(offset_encoding, &edits.unwrap_or_default());
+ contents.set_signature_doc(signature_doc);
+
+ let active_param_range = || -> Option<(usize, usize)> {
+ let param_idx = signature
+ .active_parameter
+ .or(response.active_parameter)
+ .unwrap_or(0) as usize;
+ let param = signature.parameters.as_ref()?.get(param_idx)?;
+ match &param.label {
+ lsp::ParameterLabel::Simple(string) => {
+ let start = signature.label.find(string.as_str())?;
+ Some((start, start + string.len()))
+ }
+ lsp::ParameterLabel::LabelOffsets([start, end]) => {
+ // LS sends offsets based on utf-16 based string representation
+ // but highlighting in helix is done using byte offset.
+ use helix_core::str_utils::char_to_byte_idx;
+ let from = char_to_byte_idx(&signature.label, *start as usize);
+ let to = char_to_byte_idx(&signature.label, *end as usize);
+ Some((from, to))
}
- Err(err) => cx.editor.set_error(err.to_string()),
}
- },
- )
- .with_line(prefill, editor);
+ };
+ contents.set_active_param_range(active_param_range());
+
+ let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID);
+ let popup = Popup::new(SignatureHelp::ID, contents)
+ .position(old_popup.and_then(|p| p.get_position()))
+ .position_bias(Open::Above)
+ .ignore_escape_key(true);
+ compositor.replace_or_push(SignatureHelp::ID, popup);
+ },
+ );
+}
- Box::new(prompt)
- }
+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();
- let (view, doc) = current_ref!(cx.editor);
- let history_register = cx.register;
+ // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier
- if doc
- .language_servers_with_feature(LanguageServerFeature::RenameSymbol)
- .next()
- .is_none()
- {
- cx.editor
- .set_error("No configured language server supports symbol renaming");
- return;
- }
+ let pos = doc.position(view.id, offset_encoding);
- let language_server_with_prepare_rename_support = doc
- .language_servers_with_feature(LanguageServerFeature::RenameSymbol)
- .find(|ls| {
- matches!(
- ls.capabilities().rename_provider,
- Some(lsp::OneOf::Right(lsp::RenameOptions {
- prepare_provider: Some(true),
- ..
- }))
- )
- });
+ let future = match language_server.text_document_hover(doc.identifier(), pos, None) {
+ Some(future) => future,
+ None => {
+ cx.editor
+ .set_error("Language server does not support hover");
+ return;
+ }
+ };
- if let Some(language_server) = language_server_with_prepare_rename_support {
- let ls_id = language_server.id();
- let offset_encoding = language_server.offset_encoding();
- let pos = doc.position(view.id, offset_encoding);
- let future = language_server
- .prepare_rename(doc.identifier(), pos)
- .unwrap();
- cx.callback(
- future,
- move |editor, compositor, response: Option<lsp::PrepareRenameResponse>| {
- let prefill = match get_prefill_from_lsp_response(editor, offset_encoding, response)
- {
- Ok(p) => p,
- Err(e) => {
- editor.set_error(e);
- return;
+ 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)
+ }
+ }
}
+ }
+
+ 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 prompt = create_rename_prompt(editor, prefill, history_register, Some(ls_id));
+ // skip if contents empty
- compositor.push(prompt);
- },
- );
+ 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) {
+ let (view, doc) = current_ref!(cx.editor);
+ let text = doc.text().slice(..);
+ let primary_selection = doc.selection(view.id).primary();
+ let prefill = if primary_selection.len() > 1 {
+ primary_selection
} else {
- let prefill = get_prefill_from_word_boundary(cx.editor);
- let prompt = create_rename_prompt(cx.editor, prefill, history_register, None);
- cx.push_layer(prompt);
+ use helix_core::textobject::{textobject_word, TextObject};
+ textobject_word(text, primary_selection, TextObject::Inside, 1, false)
}
+ .fragment(text)
+ .into();
+ ui::prompt_with_input(
+ cx,
+ "rename-to:".into(),
+ prefill,
+ None,
+ ui::completers::none,
+ move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
+ 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;
+ }
+ };
+ match block_on(future) {
+ Ok(edits) => apply_workspace_edit(cx.editor, offset_encoding, &edits),
+ Err(err) => cx.editor.set_error(err.to_string()),
+ }
+ },
+ );
}
pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- let language_server =
- language_server_with_feature!(cx.editor, doc, LanguageServerFeature::DocumentHighlight);
+ 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 = language_server
- .text_document_document_highlight(doc.identifier(), pos, None)
- .unwrap();
+
+ let future = match language_server.text_document_document_highlight(doc.identifier(), pos, None)
+ {
+ Some(future) => future,
+ None => {
+ cx.editor
+ .set_error("Language server does not support document highlight");
+ return;
+ }
+ };
cx.callback(
future,
@@ -1248,8 +1233,10 @@ 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().cursor(text.slice(..));
+ let pos = doc.selection(view.id).primary().head;
// We must find the range that contains our primary cursor to prevent our primary cursor to move
let mut primary_index = 0;
@@ -1269,190 +1256,3 @@ pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
},
);
}
-
-pub fn compute_inlay_hints_for_all_views(editor: &mut Editor, jobs: &mut crate::job::Jobs) {
- if !editor.config().lsp.display_inlay_hints {
- return;
- }
-
- for (view, _) in editor.tree.views() {
- let doc = match editor.documents.get(&view.doc) {
- Some(doc) => doc,
- None => continue,
- };
- if let Some(callback) = compute_inlay_hints_for_view(view, doc) {
- jobs.callback(callback);
- }
- }
-}
-
-fn compute_inlay_hints_for_view(
- view: &View,
- doc: &Document,
-) -> Option<std::pin::Pin<Box<impl Future<Output = Result<crate::job::Callback, anyhow::Error>>>>> {
- let view_id = view.id;
- let doc_id = view.doc;
-
- let language_server = doc
- .language_servers_with_feature(LanguageServerFeature::InlayHints)
- .next()?;
-
- 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(doc.view_offset(view_id).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)
- .is_some_and(|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 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 offset_encoding = language_server.offset_encoding();
-
- let callback = super::make_job_callback(
- 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() {
- return;
- }
-
- // Add annotations to relevant document, not the current one (it may have changed in between)
- let doc = match editor.documents.get_mut(&doc_id) {
- Some(doc) => doc,
- None => return,
- };
-
- // If we have neither hints nor an LSP, empty the inlay hints since they're now oudated
- let mut hints = match response {
- Some(hints) if !hints.is_empty() => hints,
- _ => {
- doc.set_inlay_hints(
- view_id,
- DocumentInlayHints::empty_with_id(new_doc_inlay_hints_id),
- );
- doc.inlay_hints_oudated = false;
- return;
- }
- };
-
- // 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);
-
- let mut padding_before_inlay_hints = Vec::new();
- let mut type_inlay_hints = Vec::new();
- let mut parameter_inlay_hints = Vec::new();
- let mut other_inlay_hints = Vec::new();
- 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 =
- match helix_lsp::util::lsp_pos_to_pos(doc_text, hint.position, offset_encoding)
- {
- Some(pos) => pos,
- // Skip inlay hints that have no "real" position
- None => continue,
- };
-
- let mut label = match hint.label {
- lsp::InlayHintLabel::String(s) => s,
- lsp::InlayHintLabel::LabelParts(parts) => parts
- .into_iter()
- .map(|p| p.value)
- .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,
- Some(lsp::InlayHintKind::PARAMETER) => &mut parameter_inlay_hints,
- // We can't warn on unknown kind here since LSPs are free to set it or not, for
- // example Rust Analyzer does not: every kind will be `None`.
- _ => &mut other_inlay_hints,
- };
-
- if let Some(true) = hint.padding_left {
- padding_before_inlay_hints.push(InlineAnnotation::new(char_idx, " "));
- }
-
- inlay_hints_vec.push(InlineAnnotation::new(char_idx, label));
-
- if let Some(true) = hint.padding_right {
- padding_after_inlay_hints.push(InlineAnnotation::new(char_idx, " "));
- }
- }
-
- doc.set_inlay_hints(
- view_id,
- DocumentInlayHints {
- id: new_doc_inlay_hints_id,
- type_inlay_hints,
- parameter_inlay_hints,
- other_inlay_hints,
- padding_before_inlay_hints,
- padding_after_inlay_hints,
- },
- );
- doc.inlay_hints_oudated = false;
- },
- );
-
- Some(callback)
-}