on master help sign in
Sort by fixed diagnostics/is_preffered within codeaction categories
commit9af7c1c9f3aa498719bd0fa39d4d35746f23adaa authorPascal Kuthe <pascal.kuthe@semimod.de> dateFri 14 Oct 2022, 11:50 (4 years ago) parent189aa0b
1 file changed+100−28
helix-term/src/commands/lsp.rs+100−28
helix-term/src/commands/lsp.rs+100−28
@@ -1,6 +1,6 @@
11use helix_lsp::{
22 block_on,
3− lsp::{self, DiagnosticSeverity, NumberOrString},
3+ lsp::{self, CodeAction, CodeActionOrCommand, DiagnosticSeverity, NumberOrString},
44 util::{diagnostic_to_lsp_diagnostic, lsp_pos_to_pos, lsp_range_to_range, range_to_lsp_range},
55 OffsetEncoding,
66};
@@ -18,7 +18,7 @@ use crate::{
1818 },
1919};
2020
21−use std::{borrow::Cow, collections::BTreeMap, path::PathBuf, sync::Arc};
21+use std::{borrow::Cow, cmp::Ordering, collections::BTreeMap, path::PathBuf, sync::Arc};
2222
2323/// Gets the language server that is attached to a document, and
2424/// if it's not active displays a status message. Using this macro
@@ -211,7 +211,6 @@ fn sym_picker(
211211 Ok(path) => path,
212212 Err(_) => {
213213 let err = format!("unable to convert URI to filepath: {}", uri);
214− log::error!("{}", err);
215214 cx.editor.set_error(err);
216215 return;
217216 }
@@ -421,6 +420,63 @@ impl ui::menu::Item for lsp::CodeActionOrCommand {
421420 }
422421}
423422
423+/// Determines the category of the `CodeAction` using the `CodeAction::kind` field.
424+/// Returns a number that represent these categories.
425+/// Categories with a lower number should be displayed first.
426+///
427+///
428+/// While the `kind` field is defined as open ended in the LSP spec (any value may be used)
429+/// in practice a closed set of common values (mostly suggested in the LSP spec) are used.
430+/// VSCode displays each of these categories seperatly (seperated by a heading in the codeactions picker)
431+/// to make them easier to navigate. Helix does not display these headings to the user.
432+/// However it does sort code actions by their categories to achieve the same order as the VScode picker,
433+/// just without the headings.
434+///
435+/// 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>)
436+fn action_category(action: &CodeActionOrCommand) -> u32 {
437+ if let CodeActionOrCommand::CodeAction(CodeAction {
438+ kind: Some(kind), ..
439+ }) = action
440+ {
441+ let mut components = kind.as_str().split('.');
442+ match components.next() {
443+ Some("quickfix") => 0,
444+ Some("refactor") => match components.next() {
445+ Some("extract") => 1,
446+ Some("inline") => 2,
447+ Some("rewrite") => 3,
448+ Some("move") => 4,
449+ Some("surround") => 5,
450+ _ => 7,
451+ },
452+ Some("source") => 6,
453+ _ => 7,
454+ }
455+ } else {
456+ 7
457+ }
458+}
459+
460+fn action_prefered(action: &CodeActionOrCommand) -> bool {
461+ matches!(
462+ action,
463+ CodeActionOrCommand::CodeAction(CodeAction {
464+ is_preferred: Some(true),
465+ ..
466+ })
467+ )
468+}
469+
470+fn action_fixes_diagnostics(action: &CodeActionOrCommand) -> bool {
471+ matches!(
472+ action,
473+ CodeActionOrCommand::CodeAction(CodeAction {
474+ diagnostics: Some(diagnostics),
475+ ..
476+ }) if !diagnostics.is_empty()
477+ )
478+}
479+
424480pub fn code_action(cx: &mut Context) {
425481 let (view, doc) = current!(cx.editor);
426482
@@ -457,37 +513,53 @@ pub fn code_action(cx: &mut Context) {
457513 None => return,
458514 };
459515
516+ // remove disabled code actions
517+ actions.retain(|action| {
518+ matches!(
519+ action,
520+ CodeActionOrCommand::Command(_)
521+ | CodeActionOrCommand::CodeAction(CodeAction { disabled: None, .. })
522+ )
523+ });
524+
460525 if actions.is_empty() {
461526 editor.set_status("No code actions available");
462527 return;
463528 }
464529
465− // sort by CodeActionKind
466− // this ensures that the most relevant codeactions (quickfix) show up first
467− // while more situational commands (like refactors) show up later
468− // this behaviour is modeled after the behaviour of vscode (https://github.com/microsoft/vscode/blob/eaec601dd69aeb4abb63b9601a6f44308c8d8c6e/src/vs/editor/contrib/codeAction/browser/codeActionWidget.ts)
469−
470− actions.sort_by_key(|action| match &action {
471− lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
472− kind: Some(kind), ..
473− }) => {
474− let mut components = kind.as_str().split('.');
475−
476− match components.next() {
477− Some("quickfix") => 0,
478− Some("refactor") => match components.next() {
479− Some("extract") => 1,
480− Some("inline") => 2,
481− Some("rewrite") => 3,
482− Some("move") => 4,
483− Some("surround") => 5,
484− _ => 7,
485− },
486− Some("source") => 6,
487− _ => 7,
488− }
530+ // Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec.
531+ // Many details are modeled after vscode because langauge servers are usually tested against it.
532+ // VScode sorts the codeaction two times:
533+ //
534+ // First the codeactions that fix some diagnostics are moved to the front.
535+ // If both codeactions fix some diagnostics (or both fix none) the codeaction
536+ // that is marked with `is_preffered` is shown first. The codeactions are then shown in seperate
537+ // submenus that only contain a certain category (see `action_category`) of actions.
538+ //
539+ // Below this done in in a single sorting step
540+ actions.sort_by(|action1, action2| {
541+ // sort actions by category
542+ let order = action_category(action1).cmp(&action_category(action2));
543+ if order != Ordering::Equal {
544+ return order;
489545 }
490− _ => 7,
546+ // within the categories sort by relevancy.
547+ // Modeled after the `codeActionsComparator` function in vscode:
548+ // https://github.com/microsoft/vscode/blob/eaec601dd69aeb4abb63b9601a6f44308c8d8c6e/src/vs/editor/contrib/codeAction/browser/codeAction.ts
549+
550+ // if one code action fixes a diagnostic but the other one doesn't show it first
551+ let order = action_fixes_diagnostics(action1)
552+ .cmp(&action_fixes_diagnostics(action2))
553+ .reverse();
554+ if order != Ordering::Equal {
555+ return order;
556+ }
557+
558+ // if one of the codeactions is marked as prefered show it first
559+ // otherwise keep the original LSP sorting
560+ action_prefered(action1)
561+ .cmp(&action_prefered(action2))
562+ .reverse()
491563 });
492564
493565 let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| {