Skip to content

Commit

Permalink
Add code actions on save
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jpttrssn committed Aug 22, 2024
1 parent 38e6fcd commit 076f63b
Show file tree
Hide file tree
Showing 11 changed files with 518 additions and 144 deletions.
1 change: 1 addition & 0 deletions book/src/languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ These configuration keys are available:
| `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. |
| `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.
| `code-actions-on-save` | List of LSP code actions to be run in order on save, for example `[{ code-action = "source.organizeImports", enabled = true }]` |

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

Expand Down
9 changes: 9 additions & 0 deletions helix-core/src/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ pub struct LanguageConfiguration {
pub block_comment_tokens: Option<Vec<BlockCommentToken>>,
pub text_width: Option<usize>,
pub soft_wrap: Option<SoftWrap>,
#[serde(default)]
pub code_actions_on_save: Option<Vec<CodeActionsOnSave>>, // List of LSP code actions to be run in order upon saving

#[serde(default)]
pub auto_format: bool,
Expand Down Expand Up @@ -490,6 +492,13 @@ pub struct AdvancedCompletion {
pub default: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct CodeActionsOnSave {
pub code_action: String,
pub enabled: bool,
}

#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", untagged)]
pub enum DebugConfigCompletion {
Expand Down
4 changes: 2 additions & 2 deletions helix-term/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ impl Application {
self.handle_terminal_events(event).await;
}
Some(callback) = self.jobs.callbacks.recv() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, Ok(Some(callback)));
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, Ok(Some(callback))).await;
self.render().await;
}
Some(msg) = self.jobs.status_messages.recv() => {
Expand All @@ -350,7 +350,7 @@ impl Application {
helix_event::request_redraw();
}
Some(callback) = self.jobs.wait_futures.next() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback).await;
self.render().await;
}
event = self.editor.wait_event() => {
Expand Down
119 changes: 98 additions & 21 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ use crate::{
args,
compositor::{self, Component, Compositor},
filter_picker_entry,
job::Callback,
job::{Callback, OnSaveCallbackData},
ui::{self, overlay::overlaid, Picker, PickerColumn, Popup, Prompt, PromptEvent},
};

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

let call: job::Callback = Callback::Editor(Box::new(move |editor| {
if !editor.documents.contains_key(&doc_id) || !editor.tree.contains(view_id) {
return;
format_callback(doc_id, doc_version, view_id, format, editor);
}));

Ok(call)
}

pub fn format_callback(
doc_id: DocumentId,
doc_version: i32,
view_id: ViewId,
format: Result<Transaction, FormatterError>,
editor: &mut Editor,
) {
if !editor.documents.contains_key(&doc_id) || !editor.tree.contains(view_id) {
return;
}

let scrolloff = editor.config().scrolloff;
let doc = doc_mut!(editor, &doc_id);
let view = view_mut!(editor, view_id);

if let Ok(format) = format {
if doc.version() == doc_version {
doc.apply(&format, view.id);
doc.append_changes_to_history(view);
doc.detect_indent_and_line_ending();
view.ensure_cursor_in_view(doc, scrolloff);
} else {
log::info!("discarded formatting changes because the document changed");
}
}
}

let scrolloff = editor.config().scrolloff;
let doc = doc_mut!(editor, &doc_id);
let view = view_mut!(editor, view_id);
pub async fn on_save_callback(
editor: &mut Editor,
doc_id: DocumentId,
view_id: ViewId,
path: Option<PathBuf>,
force: bool,
) {
let doc = doc!(editor, &doc_id);
if let Some(code_actions_on_save_cfg) = doc
.language_config()
.map(|c| c.code_actions_on_save.clone())
.flatten()
{
for code_action_on_save_cfg in code_actions_on_save_cfg
.into_iter()
.filter_map(|action| action.enabled.then_some(action.code_action))
{
log::debug!(
"Attempting code action on save {:?}",
code_action_on_save_cfg
);
let doc = doc!(editor, &doc_id);
let code_actions = code_actions_on_save(doc, code_action_on_save_cfg.clone()).await;

if let Ok(format) = format {
if doc.version() == doc_version {
doc.apply(&format, view.id);
doc.append_changes_to_history(view);
doc.detect_indent_and_line_ending();
view.ensure_cursor_in_view(doc, scrolloff);
} else {
log::info!("discarded formatting changes because the document changed");
if code_actions.is_empty() {
log::debug!(
"Code action on save not found {:?}",
code_action_on_save_cfg
);
editor.set_error(format!(
"Code Action not found: {:?}",
code_action_on_save_cfg
));
}
}

if let Some((path, force)) = write {
let id = doc.id();
if let Err(err) = editor.save(id, path, force) {
editor.set_error(format!("Error saving: {}", err));
for code_action in code_actions {
log::debug!(
"Applying code action on save {:?} for language server {:?}",
code_action.lsp_item,
code_action.language_server_id
);
apply_code_action(editor, &code_action);
}
}
}));
}

if editor.config().auto_format {
let doc = doc!(editor, &doc_id);
if let Some(fmt) = doc.auto_format() {
format_callback(doc.id(), doc.version(), view_id, fmt.await, editor);
}
}

if let Err(err) = editor.save::<PathBuf>(doc_id, path, force) {
editor.set_error(format!("Error saving: {}", err));
}
}

pub async fn make_on_save_callback(
doc_id: DocumentId,
view_id: ViewId,
path: Option<PathBuf>,
force: bool,
) -> anyhow::Result<job::Callback> {
let call: job::Callback = Callback::OnSave(Box::new({
OnSaveCallbackData {
doc_id,
view_id,
path,
force,
}
}));
Ok(call)
}

Expand Down
Loading

0 comments on commit 076f63b

Please sign in to comment.