Skip to content

Commit 5040679

Browse files
committed
Add multiple language servers support for code actions
1 parent d97da46 commit 5040679

File tree

1 file changed

+117
-68
lines changed

1 file changed

+117
-68
lines changed

helix-term/src/commands/lsp.rs

Lines changed: 117 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ use crate::{
1414
ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent},
1515
};
1616

17-
use std::borrow::Cow;
17+
use std::{
18+
borrow::Cow,
19+
sync::{Arc, Mutex},
20+
};
1821

1922
// TODO extend this to support multiple language servers
2023
#[macro_export]
@@ -30,6 +33,19 @@ macro_rules! language_server {
3033
};
3134
}
3235

36+
#[macro_export]
37+
macro_rules! language_server_by_id {
38+
($editor:expr, $id:expr) => {
39+
match $editor.language_servers.get_by_id($id) {
40+
Some(language_server) => language_server,
41+
None => {
42+
$editor.set_status("Language server not active for current buffer");
43+
return;
44+
}
45+
}
46+
};
47+
}
48+
3349
fn location_to_file_location(location: &lsp::Location) -> FileLocation {
3450
let path = location.uri.to_file_path().unwrap();
3551
let line = Some((
@@ -179,9 +195,9 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
179195
)
180196
}
181197

182-
impl ui::menu::Item for lsp::CodeActionOrCommand {
198+
impl ui::menu::Item for (lsp::CodeActionOrCommand, OffsetEncoding) {
183199
fn label(&self) -> &str {
184-
match self {
200+
match &self.0 {
185201
lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str(),
186202
lsp::CodeActionOrCommand::Command(command) => command.title.as_str(),
187203
}
@@ -191,84 +207,117 @@ impl ui::menu::Item for lsp::CodeActionOrCommand {
191207
pub fn code_action(cx: &mut Context) {
192208
let (view, doc) = current!(cx.editor);
193209

194-
let language_server = language_server!(cx.editor, doc);
195-
196210
let selection_range = doc.selection(view.id).primary();
197-
let offset_encoding = language_server.offset_encoding();
198211

199-
let range = range_to_lsp_range(doc.text(), selection_range, offset_encoding);
200-
201-
let future = language_server.code_actions(
202-
doc.identifier(),
203-
range,
204-
// Filter and convert overlapping diagnostics
205-
lsp::CodeActionContext {
206-
diagnostics: doc
207-
.diagnostics()
208-
.iter()
209-
.filter(|&diag| {
210-
selection_range
211-
.overlaps(&helix_core::Range::new(diag.range.start, diag.range.end))
212-
})
213-
.map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding))
214-
.collect(),
215-
only: None,
216-
},
217-
);
212+
// this ensures, that a previously opened menu that doesn't have anything to do with this command will be replaced with a new menu
213+
let code_actions_menu_open = Arc::new(Mutex::new(false));
218214

219-
cx.callback(
220-
future,
221-
move |editor, compositor, response: Option<lsp::CodeActionResponse>| {
222-
let actions = match response {
223-
Some(a) => a,
224-
None => return,
225-
};
226-
if actions.is_empty() {
227-
editor.set_status("No code actions available");
228-
return;
229-
}
215+
let mut requests = Vec::new();
216+
217+
for language_server in doc.language_servers() {
218+
let offset_encoding = language_server.offset_encoding();
219+
let range = range_to_lsp_range(doc.text(), selection_range, offset_encoding);
220+
221+
requests.push((
222+
language_server.code_actions(
223+
doc.identifier(),
224+
range,
225+
// Filter and convert overlapping diagnostics
226+
lsp::CodeActionContext {
227+
diagnostics: doc
228+
.diagnostics()
229+
.iter()
230+
.filter(|&diag| {
231+
selection_range
232+
.overlaps(&helix_core::Range::new(diag.range.start, diag.range.end))
233+
})
234+
.map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding))
235+
.collect(),
236+
only: None,
237+
},
238+
),
239+
offset_encoding,
240+
language_server.id(),
241+
));
242+
}
243+
244+
for (future, offset_encoding, lsp_id) in requests {
245+
let code_actions_menu_open = code_actions_menu_open.clone();
230246

231-
let mut picker = ui::Menu::new(actions, move |editor, code_action, event| {
232-
if event != PromptEvent::Validate {
247+
cx.callback(
248+
future,
249+
move |editor, compositor, response: Option<lsp::CodeActionResponse>| {
250+
let actions = match response {
251+
Some(a) => a
252+
.into_iter()
253+
.map(|a| (a, offset_encoding))
254+
.collect::<Vec<_>>(),
255+
None => return,
256+
};
257+
258+
let mut code_actions_menu_open = code_actions_menu_open.lock().unwrap();
259+
260+
if actions.is_empty() && !*code_actions_menu_open {
261+
editor.set_status("No code actions available");
233262
return;
234263
}
235264

236-
// always present here
237-
let code_action = code_action.unwrap();
265+
let code_actions_menu = compositor.find_id::<Popup<
266+
ui::Menu<(lsp::CodeActionOrCommand, OffsetEncoding)>,
267+
>>("code-action");
238268

239-
match code_action {
240-
lsp::CodeActionOrCommand::Command(command) => {
241-
log::debug!("code action command: {:?}", command);
242-
execute_lsp_command(editor, command.clone());
243-
}
244-
lsp::CodeActionOrCommand::CodeAction(code_action) => {
245-
log::debug!("code action: {:?}", code_action);
246-
if let Some(ref workspace_edit) = code_action.edit {
247-
log::debug!("edit: {:?}", workspace_edit);
248-
apply_workspace_edit(editor, offset_encoding, workspace_edit);
269+
if !*code_actions_menu_open || code_actions_menu.is_none() {
270+
let mut picker = ui::Menu::new(actions, move |editor, code_action, event| {
271+
if event != PromptEvent::Validate {
272+
return;
249273
}
250274

251-
// if code action provides both edit and command first the edit
252-
// should be applied and then the command
253-
if let Some(command) = &code_action.command {
254-
execute_lsp_command(editor, command.clone());
275+
// always present here
276+
let code_action = code_action.unwrap();
277+
278+
match code_action {
279+
(lsp::CodeActionOrCommand::Command(command), _encoding) => {
280+
log::debug!("code action command: {:?}", command);
281+
execute_lsp_command(editor, lsp_id, command.clone());
282+
}
283+
(
284+
lsp::CodeActionOrCommand::CodeAction(code_action),
285+
offset_encoding,
286+
) => {
287+
log::debug!("code action: {:?}", code_action);
288+
if let Some(ref workspace_edit) = code_action.edit {
289+
log::debug!("edit: {:?}", workspace_edit);
290+
apply_workspace_edit(editor, *offset_encoding, workspace_edit);
291+
}
292+
293+
// if code action provides both edit and command first the edit
294+
// should be applied and then the command
295+
if let Some(command) = &code_action.command {
296+
execute_lsp_command(editor, lsp_id, command.clone());
297+
}
298+
}
255299
}
256-
}
257-
}
258-
});
259-
picker.move_down(); // pre-select the first item
300+
});
301+
picker.move_down(); // pre-select the first item
260302

261-
let popup = Popup::new("code-action", picker).margin(helix_view::graphics::Margin {
262-
vertical: 1,
263-
horizontal: 1,
264-
});
265-
compositor.replace_or_push("code-action", popup);
266-
},
267-
)
303+
let popup = Popup::new("code-action", picker)
304+
.margin(helix_view::graphics::Margin {
305+
vertical: 1,
306+
horizontal: 1,
307+
})
308+
.auto_close(true);
309+
compositor.replace_or_push("code-action", popup);
310+
*code_actions_menu_open = true;
311+
} else if let Some(code_actions_menu) = code_actions_menu {
312+
let picker = code_actions_menu.contents_mut();
313+
picker.add_options(actions)
314+
}
315+
},
316+
)
317+
}
268318
}
269-
pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
270-
let doc = doc!(editor);
271-
let language_server = language_server!(editor, doc);
319+
pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd: lsp::Command) {
320+
let language_server = language_server_by_id!(editor, language_server_id);
272321

273322
// the command is executed on the server and communicated back
274323
// to the client asynchronously using workspace edits

0 commit comments

Comments
 (0)