Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve sorting for inline menu (codeaction + completion) #4134

Merged
merged 6 commits into from
Oct 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 105 additions & 4 deletions helix-term/src/commands/lsp.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use helix_lsp::{
block_on,
lsp::{self, DiagnosticSeverity, NumberOrString},
lsp::{self, CodeAction, CodeActionOrCommand, DiagnosticSeverity, NumberOrString},
util::{diagnostic_to_lsp_diagnostic, lsp_pos_to_pos, lsp_range_to_range, range_to_lsp_range},
OffsetEncoding,
};
Expand All @@ -18,7 +18,7 @@ use crate::{
},
};

use std::{borrow::Cow, collections::BTreeMap, path::PathBuf, sync::Arc};
use std::{borrow::Cow, cmp::Ordering, collections::BTreeMap, path::PathBuf, sync::Arc};

/// Gets the language server that is attached to a document, and
/// if it's not active displays a status message. Using this macro
Expand Down Expand Up @@ -211,7 +211,6 @@ fn sym_picker(
Ok(path) => path,
Err(_) => {
let err = format!("unable to convert URI to filepath: {}", uri);
log::error!("{}", err);
cx.editor.set_error(err);
return;
}
Expand Down Expand Up @@ -421,6 +420,63 @@ impl ui::menu::Item for lsp::CodeActionOrCommand {
}
}

/// Determines the category of the `CodeAction` using the `CodeAction::kind` field.
/// Returns a number that represent these categories.
/// Categories with a lower number should be displayed first.
///
///
/// While the `kind` field is defined as open ended in the LSP spec (any value may be used)
/// in practice a closed set of common values (mostly suggested in the LSP spec) are used.
/// VSCode displays each of these categories seperatly (seperated by a heading in the codeactions picker)
/// to make them easier to navigate. Helix does not display these headings to the user.
/// However it does sort code actions by their categories to achieve the same order as the VScode picker,
/// just without the headings.
///
/// The order used here is modeled after the [vscode sourcecode](https://github.com/microsoft/vscode/blob/eaec601dd69aeb4abb63b9601a6f44308c8d8c6e/src/vs/editor/contrib/codeAction/browser/codeActionWidget.ts>)
fn action_category(action: &CodeActionOrCommand) -> u32 {
if let CodeActionOrCommand::CodeAction(CodeAction {
kind: Some(kind), ..
}) = action
{
let mut components = kind.as_str().split('.');
match components.next() {
Some("quickfix") => 0,
Some("refactor") => match components.next() {
Some("extract") => 1,
Some("inline") => 2,
Some("rewrite") => 3,
Some("move") => 4,
Some("surround") => 5,
_ => 7,
},
Some("source") => 6,
_ => 7,
}
} else {
7
}
}

fn action_prefered(action: &CodeActionOrCommand) -> bool {
matches!(
action,
CodeActionOrCommand::CodeAction(CodeAction {
is_preferred: Some(true),
..
})
)
}

fn action_fixes_diagnostics(action: &CodeActionOrCommand) -> bool {
matches!(
action,
CodeActionOrCommand::CodeAction(CodeAction {
diagnostics: Some(diagnostics),
..
}) if !diagnostics.is_empty()
)
}

pub fn code_action(cx: &mut Context) {
let (view, doc) = current!(cx.editor);

Expand Down Expand Up @@ -452,15 +508,60 @@ pub fn code_action(cx: &mut Context) {
cx.callback(
future,
move |editor, compositor, response: Option<lsp::CodeActionResponse>| {
let actions = match response {
let mut actions = match response {
Some(a) => a,
None => return,
};

// remove disabled code actions
actions.retain(|action| {
matches!(
action,
CodeActionOrCommand::Command(_)
| CodeActionOrCommand::CodeAction(CodeAction { disabled: None, .. })
)
});

if actions.is_empty() {
editor.set_status("No code actions available");
return;
}

// Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec.
// Many details are modeled after vscode because langauge servers are usually tested against it.
// VScode sorts the codeaction two times:
//
// First the codeactions that fix some diagnostics are moved to the front.
// If both codeactions fix some diagnostics (or both fix none) the codeaction
// that is marked with `is_preffered` is shown first. The codeactions are then shown in seperate
// submenus that only contain a certain category (see `action_category`) of actions.
//
// Below this done in in a single sorting step
actions.sort_by(|action1, action2| {
// sort actions by category
let order = action_category(action1).cmp(&action_category(action2));
if order != Ordering::Equal {
return order;
}
// within the categories sort by relevancy.
// Modeled after the `codeActionsComparator` function in vscode:
// https://github.com/microsoft/vscode/blob/eaec601dd69aeb4abb63b9601a6f44308c8d8c6e/src/vs/editor/contrib/codeAction/browser/codeAction.ts

// if one code action fixes a diagnostic but the other one doesn't show it first
let order = action_fixes_diagnostics(action1)
.cmp(&action_fixes_diagnostics(action2))
.reverse();
if order != Ordering::Equal {
return order;
}

// if one of the codeactions is marked as prefered show it first
// otherwise keep the original LSP sorting
action_prefered(action1)
.cmp(&action_prefered(action2))
.reverse()
});

let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| {
if event != PromptEvent::Validate {
return;
Expand Down
17 changes: 5 additions & 12 deletions helix-term/src/ui/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,24 +77,20 @@ impl<T: Item> Menu<T> {
editor_data: <T as Item>::Data,
callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
) -> Self {
let mut menu = Self {
let matches = (0..options.len()).map(|i| (i, 0)).collect();
Self {
options,
editor_data,
matcher: Box::new(Matcher::default()),
matches: Vec::new(),
matches,
cursor: None,
widths: Vec::new(),
callback_fn: Box::new(callback_fn),
scroll: 0,
size: (0, 0),
viewport: (0, 0),
recalculate: true,
};

// TODO: scoring on empty input should just use a fastpath
menu.score("");
archseer marked this conversation as resolved.
Show resolved Hide resolved

menu
}
}

pub fn score(&mut self, pattern: &str) {
Expand All @@ -112,10 +108,7 @@ impl<T: Item> Menu<T> {
.map(|score| (index, score))
}),
);
// matches.sort_unstable_by_key(|(_, score)| -score);
self.matches.sort_unstable_by_key(|(index, _score)| {
self.options[*index].sort_text(&self.editor_data)
});
Comment on lines -115 to -118
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this change impact other menus now?

Copy link
Member Author

@pascalkuthe pascalkuthe Oct 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline menu is only used for codeactions and autocompletions as far as I found.
For code-actions this function is never called.

For auto-completions sorting by fuzzy match is more desirable and infarct exactly #3215.
I have opted to include this change in this PR because almost all other changes from #3215 become unnecessary and sorting by fuzzy match only requires changing this single line. I can spin this change out into a seperate followup PR if you want to review it seperatly. I only included it because it was such a small change and just made sense at this point (I think it was only disabled previously because fuzzy matching sorted codeactions badly).

I have also updated the PR description and PR title a while ago to reflect that this changes the behaviour of all inline menus:

Edit: This PR was originally just about code-actions, however I found #3215 and noticed that the changes there are sort of the same as in this PR: both PRs needed to handle code-actions separately from auto-completion sorting, #3215 sorted the code-actions alphabetically while this PR sorts them by category. Enabling fuzzy sorting for auto completions on top this PR is a single-line change and I have therefore opted to include it in this PR. I can drop the commit again and spin that out into a separate PR if this is controversial. The change is trivial and it made sense to me to include it here. fixes #2508

self.matches.sort_unstable_by_key(|(_, score)| -score);

// reset cursor position
self.cursor = None;
Expand Down