Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lsp): code lenses #5063

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion book/src/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Its settings will be merged with the configuration directory `config.toml` and the built-in configuration.

2 changes: 1 addition & 1 deletion helix-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ pub use syntax::Syntax;

pub use diagnostic::Diagnostic;

pub use line_ending::{LineEnding, NATIVE_LINE_ENDING};
pub use line_ending::{line_end_char_index, LineEnding, NATIVE_LINE_ENDING};
pub use transaction::{Assoc, Change, ChangeSet, Deletion, Operation, Transaction};

pub use uri::Uri;
2 changes: 2 additions & 0 deletions helix-core/src/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ pub enum LanguageServerFeature {
Diagnostics,
RenameSymbol,
InlayHints,
CodeLens,
}

impl Display for LanguageServerFeature {
Expand All @@ -354,6 +355,7 @@ impl Display for LanguageServerFeature {
Diagnostics => "diagnostics",
RenameSymbol => "rename-symbol",
InlayHints => "inlay-hints",
CodeLens => "code-lens",
};
write!(f, "{feature}",)
}
Expand Down
22 changes: 22 additions & 0 deletions helix-lsp/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ impl Client {
capabilities.inlay_hint_provider,
Some(OneOf::Left(true) | OneOf::Right(InlayHintServerCapabilities::Options(_)))
),
LanguageServerFeature::CodeLens => capabilities.code_lens_provider.is_some(),
}
}

Expand Down Expand Up @@ -662,6 +663,9 @@ impl Client {
dynamic_registration: Some(false),
resolve_support: None,
}),
code_lens: Some(lsp::CodeLensClientCapabilities {
..Default::default()
}),
..Default::default()
}),
window: Some(lsp::WindowClientCapabilities {
Expand Down Expand Up @@ -1549,4 +1553,22 @@ impl Client {
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))
}
}
17 changes: 15 additions & 2 deletions helix-lsp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ pub use lsp::{Position, Url};
pub use lsp_types as lsp;

use futures_util::stream::select_all::SelectAll;
use helix_core::syntax::{
LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures,
use helix_core::{
diagnostic::Range,
syntax::{LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures},
};
use helix_stdx::path;
use slotmap::SlotMap;
Expand Down Expand Up @@ -567,6 +568,7 @@ pub enum MethodCall {
RegisterCapability(lsp::RegistrationParams),
UnregisterCapability(lsp::UnregistrationParams),
ShowDocument(lsp::ShowDocumentParams),
CodeLensRefresh,
}

impl MethodCall {
Expand Down Expand Up @@ -598,6 +600,7 @@ impl MethodCall {
let params: lsp::ShowDocumentParams = params.parse()?;
Self::ShowDocument(params)
}
lsp::request::CodeLensRefresh::METHOD => Self::CodeLensRefresh,
_ => {
return Err(Error::Unhandled);
}
Expand Down Expand Up @@ -1134,3 +1137,13 @@ mod tests {
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>,
}
4 changes: 4 additions & 0 deletions helix-term/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1108,6 +1108,10 @@ impl Application {
let result = self.handle_show_document(params, offset_encoding);
Ok(json!(result))
}
Ok(MethodCall::CodeLensRefresh) => {
log::warn!("unhandled workspace/codeLens/refresh");
Ok(serde_json::Value::Null)
}
};

tokio::spawn(language_server!().reply(id, reply));
Expand Down
2 changes: 2 additions & 0 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,8 @@ impl MappableCommand {
paste_after, "Paste after selection",
paste_before, "Paste before selection",
paste_clipboard_after, "Paste clipboard after selections",
code_lens_under_cursor, "Show code lenses under cursor",
request_code_lenses, "Show code lense picker",
paste_clipboard_before, "Paste clipboard before selections",
paste_primary_clipboard_after, "Paste primary clipboard after selections",
paste_primary_clipboard_before, "Paste primary clipboard before selections",
Expand Down
144 changes: 141 additions & 3 deletions helix-term/src/commands/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ use helix_lsp::{
self, CodeAction, CodeActionOrCommand, CodeActionTriggerKind, DiagnosticSeverity,
NumberOrString,
},
util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
Client, LanguageServerId, OffsetEncoding,
util::{diagnostic_to_lsp_diagnostic, lsp_pos_to_pos, lsp_range_to_range, range_to_lsp_range},
Client, CodeLens, LanguageServerId, OffsetEncoding,
};
use tokio_stream::StreamExt;
use tui::{text::Span, widgets::Row};

use super::{align_view, push_jump, Align, Context, Editor};

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_view::{
document::{DocumentInlayHints, DocumentInlayHintsId},
Expand Down Expand Up @@ -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 {
symbol: lsp::SymbolInformation,
offset_encoding: OffsetEncoding,
Expand Down Expand Up @@ -1423,3 +1435,129 @@ fn compute_inlay_hints_for_view(

Some(callback)
}

pub(crate) 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 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 current_line_lenses: Vec<CodeLens> = doc
.code_lens()
.iter()
.filter(|cl| {
// TODO: fix the check
cl.line == pos.line as usize
})
.cloned()
.collect();

if current_line_lenses.is_empty() {
cx.editor.set_status("No code lens available");
return;
}

let mut picker = ui::Menu::new(current_line_lenses, (), move |editor, code_lens, event| {
if event != PromptEvent::Validate {
return;
}

let lens = code_lens.unwrap();

let Some(language_server) = editor.language_server_by_id(lens.language_server_id) else {
editor.set_error("Language Server disappeared");
return;
};

if let Some(cmd) = &lens.command {
let future = match language_server.command(cmd.clone()) {
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));
}

// TODO: should be run the same way as diagnostic - shouldn't require manual
// trigger to set lenses.
pub fn request_code_lenses(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();

doc.set_code_lens(lenses);
} else {
editor.set_status("no lens found");
}
},
)
}
2 changes: 2 additions & 0 deletions helix-term/src/keymap/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"g" => changed_file_picker,
"a" => code_action,
"'" => last_picker,
"l" => code_lens_under_cursor,
"L" => request_code_lenses,
"G" => { "Debug (experimental)" sticky=true
"l" => dap_launch,
"r" => dap_restart,
Expand Down
21 changes: 21 additions & 0 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,27 @@ impl EditorView {
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
pub fn doc_diagnostics_highlights(
doc: &Document,
Expand Down
13 changes: 12 additions & 1 deletion helix-view/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ pub struct Document {

pub(crate) diagnostics: Vec<Diagnostic>,
pub(crate) language_servers: HashMap<LanguageServerName, Arc<Client>>,
pub(crate) code_lens: Vec<CodeLens>,

diff_handle: Option<DiffHandle>,
version_control_head: Option<Arc<ArcSwap<Box<str>>>>,
Expand Down Expand Up @@ -633,7 +634,7 @@ where
*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;

impl Document {
Expand Down Expand Up @@ -664,6 +665,7 @@ impl Document {
changes,
old_state,
diagnostics: Vec::new(),
code_lens: Vec::new(),
version: 0,
history: Cell::new(History::default()),
savepoints: Vec::new(),
Expand Down Expand Up @@ -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]
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
Expand Down
Loading
Loading