feat(lsp): code lens

Initial implementation of LSP code lense.
pull/5063/head
Matouš Dzivjak 2 years ago
parent 22a051408a
commit 65af5400d9
No known key found for this signature in database

@ -32,4 +32,3 @@ signal to the Helix process on Unix operating systems, such as by using the comm
Finally, you can have a `config.toml` local to a project by putting it under a `.helix` directory in your repository. Finally, you can have a `config.toml` local to a project by putting it under a `.helix` directory in your repository.
Its settings will be merged with the configuration directory `config.toml` and the built-in configuration. Its settings will be merged with the configuration directory `config.toml` and the built-in configuration.

@ -331,6 +331,7 @@ pub enum LanguageServerFeature {
Diagnostics, Diagnostics,
RenameSymbol, RenameSymbol,
InlayHints, InlayHints,
CodeLens,
} }
impl Display for LanguageServerFeature { impl Display for LanguageServerFeature {
@ -354,6 +355,7 @@ impl Display for LanguageServerFeature {
Diagnostics => "diagnostics", Diagnostics => "diagnostics",
RenameSymbol => "rename-symbol", RenameSymbol => "rename-symbol",
InlayHints => "inlay-hints", InlayHints => "inlay-hints",
CodeLens => "code-lens",
}; };
write!(f, "{feature}",) write!(f, "{feature}",)
} }

@ -354,6 +354,7 @@ impl Client {
capabilities.inlay_hint_provider, capabilities.inlay_hint_provider,
Some(OneOf::Left(true) | OneOf::Right(InlayHintServerCapabilities::Options(_))) Some(OneOf::Left(true) | OneOf::Right(InlayHintServerCapabilities::Options(_)))
), ),
LanguageServerFeature::CodeLens => capabilities.code_lens_provider.is_some(),
} }
} }
@ -662,6 +663,9 @@ impl Client {
dynamic_registration: Some(false), dynamic_registration: Some(false),
resolve_support: None, resolve_support: None,
}), }),
code_lens: Some(lsp::CodeLensClientCapabilities {
..Default::default()
}),
..Default::default() ..Default::default()
}), }),
window: Some(lsp::WindowClientCapabilities { window: Some(lsp::WindowClientCapabilities {
@ -1549,4 +1553,45 @@ impl Client {
changes, changes,
}) })
} }
pub fn code_lens(
&self,
text_document: lsp::TextDocumentIdentifier,
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support code lens.
capabilities.code_lens_provider.as_ref()?;
let params = lsp::CodeLensParams {
text_document,
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
partial_result_params: lsp::PartialResultParams::default(),
};
Some(self.call::<lsp::request::CodeLensRequest>(params))
}
pub fn code_lens_resolve(
&self,
code_lens: lsp::CodeLens,
) -> Option<impl Future<Output = Result<Option<lsp::CodeLens>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support resolving code lens.
match capabilities.code_lens_provider {
Some(lsp::CodeLensOptions {
resolve_provider: Some(true),
..
}) => (),
_ => return None,
}
let request = self.call::<lsp::request::CodeLensResolve>(code_lens);
Some(async move {
let json = request.await?;
let response: Option<lsp::CodeLens> = serde_json::from_value(json)?;
Ok(response)
})
}
} }

@ -13,8 +13,9 @@ pub use lsp::{Position, Url};
pub use lsp_types as lsp; pub use lsp_types as lsp;
use futures_util::stream::select_all::SelectAll; use futures_util::stream::select_all::SelectAll;
use helix_core::syntax::{ use helix_core::{
LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures, diagnostic::Range,
syntax::{LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures},
}; };
use helix_stdx::path; use helix_stdx::path;
use slotmap::SlotMap; use slotmap::SlotMap;
@ -1134,3 +1135,13 @@ mod tests {
assert!(transaction.apply(&mut source)); assert!(transaction.apply(&mut source));
} }
} }
/// Corresponds to [`lsp_types::CodeLense`](https://docs.rs/lsp-types/0.94.0/lsp_types/struct.Diagnostic.html)
#[derive(Debug, Clone)]
pub struct CodeLens {
pub range: Range,
pub line: usize,
pub data: Option<serde_json::Value>,
pub language_server_id: LanguageServerId,
pub command: Option<lsp::Command>,
}

@ -422,6 +422,8 @@ impl MappableCommand {
paste_after, "Paste after selection", paste_after, "Paste after selection",
paste_before, "Paste before selection", paste_before, "Paste before selection",
paste_clipboard_after, "Paste clipboard after selections", paste_clipboard_after, "Paste clipboard after selections",
code_lens_under_cursor, "Show code lenses under cursor",
code_lenses_picker, "Show code lense picker",
paste_clipboard_before, "Paste clipboard before selections", paste_clipboard_before, "Paste clipboard before selections",
paste_primary_clipboard_after, "Paste primary clipboard after selections", paste_primary_clipboard_after, "Paste primary clipboard after selections",
paste_primary_clipboard_before, "Paste primary clipboard before selections", paste_primary_clipboard_before, "Paste primary clipboard before selections",

@ -5,8 +5,8 @@ use helix_lsp::{
self, CodeAction, CodeActionOrCommand, CodeActionTriggerKind, DiagnosticSeverity, self, CodeAction, CodeActionOrCommand, CodeActionTriggerKind, DiagnosticSeverity,
NumberOrString, NumberOrString,
}, },
util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range}, util::{diagnostic_to_lsp_diagnostic, lsp_pos_to_pos, lsp_range_to_range, range_to_lsp_range},
Client, LanguageServerId, OffsetEncoding, Client, CodeLens, LanguageServerId, OffsetEncoding,
}; };
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
use tui::{text::Span, widgets::Row}; use tui::{text::Span, widgets::Row};
@ -14,8 +14,9 @@ use tui::{text::Span, widgets::Row};
use super::{align_view, push_jump, Align, Context, Editor}; use super::{align_view, push_jump, Align, Context, Editor};
use helix_core::{ use helix_core::{
syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection, Uri, syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Rope, Selection, Uri,
}; };
pub use helix_lsp::lsp::Command;
use helix_stdx::path; use helix_stdx::path;
use helix_view::{ use helix_view::{
document::{DocumentInlayHints, DocumentInlayHintsId}, document::{DocumentInlayHints, DocumentInlayHintsId},
@ -61,6 +62,17 @@ macro_rules! language_server_with_feature {
}}; }};
} }
impl ui::menu::Item for CodeLens {
type Data = ();
fn format(&self, _: &Self::Data) -> Row {
match self.command.clone() {
Some(cmd) => cmd.title.into(),
None => "unresolved".into(),
}
}
}
struct SymbolInformationItem { struct SymbolInformationItem {
symbol: lsp::SymbolInformation, symbol: lsp::SymbolInformation,
offset_encoding: OffsetEncoding, offset_encoding: OffsetEncoding,
@ -1423,3 +1435,203 @@ fn compute_inlay_hints_for_view(
Some(callback) Some(callback)
} }
fn map_code_lens(
doc_text: &Rope,
cl: &lsp::CodeLens,
offset_enc: OffsetEncoding,
language_server_id: LanguageServerId,
) -> CodeLens {
use helix_core::diagnostic::Range;
let start = lsp_pos_to_pos(doc_text, cl.range.start, offset_enc).unwrap();
CodeLens {
range: Range {
start,
end: lsp_pos_to_pos(doc_text, cl.range.end, offset_enc).unwrap(),
},
line: doc_text.char_to_line(start),
data: cl.data.clone(),
language_server_id,
command: cl.command.clone(),
}
}
pub fn code_lens_under_cursor(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let doc_text = doc.text();
let language_server =
language_server_with_feature!(cx.editor, doc, LanguageServerFeature::CodeLens);
let offset_encoding = language_server.offset_encoding();
let pos = doc.position(view.id, offset_encoding);
let uri = match doc.uri() {
Some(uri) => uri,
None => {
return;
}
};
if let Some(lenses) = cx.editor.code_lenses.get(&uri) {
let lenses: Vec<CodeLens> = lenses
.iter()
.filter(|cl| {
// TODO: fix the check
cl.range.start.line == pos.line
})
.map(|cl| {
// if cl.command.is_none() {
// if let Some(req) = language_server.code_lens_resolve(cl.clone()) {
// if let Some(code_lens) = block_on(req).ok().unwrap() {
// log::info!("code_lense: resolved {:?} into {:?}", cl, code_lens);
// return map_code_lens(doc, &code_lens);
// }
// }
// }
map_code_lens(doc_text, cl, offset_encoding, language_server.id())
})
.collect();
if lenses.is_empty() {
cx.editor.set_status("No code lens available");
return;
}
let mut picker = ui::Menu::new(lenses, (), move |editor, code_lens, event| {
if event != PromptEvent::Validate {
return;
}
let code_lens = code_lens.unwrap();
let Some(language_server) = editor.language_server_by_id(code_lens.language_server_id)
else {
editor.set_error("Language Server disappeared");
return;
};
let lens = code_lens.clone();
if let Some(cmd) = lens.command {
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);
}
});
}
});
picker.move_down(); // pre-select the first item
let popup = Popup::new("code-lens", picker).with_scrollbar(false);
cx.push_layer(Box::new(popup));
};
}
pub fn code_lenses_picker(cx: &mut Context) {
let doc = doc!(cx.editor);
let language_server =
language_server_with_feature!(cx.editor, doc, LanguageServerFeature::CodeLens);
let language_server_id = language_server.id();
let request = match language_server.code_lens(doc.identifier()) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support code lens");
return;
}
};
let doc_id = doc.id();
let offset_enc = language_server.offset_encoding();
cx.callback(
request,
move |editor, compositor, lenses: Option<Vec<lsp::CodeLens>>| {
if let Some(lenses) = lenses {
let doc = doc_mut!(editor, &doc_id);
let doc_text = doc.text();
if let Some(uri) = doc.uri() {
editor.code_lenses.insert(uri, lenses.clone());
};
let lenses: Vec<CodeLens> = lenses
.iter()
.map(|l| map_code_lens(doc_text, l, offset_enc, language_server_id))
.collect();
log::error!("lenses got: {:?}", lenses);
doc.set_code_lens(lenses.clone());
let columns = [
ui::PickerColumn::new("title", |item: &CodeLens, _| match &item.command {
Some(cmd) => cmd.title.clone().into(),
None => "".into(),
}),
ui::PickerColumn::new("command", |item: &CodeLens, _| match &item.command {
Some(Command {
command,
arguments: None,
..
}) => command.clone().into(),
Some(Command {
command,
arguments: Some(arguments),
..
}) => format!(
"{} {}",
command,
arguments
.iter()
.map(|a| a.to_string())
.collect::<Vec<String>>()
.join(" ")
)
.into(),
None => "".into(),
}),
];
let picker = Picker::new(columns, 1, lenses, (), |cx, meta, _action| {
let doc = doc!(cx.editor);
let language_server = language_server_with_feature!(
cx.editor,
doc,
LanguageServerFeature::CodeLens
);
if let Some(cmd) = meta.command.clone() {
let future = match language_server.command(cmd) {
Some(future) => future,
None => {
cx.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);
}
});
}
});
compositor.push(Box::new(picker));
} else {
editor.set_status("no lens found");
}
},
)
}

@ -231,6 +231,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"a" => code_action, "a" => code_action,
"'" => last_picker, "'" => last_picker,
"G" => { "Debug (experimental)" sticky=true "G" => { "Debug (experimental)" sticky=true
"l" => code_lens_under_cursor,
"L" => code_lenses_picker,
"l" => dap_launch, "l" => dap_launch,
"r" => dap_restart, "r" => dap_restart,
"b" => dap_toggle_breakpoint, "b" => dap_toggle_breakpoint,

@ -352,6 +352,27 @@ impl EditorView {
text_annotations.collect_overlay_highlights(range) text_annotations.collect_overlay_highlights(range)
} }
pub fn doc_code_lens_highlights(
doc: &Document,
theme: &Theme,
) -> Option<Vec<(usize, std::ops::Range<usize>)>> {
let idx = theme
.find_scope_index("code_lens")
// get one of the themes below as fallback values
.or_else(|| theme.find_scope_index("diagnostic"))
.or_else(|| theme.find_scope_index("ui.cursor"))
.or_else(|| theme.find_scope_index("ui.selection"))
.expect(
"at least one of the following scopes must be defined in the theme: `diagnostic`, `ui.cursor`, or `ui.selection`",
);
Some(
doc.code_lens()
.iter()
.map(|l| (idx, l.range.start..l.range.end))
.collect(),
)
}
/// Get highlight spans for document diagnostics /// Get highlight spans for document diagnostics
pub fn doc_diagnostics_highlights( pub fn doc_diagnostics_highlights(
doc: &Document, doc: &Document,

@ -179,6 +179,7 @@ pub struct Document {
pub(crate) diagnostics: Vec<Diagnostic>, pub(crate) diagnostics: Vec<Diagnostic>,
pub(crate) language_servers: HashMap<LanguageServerName, Arc<Client>>, pub(crate) language_servers: HashMap<LanguageServerName, Arc<Client>>,
pub(crate) code_lens: Vec<CodeLens>,
diff_handle: Option<DiffHandle>, diff_handle: Option<DiffHandle>,
version_control_head: Option<Arc<ArcSwap<Box<str>>>>, version_control_head: Option<Arc<ArcSwap<Box<str>>>>,
@ -633,7 +634,7 @@ where
*mut_ref = f(mem::take(mut_ref)); *mut_ref = f(mem::take(mut_ref));
} }
use helix_lsp::{lsp, Client, LanguageServerId, LanguageServerName}; use helix_lsp::{lsp, Client, CodeLens, LanguageServerId, LanguageServerName};
use url::Url; use url::Url;
impl Document { impl Document {
@ -664,6 +665,7 @@ impl Document {
changes, changes,
old_state, old_state,
diagnostics: Vec::new(), diagnostics: Vec::new(),
code_lens: Vec::new(),
version: 0, version: 0,
history: Cell::new(History::default()), history: Cell::new(History::default()),
savepoints: Vec::new(), savepoints: Vec::new(),
@ -1883,6 +1885,15 @@ impl Document {
}) })
} }
#[inline]
pub fn code_lens(&self) -> &[CodeLens] {
&self.code_lens
}
pub fn set_code_lens(&mut self, code_lens: Vec<CodeLens>) {
self.code_lens = code_lens;
}
#[inline] #[inline]
pub fn diagnostics(&self) -> &[Diagnostic] { pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics &self.diagnostics

@ -435,6 +435,8 @@ pub struct LspConfig {
pub snippets: bool, pub snippets: bool,
/// Whether to include declaration in the goto reference query /// Whether to include declaration in the goto reference query
pub goto_reference_include_declaration: bool, pub goto_reference_include_declaration: bool,
/// Enable code lense.
pub code_lens: bool,
} }
impl Default for LspConfig { impl Default for LspConfig {
@ -447,6 +449,7 @@ impl Default for LspConfig {
display_inlay_hints: false, display_inlay_hints: false,
snippets: true, snippets: true,
goto_reference_include_declaration: true, goto_reference_include_declaration: true,
code_lens: false,
} }
} }
} }
@ -1029,6 +1032,7 @@ pub struct Editor {
pub macro_replaying: Vec<char>, pub macro_replaying: Vec<char>,
pub language_servers: helix_lsp::Registry, pub language_servers: helix_lsp::Registry,
pub diagnostics: BTreeMap<Uri, Vec<(lsp::Diagnostic, LanguageServerId)>>, pub diagnostics: BTreeMap<Uri, Vec<(lsp::Diagnostic, LanguageServerId)>>,
pub code_lenses: BTreeMap<Uri, Vec<lsp::CodeLens>>,
pub diff_providers: DiffProviderRegistry, pub diff_providers: DiffProviderRegistry,
pub debugger: Option<dap::Client>, pub debugger: Option<dap::Client>,
@ -1174,6 +1178,7 @@ impl Editor {
theme: theme_loader.default(), theme: theme_loader.default(),
language_servers, language_servers,
diagnostics: BTreeMap::new(), diagnostics: BTreeMap::new(),
code_lenses: BTreeMap::new(),
diff_providers: DiffProviderRegistry::default(), diff_providers: DiffProviderRegistry::default(),
debugger: None, debugger: None,
debugger_events: SelectAll::new(), debugger_events: SelectAll::new(),

@ -86,6 +86,8 @@ label = "honey"
"diagnostic.unnecessary" = { modifiers = ["dim"] } "diagnostic.unnecessary" = { modifiers = ["dim"] }
"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "diagnostic.deprecated" = { modifiers = ["crossed_out"] }
"code_lens" = { modifiers = ["underline"] }
warning = "lightning" warning = "lightning"
error = "apricot" error = "apricot"
info = "delta" info = "delta"

Loading…
Cancel
Save