Skip to content

Commit 076f63b

Browse files
committed
Add code actions on save
* Add code-actions-on-save config * Match VS Code config to allow future flexibility * Make Jobs::handle_callback() async to allow running code actions in callback * Refactor lsp commands to allow applying code actions from command and save * Retain only enabled code actions that do not have commands * Commands cannot be supported without adding a mechanism to await the async workspace edit * Attempt code actions for all language servers for the document * Block on didChange/textDocument to prevent race condition between code actions and auto-format * Add integration tests for gopls under test-lsp feature * Update documentation in book
1 parent 38e6fcd commit 076f63b

File tree

11 files changed

+518
-144
lines changed

11 files changed

+518
-144
lines changed

book/src/languages.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ These configuration keys are available:
7171
| `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` |
7272
| `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. |
7373
| `persistent-diagnostic-sources` | An array of LSP diagnostic sources assumed unchanged when the language server resends the same set of diagnostics. Helix can track the position for these diagnostics internally instead. Useful for diagnostics that are recomputed on save.
74+
| `code-actions-on-save` | List of LSP code actions to be run in order on save, for example `[{ code-action = "source.organizeImports", enabled = true }]` |
7475

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

helix-core/src/syntax.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ pub struct LanguageConfiguration {
118118
pub block_comment_tokens: Option<Vec<BlockCommentToken>>,
119119
pub text_width: Option<usize>,
120120
pub soft_wrap: Option<SoftWrap>,
121+
#[serde(default)]
122+
pub code_actions_on_save: Option<Vec<CodeActionsOnSave>>, // List of LSP code actions to be run in order upon saving
121123

122124
#[serde(default)]
123125
pub auto_format: bool,
@@ -490,6 +492,13 @@ pub struct AdvancedCompletion {
490492
pub default: Option<String>,
491493
}
492494

495+
#[derive(Debug, Clone, Serialize, Deserialize)]
496+
#[serde(rename_all = "kebab-case")]
497+
pub struct CodeActionsOnSave {
498+
pub code_action: String,
499+
pub enabled: bool,
500+
}
501+
493502
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
494503
#[serde(rename_all = "kebab-case", untagged)]
495504
pub enum DebugConfigCompletion {

helix-term/src/application.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ impl Application {
335335
self.handle_terminal_events(event).await;
336336
}
337337
Some(callback) = self.jobs.callbacks.recv() => {
338-
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, Ok(Some(callback)));
338+
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, Ok(Some(callback))).await;
339339
self.render().await;
340340
}
341341
Some(msg) = self.jobs.status_messages.recv() => {
@@ -350,7 +350,7 @@ impl Application {
350350
helix_event::request_redraw();
351351
}
352352
Some(callback) = self.jobs.wait_futures.next() => {
353-
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
353+
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback).await;
354354
self.render().await;
355355
}
356356
event = self.editor.wait_event() => {

helix-term/src/commands.rs

Lines changed: 98 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ use crate::{
5858
args,
5959
compositor::{self, Component, Compositor},
6060
filter_picker_entry,
61-
job::Callback,
61+
job::{Callback, OnSaveCallbackData},
6262
ui::{self, overlay::overlaid, Picker, PickerColumn, Popup, Prompt, PromptEvent},
6363
};
6464

@@ -3388,38 +3388,115 @@ async fn make_format_callback(
33883388
doc_version: i32,
33893389
view_id: ViewId,
33903390
format: impl Future<Output = Result<Transaction, FormatterError>> + Send + 'static,
3391-
write: Option<(Option<PathBuf>, bool)>,
33923391
) -> anyhow::Result<job::Callback> {
33933392
let format = format.await;
33943393

33953394
let call: job::Callback = Callback::Editor(Box::new(move |editor| {
3396-
if !editor.documents.contains_key(&doc_id) || !editor.tree.contains(view_id) {
3397-
return;
3395+
format_callback(doc_id, doc_version, view_id, format, editor);
3396+
}));
3397+
3398+
Ok(call)
3399+
}
3400+
3401+
pub fn format_callback(
3402+
doc_id: DocumentId,
3403+
doc_version: i32,
3404+
view_id: ViewId,
3405+
format: Result<Transaction, FormatterError>,
3406+
editor: &mut Editor,
3407+
) {
3408+
if !editor.documents.contains_key(&doc_id) || !editor.tree.contains(view_id) {
3409+
return;
3410+
}
3411+
3412+
let scrolloff = editor.config().scrolloff;
3413+
let doc = doc_mut!(editor, &doc_id);
3414+
let view = view_mut!(editor, view_id);
3415+
3416+
if let Ok(format) = format {
3417+
if doc.version() == doc_version {
3418+
doc.apply(&format, view.id);
3419+
doc.append_changes_to_history(view);
3420+
doc.detect_indent_and_line_ending();
3421+
view.ensure_cursor_in_view(doc, scrolloff);
3422+
} else {
3423+
log::info!("discarded formatting changes because the document changed");
33983424
}
3425+
}
3426+
}
33993427

3400-
let scrolloff = editor.config().scrolloff;
3401-
let doc = doc_mut!(editor, &doc_id);
3402-
let view = view_mut!(editor, view_id);
3428+
pub async fn on_save_callback(
3429+
editor: &mut Editor,
3430+
doc_id: DocumentId,
3431+
view_id: ViewId,
3432+
path: Option<PathBuf>,
3433+
force: bool,
3434+
) {
3435+
let doc = doc!(editor, &doc_id);
3436+
if let Some(code_actions_on_save_cfg) = doc
3437+
.language_config()
3438+
.map(|c| c.code_actions_on_save.clone())
3439+
.flatten()
3440+
{
3441+
for code_action_on_save_cfg in code_actions_on_save_cfg
3442+
.into_iter()
3443+
.filter_map(|action| action.enabled.then_some(action.code_action))
3444+
{
3445+
log::debug!(
3446+
"Attempting code action on save {:?}",
3447+
code_action_on_save_cfg
3448+
);
3449+
let doc = doc!(editor, &doc_id);
3450+
let code_actions = code_actions_on_save(doc, code_action_on_save_cfg.clone()).await;
34033451

3404-
if let Ok(format) = format {
3405-
if doc.version() == doc_version {
3406-
doc.apply(&format, view.id);
3407-
doc.append_changes_to_history(view);
3408-
doc.detect_indent_and_line_ending();
3409-
view.ensure_cursor_in_view(doc, scrolloff);
3410-
} else {
3411-
log::info!("discarded formatting changes because the document changed");
3452+
if code_actions.is_empty() {
3453+
log::debug!(
3454+
"Code action on save not found {:?}",
3455+
code_action_on_save_cfg
3456+
);
3457+
editor.set_error(format!(
3458+
"Code Action not found: {:?}",
3459+
code_action_on_save_cfg
3460+
));
34123461
}
3413-
}
34143462

3415-
if let Some((path, force)) = write {
3416-
let id = doc.id();
3417-
if let Err(err) = editor.save(id, path, force) {
3418-
editor.set_error(format!("Error saving: {}", err));
3463+
for code_action in code_actions {
3464+
log::debug!(
3465+
"Applying code action on save {:?} for language server {:?}",
3466+
code_action.lsp_item,
3467+
code_action.language_server_id
3468+
);
3469+
apply_code_action(editor, &code_action);
34193470
}
34203471
}
3421-
}));
3472+
}
34223473

3474+
if editor.config().auto_format {
3475+
let doc = doc!(editor, &doc_id);
3476+
if let Some(fmt) = doc.auto_format() {
3477+
format_callback(doc.id(), doc.version(), view_id, fmt.await, editor);
3478+
}
3479+
}
3480+
3481+
if let Err(err) = editor.save::<PathBuf>(doc_id, path, force) {
3482+
editor.set_error(format!("Error saving: {}", err));
3483+
}
3484+
}
3485+
3486+
pub async fn make_on_save_callback(
3487+
doc_id: DocumentId,
3488+
view_id: ViewId,
3489+
path: Option<PathBuf>,
3490+
force: bool,
3491+
) -> anyhow::Result<job::Callback> {
3492+
let call: job::Callback = Callback::OnSave(Box::new({
3493+
OnSaveCallbackData {
3494+
doc_id,
3495+
view_id,
3496+
path,
3497+
force,
3498+
}
3499+
}));
34233500
Ok(call)
34243501
}
34253502

0 commit comments

Comments
 (0)