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 Mar 30, 2023
1 parent 5b3dd6a commit f4c9f40
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 48 deletions.
2 changes: 2 additions & 0 deletions helix-core/src/syntax.rs
Original file line number Diff line number Diff line change
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: Vec<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 @@ -2745,6 +2745,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
79 changes: 32 additions & 47 deletions helix-term/src/commands/lsp.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
use futures_util::FutureExt;
use helix_lsp::{
block_on,
lsp::{
self, CodeAction, CodeActionOrCommand, CodeActionTriggerKind, DiagnosticSeverity,
NumberOrString,
},
util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
lsp::{self, CodeAction, CodeActionOrCommand, DiagnosticSeverity, NumberOrString},
util::lsp_range_to_range,
OffsetEncoding,
};
use tui::{
Expand Down Expand Up @@ -544,31 +541,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 doc.code_actions(selection_range) {
Some(future) => future,
None => {
cx.editor
Expand Down Expand Up @@ -642,25 +617,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 +627,34 @@ pub fn code_action(cx: &mut Context) {
)
}

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) = doc.code_actions_on_save() {
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) = doc.code_actions_on_save() {
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
90 changes: 89 additions & 1 deletion helix-view/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use helix_core::doc_formatter::TextFormat;
use helix_core::syntax::Highlight;
use helix_core::text_annotations::{InlineAnnotation, TextAnnotations};
use helix_core::Range;
use helix_lsp::util::{diagnostic_to_lsp_diagnostic, range_to_lsp_range};
use helix_vcs::{DiffHandle, DiffProviderRegistry};

use ::parking_lot::Mutex;
Expand Down Expand Up @@ -457,7 +458,7 @@ where
*mut_ref = f(mem::take(mut_ref));
}

use helix_lsp::lsp;
use helix_lsp::lsp::{self, CodeActionTriggerKind};
use url::Url;

impl Document {
Expand Down Expand Up @@ -633,6 +634,88 @@ impl Document {
Some(fut.boxed())
}

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

if code_actions_on_save.is_empty() {
return None;
}

let request = self.code_actions(self.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 mut code_actions = match response {
Some(value) => value,
None => helix_lsp::lsp::CodeActionResponse::default(),
};
log::debug!("Available code actions {:?}", code_actions);
code_actions.retain(|action| {
matches!(
action,
helix_lsp::lsp::CodeActionOrCommand::CodeAction(x) if x.disabled == None &&
code_actions_on_save.iter().any(|a| match &x.kind {
Some(kind) => kind.as_str() == a,
None => false
})
)
});
if code_actions.len() < code_actions_on_save.len() {
code_actions_on_save.iter().for_each(|configured_action| {
if !code_actions.iter().any(|action| match action {
helix_lsp::lsp::CodeActionOrCommand::CodeAction(x) => match &x.kind {
Some(kind) => kind.as_str() == configured_action,
None => false,
},
_ => false,
}) {
log::error!(
"Configured code action on save is invalid {:?}",
configured_action
);
}
})
}
return Ok(code_actions);
};
Some(fut.boxed())
}

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

language_server.code_actions(
self.identifier(),
lsp_range,
// Filter and convert overlapping diagnostics
lsp::CodeActionContext {
diagnostics: self
.diagnostics()
.iter()
.filter(|&diag| {
range.overlaps(&helix_core::Range::new(diag.range.start, diag.range.end))
})
.map(|diag| diagnostic_to_lsp_diagnostic(self.text(), diag, offset_encoding))
.collect(),
only: None,
trigger_kind: Some(CodeActionTriggerKind::INVOKED),
},
)
}

pub fn save<P: Into<PathBuf>>(
&mut self,
path: Option<P>,
Expand Down Expand Up @@ -1331,6 +1414,11 @@ impl Document {
&self.text
}

#[inline]
pub fn full_range(&self) -> Range {
Range::new(0, self.text.len_chars())
}

#[inline]
pub fn selection(&self, view_id: ViewId) -> &Selection {
&self.selections[&view_id]
Expand Down

0 comments on commit f4c9f40

Please sign in to comment.