editor: Add setting for snippet sorting behavior for code completion (#29429)

Added `snippet_sort_order`, which determines how snippets are sorted
relative to other completion items. It can have the values `top`,
`bottom`, or `inline`, with `inline` being the default.

This mimics VS Code’s setting:
https://code.visualstudio.com/docs/editing/intellisense#_snippets-in-suggestions

Release Notes:

- Added support for `snippet_sort_order` to control snippet sorting
behavior in code completion menus.
This commit is contained in:
Smit Barmase 2025-04-25 22:35:12 +05:30 committed by GitHub
parent c157b1c455
commit cc57bc7c96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 126 additions and 20 deletions

View file

@ -167,7 +167,23 @@
// Default: not set, defaults to "bar" // Default: not set, defaults to "bar"
"cursor_shape": null, "cursor_shape": null,
// Determines when the mouse cursor should be hidden in an editor or input box. // Determines when the mouse cursor should be hidden in an editor or input box.
//
// 1. Never hide the mouse cursor:
// "never"
// 2. Hide only when typing:
// "on_typing"
// 3. Hide on both typing and cursor movement:
// "on_typing_and_movement"
"hide_mouse": "on_typing_and_movement", "hide_mouse": "on_typing_and_movement",
// Determines how snippets are sorted relative to other completion items.
//
// 1. Place snippets at the top of the completion list:
// "top"
// 2. Place snippets normally without any preference:
// "inline"
// 3. Place snippets at the bottom of the completion list:
// "bottom"
"snippet_sort_order": "inline",
// How to highlight the current line in the editor. // How to highlight the current line in the editor.
// //
// 1. Don't highlight the current line: // 1. Don't highlight the current line:

View file

@ -1,4 +1,7 @@
use crate::code_context_menus::{CompletionsMenu, SortableMatch}; use crate::{
code_context_menus::{CompletionsMenu, SortableMatch},
editor_settings::SnippetSortOrder,
};
use fuzzy::StringMatch; use fuzzy::StringMatch;
use gpui::TestAppContext; use gpui::TestAppContext;
@ -74,7 +77,7 @@ fn test_sort_matches_local_variable_over_global_variable(_cx: &mut TestAppContex
sort_key: (2, "floorf128"), sort_key: (2, "floorf128"),
}, },
]; ];
CompletionsMenu::sort_matches(&mut matches, query); CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
assert_eq!( assert_eq!(
matches[0].string_match.string.as_str(), matches[0].string_match.string.as_str(),
"foo_bar_qux", "foo_bar_qux",
@ -122,7 +125,7 @@ fn test_sort_matches_local_variable_over_global_variable(_cx: &mut TestAppContex
sort_key: (1, "foo_bar_qux"), sort_key: (1, "foo_bar_qux"),
}, },
]; ];
CompletionsMenu::sort_matches(&mut matches, query); CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
assert_eq!( assert_eq!(
matches[0].string_match.string.as_str(), matches[0].string_match.string.as_str(),
"foo_bar_qux", "foo_bar_qux",
@ -185,7 +188,7 @@ fn test_sort_matches_local_variable_over_global_enum(_cx: &mut TestAppContext) {
sort_key: (0, "while let"), sort_key: (0, "while let"),
}, },
]; ];
CompletionsMenu::sort_matches(&mut matches, query); CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
assert_eq!( assert_eq!(
matches[0].string_match.string.as_str(), matches[0].string_match.string.as_str(),
"element_type", "element_type",
@ -234,7 +237,7 @@ fn test_sort_matches_local_variable_over_global_enum(_cx: &mut TestAppContext) {
sort_key: (2, "REPLACEMENT_CHARACTER"), sort_key: (2, "REPLACEMENT_CHARACTER"),
}, },
]; ];
CompletionsMenu::sort_matches(&mut matches, query); CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
assert_eq!( assert_eq!(
matches[0].string_match.string.as_str(), matches[0].string_match.string.as_str(),
"element_type", "element_type",
@ -272,7 +275,7 @@ fn test_sort_matches_local_variable_over_global_enum(_cx: &mut TestAppContext) {
sort_key: (1, "element_type"), sort_key: (1, "element_type"),
}, },
]; ];
CompletionsMenu::sort_matches(&mut matches, query); CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
assert_eq!( assert_eq!(
matches[0].string_match.string.as_str(), matches[0].string_match.string.as_str(),
"ElementType", "ElementType",
@ -335,7 +338,7 @@ fn test_sort_matches_for_unreachable(_cx: &mut TestAppContext) {
sort_key: (2, "unreachable_unchecked"), sort_key: (2, "unreachable_unchecked"),
}, },
]; ];
CompletionsMenu::sort_matches(&mut matches, query); CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
assert_eq!( assert_eq!(
matches[0].string_match.string.as_str(), matches[0].string_match.string.as_str(),
"unreachable!(…)", "unreachable!(…)",
@ -379,7 +382,7 @@ fn test_sort_matches_for_unreachable(_cx: &mut TestAppContext) {
sort_key: (3, "unreachable_unchecked"), sort_key: (3, "unreachable_unchecked"),
}, },
]; ];
CompletionsMenu::sort_matches(&mut matches, query); CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
assert_eq!( assert_eq!(
matches[0].string_match.string.as_str(), matches[0].string_match.string.as_str(),
"unreachable!(…)", "unreachable!(…)",
@ -423,7 +426,7 @@ fn test_sort_matches_for_unreachable(_cx: &mut TestAppContext) {
sort_key: (2, "unreachable_unchecked"), sort_key: (2, "unreachable_unchecked"),
}, },
]; ];
CompletionsMenu::sort_matches(&mut matches, query); CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
assert_eq!( assert_eq!(
matches[0].string_match.string.as_str(), matches[0].string_match.string.as_str(),
"unreachable!(…)", "unreachable!(…)",
@ -467,7 +470,7 @@ fn test_sort_matches_for_unreachable(_cx: &mut TestAppContext) {
sort_key: (2, "unreachable_unchecked"), sort_key: (2, "unreachable_unchecked"),
}, },
]; ];
CompletionsMenu::sort_matches(&mut matches, query); CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
assert_eq!( assert_eq!(
matches[0].string_match.string.as_str(), matches[0].string_match.string.as_str(),
"unreachable!(…)", "unreachable!(…)",
@ -503,7 +506,7 @@ fn test_sort_matches_variable_and_constants_over_function(_cx: &mut TestAppConte
sort_key: (1, "var"), // variable sort_key: (1, "var"), // variable
}, },
]; ];
CompletionsMenu::sort_matches(&mut matches, query); CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
assert_eq!( assert_eq!(
matches[0].string_match.candidate_id, 1, matches[0].string_match.candidate_id, 1,
"Match order not expected" "Match order not expected"
@ -539,7 +542,7 @@ fn test_sort_matches_variable_and_constants_over_function(_cx: &mut TestAppConte
sort_key: (2, "var"), // constant sort_key: (2, "var"), // constant
}, },
]; ];
CompletionsMenu::sort_matches(&mut matches, query); CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
assert_eq!( assert_eq!(
matches[0].string_match.candidate_id, 1, matches[0].string_match.candidate_id, 1,
"Match order not expected" "Match order not expected"
@ -622,7 +625,7 @@ fn test_sort_matches_jsx_event_handler(_cx: &mut TestAppContext) {
sort_key: (3, "className?"), sort_key: (3, "className?"),
}, },
]; ];
CompletionsMenu::sort_matches(&mut matches, query); CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
assert_eq!( assert_eq!(
matches[0].string_match.string, "onCut?", matches[0].string_match.string, "onCut?",
"Match order not expected" "Match order not expected"
@ -944,7 +947,7 @@ fn test_sort_matches_jsx_event_handler(_cx: &mut TestAppContext) {
sort_key: (3, "onLoadedData?"), sort_key: (3, "onLoadedData?"),
}, },
]; ];
CompletionsMenu::sort_matches(&mut matches, query); CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
assert_eq!( assert_eq!(
matches matches
.iter() .iter()
@ -996,7 +999,7 @@ fn test_sort_matches_for_snippets(_cx: &mut TestAppContext) {
sort_key: (2, "println!(…)"), sort_key: (2, "println!(…)"),
}, },
]; ];
CompletionsMenu::sort_matches(&mut matches, query); CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);
assert_eq!( assert_eq!(
matches[0].string_match.string.as_str(), matches[0].string_match.string.as_str(),
"println!(…)", "println!(…)",

View file

@ -25,6 +25,7 @@ use task::ResolvedTask;
use ui::{Color, IntoElement, ListItem, Pixels, Popover, Styled, prelude::*}; use ui::{Color, IntoElement, ListItem, Pixels, Popover, Styled, prelude::*};
use util::ResultExt; use util::ResultExt;
use crate::editor_settings::SnippetSortOrder;
use crate::hover_popover::{hover_markdown_style, open_markdown_url}; use crate::hover_popover::{hover_markdown_style, open_markdown_url};
use crate::{ use crate::{
CodeActionProvider, CompletionId, CompletionItemKind, CompletionProvider, DisplayRow, Editor, CodeActionProvider, CompletionId, CompletionItemKind, CompletionProvider, DisplayRow, Editor,
@ -184,6 +185,7 @@ pub struct CompletionsMenu {
pub(super) ignore_completion_provider: bool, pub(super) ignore_completion_provider: bool,
last_rendered_range: Rc<RefCell<Option<Range<usize>>>>, last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
markdown_element: Option<Entity<Markdown>>, markdown_element: Option<Entity<Markdown>>,
snippet_sort_order: SnippetSortOrder,
} }
impl CompletionsMenu { impl CompletionsMenu {
@ -195,6 +197,7 @@ impl CompletionsMenu {
initial_position: Anchor, initial_position: Anchor,
buffer: Entity<Buffer>, buffer: Entity<Buffer>,
completions: Box<[Completion]>, completions: Box<[Completion]>,
snippet_sort_order: SnippetSortOrder,
) -> Self { ) -> Self {
let match_candidates = completions let match_candidates = completions
.iter() .iter()
@ -217,6 +220,7 @@ impl CompletionsMenu {
resolve_completions: true, resolve_completions: true,
last_rendered_range: RefCell::new(None).into(), last_rendered_range: RefCell::new(None).into(),
markdown_element: None, markdown_element: None,
snippet_sort_order,
} }
} }
@ -226,6 +230,7 @@ impl CompletionsMenu {
choices: &Vec<String>, choices: &Vec<String>,
selection: Range<Anchor>, selection: Range<Anchor>,
buffer: Entity<Buffer>, buffer: Entity<Buffer>,
snippet_sort_order: SnippetSortOrder,
) -> Self { ) -> Self {
let completions = choices let completions = choices
.iter() .iter()
@ -275,6 +280,7 @@ impl CompletionsMenu {
ignore_completion_provider: false, ignore_completion_provider: false,
last_rendered_range: RefCell::new(None).into(), last_rendered_range: RefCell::new(None).into(),
markdown_element: None, markdown_element: None,
snippet_sort_order,
} }
} }
@ -657,7 +663,11 @@ impl CompletionsMenu {
) )
} }
pub fn sort_matches(matches: &mut Vec<SortableMatch<'_>>, query: Option<&str>) { pub fn sort_matches(
matches: &mut Vec<SortableMatch<'_>>,
query: Option<&str>,
snippet_sort_order: SnippetSortOrder,
) {
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
enum MatchTier<'a> { enum MatchTier<'a> {
WordStartMatch { WordStartMatch {
@ -703,7 +713,11 @@ impl CompletionsMenu {
MatchTier::OtherMatch { sort_score } MatchTier::OtherMatch { sort_score }
} else { } else {
let sort_score_int = Reverse(if score >= FUZZY_THRESHOLD { 1 } else { 0 }); let sort_score_int = Reverse(if score >= FUZZY_THRESHOLD { 1 } else { 0 });
let sort_snippet = Reverse(if mat.is_snippet { 1 } else { 0 }); let sort_snippet = match snippet_sort_order {
SnippetSortOrder::Top => Reverse(if mat.is_snippet { 1 } else { 0 }),
SnippetSortOrder::Bottom => Reverse(if mat.is_snippet { 0 } else { 1 }),
SnippetSortOrder::Inline => Reverse(0),
};
MatchTier::WordStartMatch { MatchTier::WordStartMatch {
sort_score_int, sort_score_int,
sort_snippet, sort_snippet,
@ -770,7 +784,7 @@ impl CompletionsMenu {
}) })
.collect(); .collect();
Self::sort_matches(&mut sortable_items, query); Self::sort_matches(&mut sortable_items, query, self.snippet_sort_order);
matches = sortable_items matches = sortable_items
.into_iter() .into_iter()

View file

@ -4733,6 +4733,8 @@ impl Editor {
.as_ref() .as_ref()
.map_or(true, |provider| provider.filter_completions()); .map_or(true, |provider| provider.filter_completions());
let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order;
let id = post_inc(&mut self.next_completion_id); let id = post_inc(&mut self.next_completion_id);
let task = cx.spawn_in(window, async move |editor, cx| { let task = cx.spawn_in(window, async move |editor, cx| {
async move { async move {
@ -4780,6 +4782,7 @@ impl Editor {
position, position,
buffer.clone(), buffer.clone(),
completions.into(), completions.into(),
snippet_sort_order,
); );
menu.filter( menu.filter(
@ -8229,10 +8232,18 @@ impl Editor {
let buffer_id = selection.start.buffer_id.unwrap(); let buffer_id = selection.start.buffer_id.unwrap();
let buffer = self.buffer().read(cx).buffer(buffer_id); let buffer = self.buffer().read(cx).buffer(buffer_id);
let id = post_inc(&mut self.next_completion_id); let id = post_inc(&mut self.next_completion_id);
let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order;
if let Some(buffer) = buffer { if let Some(buffer) = buffer {
*self.context_menu.borrow_mut() = Some(CodeContextMenu::Completions( *self.context_menu.borrow_mut() = Some(CodeContextMenu::Completions(
CompletionsMenu::new_snippet_choices(id, true, choices, selection, buffer), CompletionsMenu::new_snippet_choices(
id,
true,
choices,
selection,
buffer,
snippet_sort_order,
),
)); ));
} }
} }

View file

@ -39,6 +39,7 @@ pub struct EditorSettings {
pub go_to_definition_fallback: GoToDefinitionFallback, pub go_to_definition_fallback: GoToDefinitionFallback,
pub jupyter: Jupyter, pub jupyter: Jupyter,
pub hide_mouse: Option<HideMouseMode>, pub hide_mouse: Option<HideMouseMode>,
pub snippet_sort_order: SnippetSortOrder,
} }
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@ -239,6 +240,21 @@ pub enum HideMouseMode {
OnTypingAndMovement, OnTypingAndMovement,
} }
/// Determines how snippets are sorted relative to other completion items.
///
/// Default: inline
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SnippetSortOrder {
/// Place snippets at the top of the completion list
Top,
/// Sort snippets normally using the default comparison logic
#[default]
Inline,
/// Place snippets at the bottom of the completion list
Bottom,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct EditorSettingsContent { pub struct EditorSettingsContent {
/// Whether the cursor blinks in the editor. /// Whether the cursor blinks in the editor.
@ -254,6 +270,10 @@ pub struct EditorSettingsContent {
/// ///
/// Default: on_typing_and_movement /// Default: on_typing_and_movement
pub hide_mouse: Option<HideMouseMode>, pub hide_mouse: Option<HideMouseMode>,
/// Determines how snippets are sorted relative to other completion items.
///
/// Default: inline
pub snippet_sort_order: Option<SnippetSortOrder>,
/// How to highlight the current line in the editor. /// How to highlight the current line in the editor.
/// ///
/// Default: all /// Default: all

View file

@ -592,7 +592,49 @@ List of `string` values
**Options** **Options**
`boolean` values 1. Never hide the mouse cursor:
```json
"hide_mouse": "never"
```
2. Hide only when typing:
```json
"hide_mouse": "on_typing"
```
3. Hide on both typing and cursor movement:
```json
"hide_mouse": "on_typing_and_movement"
```
## Snippet Sort Order
- Description: Determines how snippets are sorted relative to other completion items.
- Setting: `snippet_sort_order`
- Default: `inline`
**Options**
1. Place snippets at the top of the completion list:
```json
"snippet_sort_order": "top"
```
2. Place snippets normally without any preference:
```json
"snippet_sort_order": "inline"
```
3. Place snippets at the bottom of the completion list:
```json
"snippet_sort_order": "bottom"
```
## Editor Scrollbar ## Editor Scrollbar