Skip to content

Commit

Permalink
Add multiple language servers support for code actions
Browse files Browse the repository at this point in the history
  • Loading branch information
Philipp-M committed May 24, 2022
1 parent d97da46 commit 5040679
Showing 1 changed file with 117 additions and 68 deletions.
185 changes: 117 additions & 68 deletions helix-term/src/commands/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ use crate::{
ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent},
};

use std::borrow::Cow;
use std::{
borrow::Cow,
sync::{Arc, Mutex},
};

// TODO extend this to support multiple language servers
#[macro_export]
Expand All @@ -30,6 +33,19 @@ macro_rules! language_server {
};
}

#[macro_export]
macro_rules! language_server_by_id {
($editor:expr, $id:expr) => {
match $editor.language_servers.get_by_id($id) {
Some(language_server) => language_server,
None => {
$editor.set_status("Language server not active for current buffer");
return;
}
}
};
}

fn location_to_file_location(location: &lsp::Location) -> FileLocation {
let path = location.uri.to_file_path().unwrap();
let line = Some((
Expand Down Expand Up @@ -179,9 +195,9 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
)
}

impl ui::menu::Item for lsp::CodeActionOrCommand {
impl ui::menu::Item for (lsp::CodeActionOrCommand, OffsetEncoding) {
fn label(&self) -> &str {
match self {
match &self.0 {
lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str(),
lsp::CodeActionOrCommand::Command(command) => command.title.as_str(),
}
Expand All @@ -191,84 +207,117 @@ impl ui::menu::Item for lsp::CodeActionOrCommand {
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 range = range_to_lsp_range(doc.text(), selection_range, offset_encoding);

let future = 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,
},
);
// this ensures, that a previously opened menu that doesn't have anything to do with this command will be replaced with a new menu
let code_actions_menu_open = Arc::new(Mutex::new(false));

cx.callback(
future,
move |editor, compositor, response: Option<lsp::CodeActionResponse>| {
let actions = match response {
Some(a) => a,
None => return,
};
if actions.is_empty() {
editor.set_status("No code actions available");
return;
}
let mut requests = Vec::new();

for language_server in doc.language_servers() {
let offset_encoding = language_server.offset_encoding();
let range = range_to_lsp_range(doc.text(), selection_range, offset_encoding);

requests.push((
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,
},
),
offset_encoding,
language_server.id(),
));
}

for (future, offset_encoding, lsp_id) in requests {
let code_actions_menu_open = code_actions_menu_open.clone();

let mut picker = ui::Menu::new(actions, move |editor, code_action, event| {
if event != PromptEvent::Validate {
cx.callback(
future,
move |editor, compositor, response: Option<lsp::CodeActionResponse>| {
let actions = match response {
Some(a) => a
.into_iter()
.map(|a| (a, offset_encoding))
.collect::<Vec<_>>(),
None => return,
};

let mut code_actions_menu_open = code_actions_menu_open.lock().unwrap();

if actions.is_empty() && !*code_actions_menu_open {
editor.set_status("No code actions available");
return;
}

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

match code_action {
lsp::CodeActionOrCommand::Command(command) => {
log::debug!("code action command: {:?}", command);
execute_lsp_command(editor, command.clone());
}
lsp::CodeActionOrCommand::CodeAction(code_action) => {
log::debug!("code action: {:?}", code_action);
if let Some(ref workspace_edit) = code_action.edit {
log::debug!("edit: {:?}", workspace_edit);
apply_workspace_edit(editor, offset_encoding, workspace_edit);
if !*code_actions_menu_open || code_actions_menu.is_none() {
let mut picker = ui::Menu::new(actions, move |editor, code_action, event| {
if event != PromptEvent::Validate {
return;
}

// 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 {
execute_lsp_command(editor, command.clone());
// always present here
let code_action = code_action.unwrap();

match code_action {
(lsp::CodeActionOrCommand::Command(command), _encoding) => {
log::debug!("code action command: {:?}", command);
execute_lsp_command(editor, lsp_id, command.clone());
}
(
lsp::CodeActionOrCommand::CodeAction(code_action),
offset_encoding,
) => {
log::debug!("code action: {:?}", code_action);
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 {
execute_lsp_command(editor, lsp_id, command.clone());
}
}
}
}
}
});
picker.move_down(); // pre-select the first item
});
picker.move_down(); // pre-select the first item

let popup = Popup::new("code-action", picker).margin(helix_view::graphics::Margin {
vertical: 1,
horizontal: 1,
});
compositor.replace_or_push("code-action", popup);
},
)
let popup = Popup::new("code-action", picker)
.margin(helix_view::graphics::Margin {
vertical: 1,
horizontal: 1,
})
.auto_close(true);
compositor.replace_or_push("code-action", popup);
*code_actions_menu_open = true;
} else if let Some(code_actions_menu) = code_actions_menu {
let picker = code_actions_menu.contents_mut();
picker.add_options(actions)
}
},
)
}
}
pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
let doc = doc!(editor);
let language_server = language_server!(editor, doc);
pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd: lsp::Command) {
let language_server = language_server_by_id!(editor, language_server_id);

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

0 comments on commit 5040679

Please sign in to comment.