From cc57bc7c96a1acad04059d7259c1e99e1a14250b Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Fri, 25 Apr 2025 22:35:12 +0530 Subject: [PATCH] editor: Add setting for snippet sorting behavior for code completion (#29429) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- assets/settings/default.json | 16 ++++++++ crates/editor/src/code_completion_tests.rs | 33 ++++++++-------- crates/editor/src/code_context_menus.rs | 20 ++++++++-- crates/editor/src/editor.rs | 13 ++++++- crates/editor/src/editor_settings.rs | 20 ++++++++++ docs/src/configuring-zed.md | 44 +++++++++++++++++++++- 6 files changed, 126 insertions(+), 20 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 44492b0653..492a482198 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -167,7 +167,23 @@ // Default: not set, defaults to "bar" "cursor_shape": null, // 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", + // 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. // // 1. Don't highlight the current line: diff --git a/crates/editor/src/code_completion_tests.rs b/crates/editor/src/code_completion_tests.rs index 632b16d4cc..866fc4575f 100644 --- a/crates/editor/src/code_completion_tests.rs +++ b/crates/editor/src/code_completion_tests.rs @@ -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 gpui::TestAppContext; @@ -74,7 +77,7 @@ fn test_sort_matches_local_variable_over_global_variable(_cx: &mut TestAppContex sort_key: (2, "floorf128"), }, ]; - CompletionsMenu::sort_matches(&mut matches, query); + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); assert_eq!( matches[0].string_match.string.as_str(), "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"), }, ]; - CompletionsMenu::sort_matches(&mut matches, query); + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); assert_eq!( matches[0].string_match.string.as_str(), "foo_bar_qux", @@ -185,7 +188,7 @@ fn test_sort_matches_local_variable_over_global_enum(_cx: &mut TestAppContext) { sort_key: (0, "while let"), }, ]; - CompletionsMenu::sort_matches(&mut matches, query); + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); assert_eq!( matches[0].string_match.string.as_str(), "element_type", @@ -234,7 +237,7 @@ fn test_sort_matches_local_variable_over_global_enum(_cx: &mut TestAppContext) { sort_key: (2, "REPLACEMENT_CHARACTER"), }, ]; - CompletionsMenu::sort_matches(&mut matches, query); + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); assert_eq!( matches[0].string_match.string.as_str(), "element_type", @@ -272,7 +275,7 @@ fn test_sort_matches_local_variable_over_global_enum(_cx: &mut TestAppContext) { sort_key: (1, "element_type"), }, ]; - CompletionsMenu::sort_matches(&mut matches, query); + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); assert_eq!( matches[0].string_match.string.as_str(), "ElementType", @@ -335,7 +338,7 @@ fn test_sort_matches_for_unreachable(_cx: &mut TestAppContext) { sort_key: (2, "unreachable_unchecked"), }, ]; - CompletionsMenu::sort_matches(&mut matches, query); + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); assert_eq!( matches[0].string_match.string.as_str(), "unreachable!(…)", @@ -379,7 +382,7 @@ fn test_sort_matches_for_unreachable(_cx: &mut TestAppContext) { sort_key: (3, "unreachable_unchecked"), }, ]; - CompletionsMenu::sort_matches(&mut matches, query); + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); assert_eq!( matches[0].string_match.string.as_str(), "unreachable!(…)", @@ -423,7 +426,7 @@ fn test_sort_matches_for_unreachable(_cx: &mut TestAppContext) { sort_key: (2, "unreachable_unchecked"), }, ]; - CompletionsMenu::sort_matches(&mut matches, query); + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); assert_eq!( matches[0].string_match.string.as_str(), "unreachable!(…)", @@ -467,7 +470,7 @@ fn test_sort_matches_for_unreachable(_cx: &mut TestAppContext) { sort_key: (2, "unreachable_unchecked"), }, ]; - CompletionsMenu::sort_matches(&mut matches, query); + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); assert_eq!( matches[0].string_match.string.as_str(), "unreachable!(…)", @@ -503,7 +506,7 @@ fn test_sort_matches_variable_and_constants_over_function(_cx: &mut TestAppConte sort_key: (1, "var"), // variable }, ]; - CompletionsMenu::sort_matches(&mut matches, query); + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); assert_eq!( matches[0].string_match.candidate_id, 1, "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 }, ]; - CompletionsMenu::sort_matches(&mut matches, query); + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); assert_eq!( matches[0].string_match.candidate_id, 1, "Match order not expected" @@ -622,7 +625,7 @@ fn test_sort_matches_jsx_event_handler(_cx: &mut TestAppContext) { sort_key: (3, "className?"), }, ]; - CompletionsMenu::sort_matches(&mut matches, query); + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); assert_eq!( matches[0].string_match.string, "onCut?", "Match order not expected" @@ -944,7 +947,7 @@ fn test_sort_matches_jsx_event_handler(_cx: &mut TestAppContext) { sort_key: (3, "onLoadedData?"), }, ]; - CompletionsMenu::sort_matches(&mut matches, query); + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); assert_eq!( matches .iter() @@ -996,7 +999,7 @@ fn test_sort_matches_for_snippets(_cx: &mut TestAppContext) { sort_key: (2, "println!(…)"), }, ]; - CompletionsMenu::sort_matches(&mut matches, query); + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); assert_eq!( matches[0].string_match.string.as_str(), "println!(…)", diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 0951497ac7..28ae2eb771 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -25,6 +25,7 @@ use task::ResolvedTask; use ui::{Color, IntoElement, ListItem, Pixels, Popover, Styled, prelude::*}; use util::ResultExt; +use crate::editor_settings::SnippetSortOrder; use crate::hover_popover::{hover_markdown_style, open_markdown_url}; use crate::{ CodeActionProvider, CompletionId, CompletionItemKind, CompletionProvider, DisplayRow, Editor, @@ -184,6 +185,7 @@ pub struct CompletionsMenu { pub(super) ignore_completion_provider: bool, last_rendered_range: Rc>>>, markdown_element: Option>, + snippet_sort_order: SnippetSortOrder, } impl CompletionsMenu { @@ -195,6 +197,7 @@ impl CompletionsMenu { initial_position: Anchor, buffer: Entity, completions: Box<[Completion]>, + snippet_sort_order: SnippetSortOrder, ) -> Self { let match_candidates = completions .iter() @@ -217,6 +220,7 @@ impl CompletionsMenu { resolve_completions: true, last_rendered_range: RefCell::new(None).into(), markdown_element: None, + snippet_sort_order, } } @@ -226,6 +230,7 @@ impl CompletionsMenu { choices: &Vec, selection: Range, buffer: Entity, + snippet_sort_order: SnippetSortOrder, ) -> Self { let completions = choices .iter() @@ -275,6 +280,7 @@ impl CompletionsMenu { ignore_completion_provider: false, last_rendered_range: RefCell::new(None).into(), markdown_element: None, + snippet_sort_order, } } @@ -657,7 +663,11 @@ impl CompletionsMenu { ) } - pub fn sort_matches(matches: &mut Vec>, query: Option<&str>) { + pub fn sort_matches( + matches: &mut Vec>, + query: Option<&str>, + snippet_sort_order: SnippetSortOrder, + ) { #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] enum MatchTier<'a> { WordStartMatch { @@ -703,7 +713,11 @@ impl CompletionsMenu { MatchTier::OtherMatch { sort_score } } else { 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 { sort_score_int, sort_snippet, @@ -770,7 +784,7 @@ impl CompletionsMenu { }) .collect(); - Self::sort_matches(&mut sortable_items, query); + Self::sort_matches(&mut sortable_items, query, self.snippet_sort_order); matches = sortable_items .into_iter() diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8069b20109..74dfe1e3fa 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4733,6 +4733,8 @@ impl Editor { .as_ref() .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 task = cx.spawn_in(window, async move |editor, cx| { async move { @@ -4780,6 +4782,7 @@ impl Editor { position, buffer.clone(), completions.into(), + snippet_sort_order, ); menu.filter( @@ -8229,10 +8232,18 @@ impl Editor { let buffer_id = selection.start.buffer_id.unwrap(); let buffer = self.buffer().read(cx).buffer(buffer_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 { *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, + ), )); } } diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 04e477a5ea..1706951dc4 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -39,6 +39,7 @@ pub struct EditorSettings { pub go_to_definition_fallback: GoToDefinitionFallback, pub jupyter: Jupyter, pub hide_mouse: Option, + pub snippet_sort_order: SnippetSortOrder, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -239,6 +240,21 @@ pub enum HideMouseMode { 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)] pub struct EditorSettingsContent { /// Whether the cursor blinks in the editor. @@ -254,6 +270,10 @@ pub struct EditorSettingsContent { /// /// Default: on_typing_and_movement pub hide_mouse: Option, + /// Determines how snippets are sorted relative to other completion items. + /// + /// Default: inline + pub snippet_sort_order: Option, /// How to highlight the current line in the editor. /// /// Default: all diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 4c05059ed9..96dbdc26d4 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -592,7 +592,49 @@ List of `string` values **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