macOS: Command palette has no selection by default, selection wraps

#7173

(1) The command palette no longer has any selection by default.
If and when we introduce most recently used commands, defaulting to that
would make sense. A selection only appears when the arrow keys are used
or the user starts typing.

(2) The selection with arrow keys now wraps, so if you press "down" on
the last option, it will wrap to the first option, and if you press "up"
on the first option, it will wrap to the last option. This matches both
VSCode and Zed.
pull/7175/head
Mitchell Hashimoto 2025-04-23 10:30:50 -07:00
parent f2c798d319
commit 9bfe4544bf
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
1 changed files with 40 additions and 14 deletions

View File

@ -21,8 +21,8 @@ struct CommandPaletteView: View {
var backgroundColor: Color = Color(nsColor: .windowBackgroundColor) var backgroundColor: Color = Color(nsColor: .windowBackgroundColor)
var options: [CommandOption] var options: [CommandOption]
@State private var query = "" @State private var query = ""
@State private var selectedIndex: UInt = 0 @State private var selectedIndex: UInt?
@State private var hoveredOptionID: UUID? = nil @State private var hoveredOptionID: UUID?
// The options that we should show, taking into account any filtering from // The options that we should show, taking into account any filtering from
// the query. // the query.
@ -35,7 +35,8 @@ struct CommandPaletteView: View {
} }
var selectedOption: CommandOption? { var selectedOption: CommandOption? {
if selectedIndex < filteredOptions.count { guard let selectedIndex else { return nil }
return if selectedIndex < filteredOptions.count {
filteredOptions[Int(selectedIndex)] filteredOptions[Int(selectedIndex)]
} else { } else {
filteredOptions.last filteredOptions.last
@ -54,20 +55,38 @@ struct CommandPaletteView: View {
selectedOption?.action() selectedOption?.action()
case .move(.up): case .move(.up):
if selectedIndex > 0 { if filteredOptions.isEmpty { break }
selectedIndex -= 1 let current = selectedIndex ?? UInt(filteredOptions.count)
} selectedIndex = (current == 0)
? UInt(filteredOptions.count - 1)
: current - 1
case .move(.down): case .move(.down):
if selectedIndex < filteredOptions.count - 1 { if filteredOptions.isEmpty { break }
selectedIndex += 1 let current = selectedIndex ?? UInt.max
} selectedIndex = (current >= UInt(filteredOptions.count - 1))
? 0
: current + 1
case .move(_): case .move(_):
// Unknown, ignore // Unknown, ignore
break break
} }
} }
.onChange(of: query) { newValue in
// If the user types a query then we want to make sure the first
// value is selected. If the user clears the query and we were selecting
// the first, we unset any selection.
if !newValue.isEmpty {
if selectedIndex == nil {
selectedIndex = 0
}
} else {
if let selectedIndex, selectedIndex == 0 {
self.selectedIndex = nil
}
}
}
Divider() Divider()
.padding(.bottom, 4) .padding(.bottom, 4)
@ -148,7 +167,7 @@ fileprivate struct CommandPaletteQuery: View {
fileprivate struct CommandTable: View { fileprivate struct CommandTable: View {
var options: [CommandOption] var options: [CommandOption]
@Binding var selectedIndex: UInt @Binding var selectedIndex: UInt?
@Binding var hoveredOptionID: UUID? @Binding var hoveredOptionID: UUID?
var action: (CommandOption) -> Void var action: (CommandOption) -> Void
@ -164,9 +183,15 @@ fileprivate struct CommandTable: View {
ForEach(Array(options.enumerated()), id: \.1.id) { index, option in ForEach(Array(options.enumerated()), id: \.1.id) { index, option in
CommandRow( CommandRow(
option: option, option: option,
isSelected: selectedIndex == index || isSelected: {
(selectedIndex >= options.count && if let selected = selectedIndex {
index == options.count - 1), return selected == index ||
(selected >= options.count &&
index == options.count - 1)
} else {
return false
}
}(),
hoveredID: $hoveredOptionID hoveredID: $hoveredOptionID
) { ) {
action(option) action(option)
@ -176,7 +201,8 @@ fileprivate struct CommandTable: View {
} }
.frame(maxHeight: 200) .frame(maxHeight: 200)
.onChange(of: selectedIndex) { _ in .onChange(of: selectedIndex) { _ in
guard selectedIndex < options.count else { return } guard let selectedIndex,
selectedIndex < options.count else { return }
proxy.scrollTo( proxy.scrollTo(
options[Int(selectedIndex)].id) options[Int(selectedIndex)].id)
} }