Skip to content

Commit 867e027

Browse files
committed
feat(lsp): code lens under cursor picker
1 parent 2886377 commit 867e027

12 files changed

Lines changed: 176 additions & 8 deletions

File tree

book/src/configuration.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,12 @@ The following statusline elements can be configured:
112112

113113
### `[editor.lsp]` Section
114114

115-
| Key | Description | Default |
116-
| --- | ----------- | ------- |
117-
| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
118-
| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
119-
| `display-signature-help-docs` | Display docs under signature help popup | `true` |
115+
| Key | Description | Default |
116+
| --- | ----------- | ------- |
117+
| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
118+
| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
119+
| `display-signature-help-docs` | Display docs under signature help popup | `true` |
120+
| `code-lens` | Enable code lens | `true` |
120121

121122
[^1]: By default, a progress spinner is shown in the statusline beside the file path.
122123

helix-lsp/src/client.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,4 +1105,27 @@ impl Client {
11051105

11061106
Some(self.call::<lsp::request::CodeLensRequest>(params))
11071107
}
1108+
1109+
pub fn code_lens_resolve(
1110+
&self,
1111+
code_lens: lsp::CodeLens,
1112+
) -> Option<impl Future<Output = Result<Option<lsp::CodeLens>>>> {
1113+
let capabilities = self.capabilities.get().unwrap();
1114+
1115+
// Return early if the server does not support resolving code lens.
1116+
match capabilities.code_lens_provider {
1117+
Some(lsp::CodeLensOptions {
1118+
resolve_provider: Some(true),
1119+
..
1120+
}) => (),
1121+
_ => return None,
1122+
}
1123+
1124+
let request = self.call::<lsp::request::CodeLensResolve>(code_lens);
1125+
Some(async move {
1126+
let json = request.await?;
1127+
let response: Option<lsp::CodeLens> = serde_json::from_value(json)?;
1128+
Ok(response)
1129+
})
1130+
}
11081131
}

helix-term/src/commands.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ impl MappableCommand {
348348
paste_after, "Paste after selection",
349349
paste_before, "Paste before selection",
350350
paste_clipboard_after, "Paste clipboard after selections",
351+
code_lens_under_cursor, "Show code lenses under cursor",
351352
code_lenses_picker, "Show code lense picker",
352353
paste_clipboard_before, "Paste clipboard before selections",
353354
paste_primary_clipboard_after, "Paste primary clipboard after selections",

helix-term/src/commands/lsp.rs

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1269,6 +1269,82 @@ impl ui::menu::Item for lsp::CodeLens {
12691269
}
12701270
}
12711271

1272+
pub fn code_lens_under_cursor(cx: &mut Context) {
1273+
let (view, doc) = current!(cx.editor);
1274+
1275+
let language_server = match doc.language_server() {
1276+
Some(language_server) => language_server,
1277+
None => return,
1278+
};
1279+
1280+
let offset_encoding = language_server.offset_encoding();
1281+
let pos = doc.position(view.id, offset_encoding);
1282+
let url = doc.url();
1283+
1284+
if url.is_none() {
1285+
return;
1286+
};
1287+
1288+
if let Some(lenses) = cx.editor.code_lenses.get(&url.unwrap()) {
1289+
let lenses: Vec<lsp::CodeLens> = lenses
1290+
.iter()
1291+
.filter(|cl| {
1292+
// TODO: fix the check
1293+
cl.range.start.line == pos.line
1294+
})
1295+
.map(|cl| {
1296+
if cl.command.is_none() {
1297+
if let Some(req) = language_server.code_lens_resolve(cl.clone()) {
1298+
if let Some(code_lens) = block_on(req).ok().unwrap() {
1299+
log::info!("code_lense: resolved {:?} into {:?}", cl, code_lens);
1300+
return code_lens;
1301+
}
1302+
}
1303+
}
1304+
cl.clone()
1305+
})
1306+
.collect();
1307+
1308+
if lenses.is_empty() {
1309+
cx.editor.set_status("No code lens available");
1310+
return;
1311+
}
1312+
1313+
let mut picker = ui::Menu::new(lenses, (), move |editor, code_lens, event| {
1314+
let doc = doc!(editor);
1315+
1316+
if event != PromptEvent::Validate {
1317+
return;
1318+
}
1319+
1320+
if let Some(language_server) = doc.language_server() {
1321+
let lens = code_lens.unwrap().clone();
1322+
if let Some(cmd) = lens.command {
1323+
let future = match language_server.command(cmd) {
1324+
Some(future) => future,
1325+
None => {
1326+
editor.set_error("Language server does not support executing commands");
1327+
return;
1328+
}
1329+
};
1330+
1331+
tokio::spawn(async move {
1332+
let res = future.await;
1333+
1334+
if let Err(e) = res {
1335+
log::error!("execute LSP command: {}", e);
1336+
}
1337+
});
1338+
}
1339+
}
1340+
});
1341+
picker.move_down(); // pre-select the first item
1342+
1343+
let popup = Popup::new("code-lens", picker).with_scrollbar(false);
1344+
cx.push_layer(Box::new(popup));
1345+
};
1346+
}
1347+
12721348
pub fn code_lenses_picker(cx: &mut Context) {
12731349
let doc = doc!(cx.editor);
12741350

@@ -1281,7 +1357,7 @@ pub fn code_lenses_picker(cx: &mut Context) {
12811357
Some(future) => future,
12821358
None => {
12831359
cx.editor
1284-
.set_error("Language server does not support code lense");
1360+
.set_error("Language server does not support code lens");
12851361
return;
12861362
}
12871363
};
@@ -1291,6 +1367,11 @@ pub fn code_lenses_picker(cx: &mut Context) {
12911367
request,
12921368
move |editor, compositor, lenses: Option<Vec<lsp::CodeLens>>| {
12931369
if let Some(lenses) = lenses {
1370+
let doc = doc_mut!(editor, &doc_id);
1371+
if let Some(current_url) = doc.url() {
1372+
editor.code_lenses.insert(current_url, lenses.clone());
1373+
doc.set_code_lens(lenses.clone());
1374+
};
12941375
log::error!("lenses got: {:?}", lenses);
12951376
let picker = FilePicker::new(
12961377
lenses,
@@ -1327,7 +1408,7 @@ pub fn code_lenses_picker(cx: &mut Context) {
13271408
);
13281409
compositor.push(Box::new(overlayed(picker)));
13291410
} else {
1330-
editor.set_status("no lense found");
1411+
editor.set_status("no lens found");
13311412
}
13321413
},
13331414
)

helix-term/src/keymap/default.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
217217
"D" => workspace_diagnostics_picker,
218218
"a" => code_action,
219219
"'" => last_picker,
220-
// "l" => execute_lense_under_cursor,
220+
"l" => code_lens_under_cursor,
221221
"L" => code_lenses_picker,
222222
"g" => { "Debug (experimental)" sticky=true
223223
"l" => dap_launch,

helix-term/src/ui/editor.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use helix_core::{
1616
unicode::width::UnicodeWidthStr,
1717
visual_coords_at_pos, LineEnding, Position, Range, Selection, Transaction,
1818
};
19+
use helix_lsp::util::lsp_pos_to_pos;
1920
use helix_view::{
2021
apply_transaction,
2122
document::{Mode, SCRATCH_BUFFER_NAME},
@@ -134,6 +135,9 @@ impl EditorView {
134135
}
135136
highlights = Box::new(syntax::merge(highlights, diagnostic));
136137
}
138+
if let Some(spans) = EditorView::doc_code_lens_highlights(doc, theme) {
139+
highlights = Box::new(helix_core::syntax::merge(highlights, spans));
140+
}
137141
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
138142
Box::new(syntax::merge(
139143
highlights,
@@ -265,6 +269,39 @@ impl EditorView {
265269
}
266270
}
267271

272+
pub fn doc_code_lens_highlights(
273+
doc: &Document,
274+
theme: &Theme,
275+
) -> Option<Vec<(usize, std::ops::Range<usize>)>> {
276+
let idx = theme
277+
.find_scope_index("code_lens")
278+
// get one of the themes below as fallback values
279+
.or_else(|| theme.find_scope_index("diagnostic"))
280+
.or_else(|| theme.find_scope_index("ui.cursor"))
281+
.or_else(|| theme.find_scope_index("ui.selection"))
282+
.expect(
283+
"at least one of the following scopes must be defined in the theme: `diagnostic`, `ui.cursor`, or `ui.selection`",
284+
);
285+
if let Some(ls) = doc.language_server() {
286+
return Some(
287+
doc.code_lens()
288+
.iter()
289+
.map(|l| {
290+
// TODO: optimize
291+
let start = lsp_pos_to_pos(doc.text(), l.range.start, ls.offset_encoding())
292+
.unwrap();
293+
let end =
294+
lsp_pos_to_pos(doc.text(), l.range.end, ls.offset_encoding()).unwrap();
295+
296+
(idx, start..end)
297+
})
298+
.collect(),
299+
);
300+
}
301+
302+
None
303+
}
304+
268305
/// Get highlight spans for document diagnostics
269306
pub fn doc_diagnostics_highlights(
270307
doc: &Document,

helix-term/src/ui/picker.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,9 @@ impl<T: Item + 'static> Component for FilePicker<T> {
288288
}
289289
highlights = Box::new(helix_core::syntax::merge(highlights, spans));
290290
}
291+
if let Some(spans) = EditorView::doc_code_lens_highlights(doc, &cx.editor.theme) {
292+
highlights = Box::new(helix_core::syntax::merge(highlights, spans));
293+
}
291294
EditorView::render_text_highlights(
292295
doc,
293296
offset,

helix-view/src/document.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ pub struct Document {
135135
pub(crate) modified_since_accessed: bool,
136136

137137
diagnostics: Vec<Diagnostic>,
138+
code_lens: Vec<helix_lsp::lsp::CodeLens>,
138139
language_server: Option<Arc<helix_lsp::Client>>,
139140

140141
diff_handle: Option<DiffHandle>,
@@ -370,6 +371,7 @@ impl Document {
370371
changes,
371372
old_state,
372373
diagnostics: Vec::new(),
374+
code_lens: Vec::new(),
373375
version: 0,
374376
history: Cell::new(History::default()),
375377
savepoint: None,
@@ -1163,6 +1165,15 @@ impl Document {
11631165
)
11641166
}
11651167

1168+
#[inline]
1169+
pub fn code_lens(&self) -> &[lsp::CodeLens] {
1170+
&self.code_lens
1171+
}
1172+
1173+
pub fn set_code_lens(&mut self, code_lens: Vec<lsp::CodeLens>) {
1174+
self.code_lens = code_lens;
1175+
}
1176+
11661177
#[inline]
11671178
pub fn diagnostics(&self) -> &[Diagnostic] {
11681179
&self.diagnostics

helix-view/src/editor.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,8 @@ pub struct LspConfig {
242242
pub auto_signature_help: bool,
243243
/// Display docs under signature help popup
244244
pub display_signature_help_docs: bool,
245+
/// Enable code lense.
246+
pub code_lens: bool,
245247
}
246248

247249
impl Default for LspConfig {
@@ -250,6 +252,7 @@ impl Default for LspConfig {
250252
display_messages: false,
251253
auto_signature_help: true,
252254
display_signature_help_docs: true,
255+
code_lens: false,
253256
}
254257
}
255258
}
@@ -690,6 +693,7 @@ pub struct Editor {
690693
pub macro_replaying: Vec<char>,
691694
pub language_servers: helix_lsp::Registry,
692695
pub diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
696+
pub code_lenses: BTreeMap<lsp::Url, Vec<lsp::CodeLens>>,
693697
pub diff_providers: DiffProviderRegistry,
694698

695699
pub debugger: Option<dap::Client>,
@@ -802,6 +806,7 @@ impl Editor {
802806
theme: theme_loader.default(),
803807
language_servers: helix_lsp::Registry::new(),
804808
diagnostics: BTreeMap::new(),
809+
code_lenses: BTreeMap::new(),
805810
diff_providers: DiffProviderRegistry::default(),
806811
debugger: None,
807812
debugger_events: SelectAll::new(),

languages.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ comment-token = "//"
1212
language-server = { command = "rust-analyzer" }
1313
indent = { tab-width = 4, unit = " " }
1414

15+
[language.config]
16+
checkOnSave = { command = "clippy" }
17+
1518
[language.auto-pairs]
1619
'(' = ')'
1720
'{' = '}'

0 commit comments

Comments
 (0)