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] 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 + } +}