diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 1113b44ecdf7f..217d8a7104e77 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -452,43 +452,77 @@ pub fn code_action(cx: &mut Context) { cx.callback( future, move |editor, compositor, response: Option| { - let actions = match response { + let mut actions = match response { Some(a) => a, None => return, }; + if actions.is_empty() { editor.set_status("No code actions available"); return; } - let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| { - if event != PromptEvent::Validate { - return; - } + // sort by CodeActionKind + // this ensures that the most relevant codeactions (quickfix) show up first + // while more situational commands (like refactors) show up later + // this behaviour is modeled after the behaviour of vscode (editor/contrib/codeAction/browser/codeActionWidget.ts) + + let mut categories = vec![Vec::new(); 8]; + for action in actions.drain(..) { + let category = match &action { + lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { + kind: Some(kind), + .. + }) => match kind.as_str() { + "quickfix" => 0, + "refactor.extract" => 1, + "refactor.inline" => 2, + "refactor.rewrite" => 3, + "refactor.move" => 4, + "refactor.surround" => 5, + "source" => 6, + _ => 7, + }, + _ => 7, + }; + log::error!("{category}"); - // always present here - let code_action = code_action.unwrap(); + categories[category].push(action); + } + + for category in categories { + actions.extend(category.into_iter()) + } - match code_action { - lsp::CodeActionOrCommand::Command(command) => { - log::debug!("code action command: {:?}", command); - execute_lsp_command(editor, command.clone()); + let mut picker = + ui::Menu::new(actions, false, (), move |editor, code_action, event| { + if event != PromptEvent::Validate { + return; } - 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); - 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 { + // 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); + 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()); + } + } } - } - }); + }); picker.move_down(); // pre-select the first item let popup = Popup::new("code-action", picker); diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 2d7d4f9256959..7817f7db0b707 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -97,7 +97,7 @@ impl Completion { start_offset: usize, trigger_offset: usize, ) -> Self { - let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| { + let menu = Menu::new(items, true, (), move |editor: &mut Editor, item, event| { fn item_to_transaction( doc: &Document, item: &CompletionItem, diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 1d247b1adc822..4a9670bd7c603 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -74,6 +74,7 @@ impl Menu { // rendering) pub fn new( options: Vec, + sort: bool, editor_data: ::Data, callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static, ) -> Self { @@ -91,8 +92,12 @@ impl Menu { recalculate: true, }; - // TODO: scoring on empty input should just use a fastpath - menu.score(""); + if sort { + // TODO: scoring on empty input should just use a fastpath + menu.score(""); + } else { + menu.matches = (0..menu.options.len()).map(|i| (i, 0)).collect(); + } menu }