Skip to content

Commit 0365774

Browse files
committed
Add code actions on save
1 parent ca65d31 commit 0365774

File tree

5 files changed

+131
-45
lines changed

5 files changed

+131
-45
lines changed

book/src/languages.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ These configuration keys are available:
6565
| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
6666
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set, defaults to `editor.text-width` |
6767
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. |
68+
| `code-actions-on-save` | List of LSP code actions to be run in order on save, for example `["source.organizeImports"]` |
6869

6970
### File-type detection and the `file-types` key
7071

helix-core/src/syntax.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use slotmap::{DefaultKey as LayerId, HopSlotMap};
1616
use std::{
1717
borrow::Cow,
1818
cell::RefCell,
19-
collections::{HashMap, VecDeque},
19+
collections::{HashMap, HashSet, VecDeque},
2020
fmt,
2121
hash::{Hash, Hasher},
2222
mem::{replace, transmute},
@@ -84,6 +84,8 @@ pub struct LanguageConfiguration {
8484
pub comment_token: Option<String>,
8585
pub text_width: Option<usize>,
8686
pub soft_wrap: Option<SoftWrap>,
87+
#[serde(default)]
88+
pub code_actions_on_save: HashSet<String>, // List of LSP code actions to be run in order upon saving
8789

8890
#[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
8991
pub config: Option<serde_json::Value>,

helix-term/src/commands.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2752,6 +2752,22 @@ async fn make_format_callback(
27522752
Ok(call)
27532753
}
27542754

2755+
async fn make_code_actions_on_save_callback(
2756+
future: impl Future<Output = Result<Vec<helix_lsp::lsp::CodeActionOrCommand>, anyhow::Error>>
2757+
+ Send
2758+
+ 'static,
2759+
) -> anyhow::Result<job::Callback> {
2760+
let code_actions = future.await?;
2761+
let call: job::Callback = Callback::Editor(Box::new(move |editor: &mut Editor| {
2762+
log::debug!("Applying code actions on save {:?}", code_actions);
2763+
code_actions
2764+
.iter()
2765+
.map(|code_action| apply_code_action(editor, code_action))
2766+
.collect()
2767+
}));
2768+
Ok(call)
2769+
}
2770+
27552771
#[derive(PartialEq, Eq)]
27562772
pub enum Open {
27572773
Below,

helix-term/src/commands/lsp.rs

Lines changed: 101 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use futures_util::FutureExt;
1+
use futures_util::{future::BoxFuture, FutureExt};
22
use helix_lsp::{
33
block_on,
44
lsp::{
@@ -15,7 +15,7 @@ use tui::{
1515

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

18-
use helix_core::{path, text_annotations::InlineAnnotation, Selection};
18+
use helix_core::{path, text_annotations::InlineAnnotation, Range, Selection};
1919
use helix_view::{
2020
document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
2121
editor::Action,
@@ -544,31 +544,9 @@ fn action_fixes_diagnostics(action: &CodeActionOrCommand) -> bool {
544544
pub fn code_action(cx: &mut Context) {
545545
let (view, doc) = current!(cx.editor);
546546

547-
let language_server = language_server!(cx.editor, doc);
548-
549547
let selection_range = doc.selection(view.id).primary();
550-
let offset_encoding = language_server.offset_encoding();
551548

552-
let range = range_to_lsp_range(doc.text(), selection_range, offset_encoding);
553-
554-
let future = match language_server.code_actions(
555-
doc.identifier(),
556-
range,
557-
// Filter and convert overlapping diagnostics
558-
lsp::CodeActionContext {
559-
diagnostics: doc
560-
.diagnostics()
561-
.iter()
562-
.filter(|&diag| {
563-
selection_range
564-
.overlaps(&helix_core::Range::new(diag.range.start, diag.range.end))
565-
})
566-
.map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding))
567-
.collect(),
568-
only: None,
569-
trigger_kind: Some(CodeActionTriggerKind::INVOKED),
570-
},
571-
) {
549+
let future = match code_actions_for_range(doc, selection_range) {
572550
Some(future) => future,
573551
None => {
574552
cx.editor
@@ -642,25 +620,7 @@ pub fn code_action(cx: &mut Context) {
642620
// always present here
643621
let code_action = code_action.unwrap();
644622

645-
match code_action {
646-
lsp::CodeActionOrCommand::Command(command) => {
647-
log::debug!("code action command: {:?}", command);
648-
execute_lsp_command(editor, command.clone());
649-
}
650-
lsp::CodeActionOrCommand::CodeAction(code_action) => {
651-
log::debug!("code action: {:?}", code_action);
652-
if let Some(ref workspace_edit) = code_action.edit {
653-
log::debug!("edit: {:?}", workspace_edit);
654-
let _ = apply_workspace_edit(editor, offset_encoding, workspace_edit);
655-
}
656-
657-
// if code action provides both edit and command first the edit
658-
// should be applied and then the command
659-
if let Some(command) = &code_action.command {
660-
execute_lsp_command(editor, command.clone());
661-
}
662-
}
663-
}
623+
apply_code_action(editor, code_action);
664624
});
665625
picker.move_down(); // pre-select the first item
666626

@@ -670,6 +630,103 @@ pub fn code_action(cx: &mut Context) {
670630
)
671631
}
672632

633+
pub fn code_actions_for_range(
634+
doc: &mut Document,
635+
range: helix_core::Range,
636+
) -> Option<impl Future<Output = Result<serde_json::Value, helix_lsp::Error>>> {
637+
let language_server = doc.language_server()?;
638+
let offset_encoding = language_server.offset_encoding();
639+
let lsp_range = range_to_lsp_range(doc.text(), range, offset_encoding);
640+
641+
language_server.code_actions(
642+
doc.identifier(),
643+
lsp_range,
644+
// Filter and convert overlapping diagnostics
645+
lsp::CodeActionContext {
646+
diagnostics: doc
647+
.diagnostics()
648+
.iter()
649+
.filter(|&diag| {
650+
range.overlaps(&helix_core::Range::new(diag.range.start, diag.range.end))
651+
})
652+
.map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding))
653+
.collect(),
654+
only: None,
655+
trigger_kind: Some(CodeActionTriggerKind::INVOKED),
656+
},
657+
)
658+
}
659+
660+
pub fn code_actions_on_save(
661+
doc: &mut Document,
662+
) -> Option<BoxFuture<'static, Result<Vec<helix_lsp::lsp::CodeActionOrCommand>, anyhow::Error>>> {
663+
let code_actions_on_save = doc
664+
.language_config()
665+
.map(|c| c.code_actions_on_save.clone())?;
666+
667+
if code_actions_on_save.is_empty() {
668+
return None;
669+
}
670+
671+
let full_range = Range::new(0, doc.text().len_chars());
672+
let request = code_actions_for_range(doc, full_range)?;
673+
674+
let fut = async move {
675+
log::debug!("Configured code actions on save {:?}", code_actions_on_save);
676+
let json = request.await?;
677+
let response: Option<helix_lsp::lsp::CodeActionResponse> = serde_json::from_value(json)?;
678+
let available_code_actions = match response {
679+
Some(value) => value,
680+
None => helix_lsp::lsp::CodeActionResponse::default(),
681+
};
682+
log::debug!("Available code actions {:?}", available_code_actions);
683+
684+
let code_actions: Vec<CodeActionOrCommand> = available_code_actions
685+
.into_iter()
686+
.filter(|action| match action {
687+
helix_lsp::lsp::CodeActionOrCommand::CodeAction(x) if x.disabled.is_none() => {
688+
match &x.kind {
689+
Some(kind) => code_actions_on_save.get(kind.as_str()).is_some(),
690+
None => false,
691+
}
692+
}
693+
_ => false,
694+
})
695+
.collect();
696+
697+
Ok(code_actions)
698+
};
699+
Some(fut.boxed())
700+
}
701+
702+
pub fn apply_code_action(editor: &mut Editor, code_action: &CodeActionOrCommand) {
703+
let (_view, doc) = current!(editor);
704+
705+
let language_server = language_server!(editor, doc);
706+
707+
let offset_encoding = language_server.offset_encoding();
708+
709+
match code_action {
710+
lsp::CodeActionOrCommand::Command(command) => {
711+
log::debug!("code action command: {:?}", command);
712+
execute_lsp_command(editor, command.clone());
713+
}
714+
lsp::CodeActionOrCommand::CodeAction(code_action) => {
715+
log::debug!("code action: {:?}", code_action);
716+
if let Some(ref workspace_edit) = code_action.edit {
717+
log::debug!("edit: {:?}", workspace_edit);
718+
let _ = apply_workspace_edit(editor, offset_encoding, workspace_edit);
719+
}
720+
721+
// if code action provides both edit and command first the edit
722+
// should be applied and then the command
723+
if let Some(command) = &code_action.command {
724+
execute_lsp_command(editor, command.clone());
725+
}
726+
}
727+
}
728+
}
729+
673730
impl ui::menu::Item for lsp::Command {
674731
type Data = ();
675732
fn format(&self, _data: &Self::Data) -> Row {

helix-term/src/commands/typed.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,11 @@ fn write_impl(
334334
let (view, doc) = current!(cx.editor);
335335
let path = path.map(AsRef::as_ref);
336336

337+
if let Some(future) = code_actions_on_save(doc) {
338+
let callback = make_code_actions_on_save_callback(future);
339+
jobs.add(Job::with_callback(callback).wait_before_exiting());
340+
}
341+
337342
let fmt = if editor_auto_fmt {
338343
doc.auto_format().map(|fmt| {
339344
let callback = make_format_callback(
@@ -647,6 +652,11 @@ pub fn write_all_impl(
647652
return None;
648653
}
649654

655+
if let Some(future) = code_actions_on_save(doc) {
656+
let callback = make_code_actions_on_save_callback(future);
657+
jobs.add(Job::with_callback(callback).wait_before_exiting());
658+
}
659+
650660
// Look for a view to apply the formatting change to. If the document
651661
// is in the current view, just use that. Otherwise, since we don't
652662
// have any other metric available for better selection, just pick

0 commit comments

Comments
 (0)