Skip to content

Commit f4c9f40

Browse files
committed
Add code actions on save
1 parent 5b3dd6a commit f4c9f40

File tree

5 files changed

+149
-48
lines changed

5 files changed

+149
-48
lines changed

helix-core/src/syntax.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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: Vec<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
@@ -2745,6 +2745,22 @@ async fn make_format_callback(
27452745
Ok(call)
27462746
}
27472747

2748+
async fn make_code_actions_on_save_callback(
2749+
future: impl Future<Output = Result<Vec<helix_lsp::lsp::CodeActionOrCommand>, anyhow::Error>>
2750+
+ Send
2751+
+ 'static,
2752+
) -> anyhow::Result<job::Callback> {
2753+
let code_actions = future.await?;
2754+
let call: job::Callback = Callback::Editor(Box::new(move |editor: &mut Editor| {
2755+
log::debug!("Applying code actions on save {:?}", code_actions);
2756+
code_actions
2757+
.iter()
2758+
.map(|code_action| apply_code_action(editor, code_action))
2759+
.collect()
2760+
}));
2761+
Ok(call)
2762+
}
2763+
27482764
#[derive(PartialEq, Eq)]
27492765
pub enum Open {
27502766
Below,

helix-term/src/commands/lsp.rs

Lines changed: 32 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
use futures_util::FutureExt;
22
use helix_lsp::{
33
block_on,
4-
lsp::{
5-
self, CodeAction, CodeActionOrCommand, CodeActionTriggerKind, DiagnosticSeverity,
6-
NumberOrString,
7-
},
8-
util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
4+
lsp::{self, CodeAction, CodeActionOrCommand, DiagnosticSeverity, NumberOrString},
5+
util::lsp_range_to_range,
96
OffsetEncoding,
107
};
118
use tui::{
@@ -544,31 +541,9 @@ fn action_fixes_diagnostics(action: &CodeActionOrCommand) -> bool {
544541
pub fn code_action(cx: &mut Context) {
545542
let (view, doc) = current!(cx.editor);
546543

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

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-
) {
546+
let future = match doc.code_actions(selection_range) {
572547
Some(future) => future,
573548
None => {
574549
cx.editor
@@ -642,25 +617,7 @@ pub fn code_action(cx: &mut Context) {
642617
// always present here
643618
let code_action = code_action.unwrap();
644619

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-
}
620+
apply_code_action(editor, code_action);
664621
});
665622
picker.move_down(); // pre-select the first item
666623

@@ -670,6 +627,34 @@ pub fn code_action(cx: &mut Context) {
670627
)
671628
}
672629

630+
pub fn apply_code_action(editor: &mut Editor, code_action: &CodeActionOrCommand) {
631+
let (_view, doc) = current!(editor);
632+
633+
let language_server = language_server!(editor, doc);
634+
635+
let offset_encoding = language_server.offset_encoding();
636+
637+
match code_action {
638+
lsp::CodeActionOrCommand::Command(command) => {
639+
log::debug!("code action command: {:?}", command);
640+
execute_lsp_command(editor, command.clone());
641+
}
642+
lsp::CodeActionOrCommand::CodeAction(code_action) => {
643+
log::debug!("code action: {:?}", code_action);
644+
if let Some(ref workspace_edit) = code_action.edit {
645+
log::debug!("edit: {:?}", workspace_edit);
646+
let _ = apply_workspace_edit(editor, offset_encoding, workspace_edit);
647+
}
648+
649+
// if code action provides both edit and command first the edit
650+
// should be applied and then the command
651+
if let Some(command) = &code_action.command {
652+
execute_lsp_command(editor, command.clone());
653+
}
654+
}
655+
}
656+
}
657+
673658
impl ui::menu::Item for lsp::Command {
674659
type Data = ();
675660
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) = doc.code_actions_on_save() {
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) = doc.code_actions_on_save() {
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

helix-view/src/document.rs

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use helix_core::doc_formatter::TextFormat;
88
use helix_core::syntax::Highlight;
99
use helix_core::text_annotations::{InlineAnnotation, TextAnnotations};
1010
use helix_core::Range;
11+
use helix_lsp::util::{diagnostic_to_lsp_diagnostic, range_to_lsp_range};
1112
use helix_vcs::{DiffHandle, DiffProviderRegistry};
1213

1314
use ::parking_lot::Mutex;
@@ -457,7 +458,7 @@ where
457458
*mut_ref = f(mem::take(mut_ref));
458459
}
459460

460-
use helix_lsp::lsp;
461+
use helix_lsp::lsp::{self, CodeActionTriggerKind};
461462
use url::Url;
462463

463464
impl Document {
@@ -633,6 +634,88 @@ impl Document {
633634
Some(fut.boxed())
634635
}
635636

637+
pub fn code_actions_on_save(
638+
&self,
639+
) -> Option<BoxFuture<'static, Result<Vec<helix_lsp::lsp::CodeActionOrCommand>, anyhow::Error>>>
640+
{
641+
let code_actions_on_save = self
642+
.language_config()
643+
.and_then(|c| Some(c.code_actions_on_save.clone()))?;
644+
645+
if code_actions_on_save.is_empty() {
646+
return None;
647+
}
648+
649+
let request = self.code_actions(self.full_range())?;
650+
651+
let fut = async move {
652+
log::debug!("Configured code actions on save {:?}", code_actions_on_save);
653+
let json = request.await?;
654+
let response: Option<helix_lsp::lsp::CodeActionResponse> =
655+
serde_json::from_value(json)?;
656+
let mut code_actions = match response {
657+
Some(value) => value,
658+
None => helix_lsp::lsp::CodeActionResponse::default(),
659+
};
660+
log::debug!("Available code actions {:?}", code_actions);
661+
code_actions.retain(|action| {
662+
matches!(
663+
action,
664+
helix_lsp::lsp::CodeActionOrCommand::CodeAction(x) if x.disabled == None &&
665+
code_actions_on_save.iter().any(|a| match &x.kind {
666+
Some(kind) => kind.as_str() == a,
667+
None => false
668+
})
669+
)
670+
});
671+
if code_actions.len() < code_actions_on_save.len() {
672+
code_actions_on_save.iter().for_each(|configured_action| {
673+
if !code_actions.iter().any(|action| match action {
674+
helix_lsp::lsp::CodeActionOrCommand::CodeAction(x) => match &x.kind {
675+
Some(kind) => kind.as_str() == configured_action,
676+
None => false,
677+
},
678+
_ => false,
679+
}) {
680+
log::error!(
681+
"Configured code action on save is invalid {:?}",
682+
configured_action
683+
);
684+
}
685+
})
686+
}
687+
return Ok(code_actions);
688+
};
689+
Some(fut.boxed())
690+
}
691+
692+
pub fn code_actions(
693+
&self,
694+
range: Range,
695+
) -> Option<impl Future<Output = Result<serde_json::Value, helix_lsp::Error>>> {
696+
let language_server = self.language_server()?;
697+
let offset_encoding = language_server.offset_encoding();
698+
let lsp_range = range_to_lsp_range(self.text(), range, offset_encoding);
699+
700+
language_server.code_actions(
701+
self.identifier(),
702+
lsp_range,
703+
// Filter and convert overlapping diagnostics
704+
lsp::CodeActionContext {
705+
diagnostics: self
706+
.diagnostics()
707+
.iter()
708+
.filter(|&diag| {
709+
range.overlaps(&helix_core::Range::new(diag.range.start, diag.range.end))
710+
})
711+
.map(|diag| diagnostic_to_lsp_diagnostic(self.text(), diag, offset_encoding))
712+
.collect(),
713+
only: None,
714+
trigger_kind: Some(CodeActionTriggerKind::INVOKED),
715+
},
716+
)
717+
}
718+
636719
pub fn save<P: Into<PathBuf>>(
637720
&mut self,
638721
path: Option<P>,
@@ -1331,6 +1414,11 @@ impl Document {
13311414
&self.text
13321415
}
13331416

1417+
#[inline]
1418+
pub fn full_range(&self) -> Range {
1419+
Range::new(0, self.text.len_chars())
1420+
}
1421+
13341422
#[inline]
13351423
pub fn selection(&self, view_id: ViewId) -> &Selection {
13361424
&self.selections[&view_id]

0 commit comments

Comments
 (0)