From 2bdc6bb1f7f2a6093d1e50484fba4aac9e7334a7 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:44:04 +0200 Subject: [PATCH 1/3] macOS: Highlight matching text in command palette search results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add String.matchedIndices(for:) to find substring matches and use it to bold and tint matched characters with the accent color in both titles and subtitles. Title matches take priority — subtitles are only highlighted when the title didn't match. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Command Palette/CommandPalette.swift | 69 +++++++++++++++++-- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index c4ba7106c..d5a7fc11e 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -73,8 +73,8 @@ struct CommandPaletteView: View { } else { // Filter by title/subtitle match OR color match let filtered = options.filter { - $0.title.localizedCaseInsensitiveContains(query) || - ($0.subtitle?.localizedCaseInsensitiveContains(query) ?? false) || + $0.title.matchedIndices(for: query) != nil || + ($0.subtitle?.matchedIndices(for: query) != nil) || colorMatchScore(for: $0.leadingColor, query: query) > 0 } @@ -151,6 +151,7 @@ struct CommandPaletteView: View { CommandTable( options: filteredOptions, + query: query, selectedIndex: $selectedIndex, hoveredOptionID: $hoveredOptionID) { option in isPresented = false @@ -281,6 +282,7 @@ private struct CommandPaletteQuery: View { private struct CommandTable: View { var options: [CommandOption] + var query: String @Binding var selectedIndex: UInt? @Binding var hoveredOptionID: UUID? var action: (CommandOption) -> Void @@ -297,6 +299,7 @@ private struct CommandTable: View { ForEach(Array(options.enumerated()), id: \.1.id) { index, option in CommandRow( option: option, + query: query, isSelected: { if let selected = selectedIndex { return selected == index || @@ -329,10 +332,53 @@ private struct CommandTable: View { /// A single row in the command palette. private struct CommandRow: View { let option: CommandOption + var query: String var isSelected: Bool @Binding var hoveredID: UUID? var action: () -> Void + private var highlightedTitle: Text { + guard !query.isEmpty, + let indices = option.title.matchedIndices(for: query) else { + return Text(option.title) + .fontWeight(option.emphasis ? .medium : .regular) + } + + var attributed = AttributedString(option.title) + attributed[attributed.startIndex...].font = .body + .weight(option.emphasis ? .medium : .regular) + + for idx in indices { + let offset = option.title.distance(from: option.title.startIndex, to: idx) + let attrStart = attributed.index(attributed.startIndex, offsetByCharacters: offset) + let attrEnd = attributed.index(attrStart, offsetByCharacters: 1) + attributed[attrStart.. Text { + guard !query.isEmpty, + option.title.matchedIndices(for: query) == nil, + let indices = subtitle.matchedIndices(for: query) else { + return Text(subtitle) + } + + var attributed = AttributedString(subtitle) + + for idx in indices { + let offset = subtitle.distance(from: subtitle.startIndex, to: idx) + let attrStart = attributed.index(attributed.startIndex, offsetByCharacters: offset) + let attrEnd = attributed.index(attrStart, offsetByCharacters: 1) + attributed[attrStart.. [String.Index]? { + guard !query.isEmpty else { return nil } + + if let range = self.range(of: query, options: .caseInsensitive) { + return Array(self[range].indices) + } + + return nil + } +} From 2e169c42e8334bdd343b17cd3489281d32cd5044 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:44:18 +0200 Subject: [PATCH 2/3] macOS: Support initials matching in command palette search Extend String.matchedIndices(for:) to fall back to initials matching when no substring match is found. Typing the first letter of each word now matches commands, e.g. "tbo" matches "Toggle Background Opacity", with each matched initial highlighted. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Command Palette/CommandPalette.swift | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index d5a7fc11e..7321745a7 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -460,15 +460,31 @@ private struct ShortcutSymbolsView: View { } extension String { - /// Returns the character indices that match `query` + /// Returns the character indices that match `query`, trying a substring match first, + /// then falling back to initials matching (first letter of each word). /// - Returns: `nil` if neither matches. func matchedIndices(for query: String) -> [String.Index]? { guard !query.isEmpty else { return nil } + // Prefer substring match. if let range = self.range(of: query, options: .caseInsensitive) { return Array(self[range].indices) } - return nil + // Fall back to initials match. + let words = self.split(whereSeparator: \.isWhitespace) + var queryIndex = query.startIndex + var matched: [String.Index] = [] + + for word in words { + guard queryIndex < query.endIndex else { break } + + if word.first?.lowercased() == query[queryIndex].lowercased() { + matched.append(word.startIndex) + queryIndex = query.index(after: queryIndex) + } + } + + return queryIndex == query.endIndex ? matched : nil } } From 073dd8a39974d19e482a093c2f070d18deca3cc7 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Mon, 13 Apr 2026 12:39:32 +0200 Subject: [PATCH 3/3] macOS: trim query before filtering commands --- .../Features/Command Palette/CommandPalette.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 7321745a7..de440fdc3 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -61,10 +61,14 @@ struct CommandPaletteView: View { @Binding var isPresented: Bool var backgroundColor: Color = Color(nsColor: .windowBackgroundColor) var options: [CommandOption] - @State private var query = "" + @State private var rawQuery = "" @State private var selectedIndex: UInt? @State private var hoveredOptionID: UUID? + var query: String { + rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) + } + // The options that we should show, taking into account any filtering from // the query. Options with matching leadingColor are ranked higher. var filteredOptions: [CommandOption] { @@ -104,7 +108,7 @@ struct CommandPaletteView: View { } VStack(alignment: .leading, spacing: 0) { - CommandPaletteQuery(query: $query) { event in + CommandPaletteQuery(query: $rawQuery) { event in switch event { case .exit: isPresented = false @@ -182,7 +186,7 @@ struct CommandPaletteView: View { // This is optional, since most of the time // there will be a delay before the next use. // To keep behavior the same as before, we reset it. - query = "" + rawQuery = "" } } }