Skip to content

Commit

Permalink
Add code actions on save
Browse files Browse the repository at this point in the history
  • Loading branch information
jpttrssn committed Apr 18, 2023
1 parent ca65d31 commit 0365774
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 45 deletions.
1 change: 1 addition & 0 deletions book/src/languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ These configuration keys are available:
| `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 |
| `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` |
| `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. |
| `code-actions-on-save` | List of LSP code actions to be run in order on save, for example `["source.organizeImports"]` |

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

Expand Down
4 changes: 3 additions & 1 deletion helix-core/src/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use slotmap::{DefaultKey as LayerId, HopSlotMap};
use std::{
borrow::Cow,
cell::RefCell,
collections::{HashMap, VecDeque},
collections::{HashMap, HashSet, VecDeque},
fmt,
hash::{Hash, Hasher},
mem::{replace, transmute},
Expand Down Expand Up @@ -84,6 +84,8 @@ pub struct LanguageConfiguration {
pub comment_token: Option<String>,
pub text_width: Option<usize>,
pub soft_wrap: Option<SoftWrap>,
#[serde(default)]
pub code_actions_on_save: HashSet<String>, // List of LSP code actions to be run in order upon saving

#[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
pub config: Option<serde_json::Value>,
Expand Down
16 changes: 16 additions & 0 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2752,6 +2752,22 @@ async fn make_format_callback(
Ok(call)
}

async fn make_code_actions_on_save_callback(
future: impl Future<Output = Result<Vec<helix_lsp::lsp::CodeActionOrCommand>, anyhow::Error>>
+ Send
+ 'static,
) -> anyhow::Result<job::Callback> {
let code_actions = future.await?;
let call: job::Callback = Callback::Editor(Box::new(move |editor: &mut Editor| {
log::debug!("Applying code actions on save {:?}", code_actions);
code_actions
.iter()
.map(|code_action| apply_code_action(editor, code_action))
.collect()
}));
Ok(call)
}

#[derive(PartialEq, Eq)]
pub enum Open {
Below,
Expand Down
145 changes: 101 additions & 44 deletions helix-term/src/commands/lsp.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use futures_util::FutureExt;
use futures_util::{future::BoxFuture, FutureExt};
use helix_lsp::{
block_on,
lsp::{
Expand All @@ -15,7 +15,7 @@ use tui::{

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

use helix_core::{path, text_annotations::InlineAnnotation, Selection};
use helix_core::{path, text_annotations::InlineAnnotation, Range, Selection};
use helix_view::{
document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
editor::Action,
Expand Down Expand Up @@ -544,31 +544,9 @@ fn action_fixes_diagnostics(action: &CodeActionOrCommand) -> bool {
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 = match 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,
trigger_kind: Some(CodeActionTriggerKind::INVOKED),
},
) {
let future = match code_actions_for_range(doc, selection_range) {
Some(future) => future,
None => {
cx.editor
Expand Down Expand Up @@ -642,25 +620,7 @@ pub fn code_action(cx: &mut Context) {
// always present here
let code_action = code_action.unwrap();

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);
let _ = 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, command.clone());
}
}
}
apply_code_action(editor, code_action);
});
picker.move_down(); // pre-select the first item

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

pub fn code_actions_for_range(
doc: &mut Document,
range: helix_core::Range,
) -> Option<impl Future<Output = Result<serde_json::Value, helix_lsp::Error>>> {
let language_server = doc.language_server()?;
let offset_encoding = language_server.offset_encoding();
let lsp_range = range_to_lsp_range(doc.text(), range, offset_encoding);

language_server.code_actions(
doc.identifier(),
lsp_range,
// Filter and convert overlapping diagnostics
lsp::CodeActionContext {
diagnostics: doc
.diagnostics()
.iter()
.filter(|&diag| {
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,
trigger_kind: Some(CodeActionTriggerKind::INVOKED),
},
)
}

pub fn code_actions_on_save(
doc: &mut Document,
) -> Option<BoxFuture<'static, Result<Vec<helix_lsp::lsp::CodeActionOrCommand>, anyhow::Error>>> {
let code_actions_on_save = doc
.language_config()
.map(|c| c.code_actions_on_save.clone())?;

if code_actions_on_save.is_empty() {
return None;
}

let full_range = Range::new(0, doc.text().len_chars());
let request = code_actions_for_range(doc, full_range)?;

let fut = async move {
log::debug!("Configured code actions on save {:?}", code_actions_on_save);
let json = request.await?;
let response: Option<helix_lsp::lsp::CodeActionResponse> = serde_json::from_value(json)?;
let available_code_actions = match response {
Some(value) => value,
None => helix_lsp::lsp::CodeActionResponse::default(),
};
log::debug!("Available code actions {:?}", available_code_actions);

let code_actions: Vec<CodeActionOrCommand> = available_code_actions
.into_iter()
.filter(|action| match action {
helix_lsp::lsp::CodeActionOrCommand::CodeAction(x) if x.disabled.is_none() => {
match &x.kind {
Some(kind) => code_actions_on_save.get(kind.as_str()).is_some(),
None => false,
}
}
_ => false,
})
.collect();

Ok(code_actions)
};
Some(fut.boxed())
}

pub fn apply_code_action(editor: &mut Editor, code_action: &CodeActionOrCommand) {
let (_view, doc) = current!(editor);

let language_server = language_server!(editor, doc);

let offset_encoding = language_server.offset_encoding();

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);
let _ = 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, command.clone());
}
}
}
}

impl ui::menu::Item for lsp::Command {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
Expand Down
10 changes: 10 additions & 0 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,11 @@ fn write_impl(
let (view, doc) = current!(cx.editor);
let path = path.map(AsRef::as_ref);

if let Some(future) = code_actions_on_save(doc) {
let callback = make_code_actions_on_save_callback(future);
jobs.add(Job::with_callback(callback).wait_before_exiting());
}

let fmt = if editor_auto_fmt {
doc.auto_format().map(|fmt| {
let callback = make_format_callback(
Expand Down Expand Up @@ -647,6 +652,11 @@ pub fn write_all_impl(
return None;
}

if let Some(future) = code_actions_on_save(doc) {
let callback = make_code_actions_on_save_callback(future);
jobs.add(Job::with_callback(callback).wait_before_exiting());
}

// Look for a view to apply the formatting change to. If the document
// is in the current view, just use that. Otherwise, since we don't
// have any other metric available for better selection, just pick
Expand Down

0 comments on commit 0365774

Please sign in to comment.