@@ -6,7 +6,54 @@ import { Command } from 'cmdk'
66import { File , Workflow } from '@/components/emcn/icons'
77import { cn } from '@/lib/core/utils/cn'
88import type { CommandItemProps } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils'
9- import { COMMAND_ITEM_CLASSNAME } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils'
9+ import {
10+ COMMAND_ITEM_CLASSNAME ,
11+ fuzzyMatch ,
12+ } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils'
13+
14+ interface Segment {
15+ text : string
16+ hit : boolean
17+ }
18+
19+ function buildSegments ( text : string , positions : readonly number [ ] ) : Segment [ ] {
20+ const hits = new Set ( positions )
21+ const segments : Segment [ ] = [ ]
22+ for ( let i = 0 ; i < text . length ; i ++ ) {
23+ const hit = hits . has ( i )
24+ const last = segments [ segments . length - 1 ]
25+ if ( last && last . hit === hit ) last . text += text [ i ]
26+ else segments . push ( { text : text [ i ] , hit } )
27+ }
28+ return segments
29+ }
30+
31+ /**
32+ * Renders `text` with the characters that match `query` emphasized. Falls back
33+ * to plain text when there is no query or no positional match against the
34+ * display text (e.g. the row matched on a hidden id rather than its label).
35+ */
36+ export const HighlightedText = memo (
37+ function HighlightedText ( { text, query } : { text : string ; query ?: string } ) {
38+ if ( ! query ) return < > { text } </ >
39+ const { positions } = fuzzyMatch ( text , query )
40+ if ( positions . length === 0 ) return < > { text } </ >
41+ return (
42+ < >
43+ { buildSegments ( text , positions ) . map ( ( segment , index ) =>
44+ segment . hit ? (
45+ < span key = { index } className = 'font-semibold text-[var(--text-body)]' >
46+ { segment . text }
47+ </ span >
48+ ) : (
49+ < span key = { index } > { segment . text } </ span >
50+ )
51+ ) }
52+ </ >
53+ )
54+ } ,
55+ ( prev , next ) => prev . text === next . text && prev . query === next . query
56+ )
1057
1158export const MemoizedCommandItem = memo (
1259 function CommandItem ( {
@@ -15,7 +62,8 @@ export const MemoizedCommandItem = memo(
1562 icon : Icon ,
1663 bgColor,
1764 showColoredIcon,
18- children,
65+ label,
66+ query,
1967 } : CommandItemProps ) {
2068 return (
2169 < Command . Item value = { value } onSelect = { onSelect } className = { COMMAND_ITEM_CLASSNAME } >
@@ -32,7 +80,9 @@ export const MemoizedCommandItem = memo(
3280 ) }
3381 />
3482 </ div >
35- < span className = 'truncate text-[var(--text-body)]' > { children } </ span >
83+ < span className = 'truncate text-[var(--text-body)]' >
84+ < HighlightedText text = { label } query = { query } />
85+ </ span >
3686 </ Command . Item >
3787 )
3888 } ,
@@ -41,7 +91,46 @@ export const MemoizedCommandItem = memo(
4191 prev . icon === next . icon &&
4292 prev . bgColor === next . bgColor &&
4393 prev . showColoredIcon === next . showColoredIcon &&
44- prev . children === next . children
94+ prev . label === next . label &&
95+ prev . query === next . query
96+ )
97+
98+ export const MemoizedActionItem = memo (
99+ function ActionItem ( {
100+ value,
101+ onSelect,
102+ icon : Icon ,
103+ name,
104+ shortcut,
105+ query,
106+ } : {
107+ value : string
108+ onSelect : ( ) => void
109+ icon : ComponentType < { className ?: string } >
110+ name : string
111+ shortcut ?: string
112+ query ?: string
113+ } ) {
114+ return (
115+ < Command . Item value = { value } onSelect = { onSelect } className = { COMMAND_ITEM_CLASSNAME } >
116+ < Icon className = 'size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
117+ < span className = 'truncate text-[var(--text-body)]' >
118+ < HighlightedText text = { name } query = { query } />
119+ </ span >
120+ { shortcut && (
121+ < span className = 'ml-auto flex-shrink-0 text-[var(--text-subtle)] text-small' >
122+ { shortcut }
123+ </ span >
124+ ) }
125+ </ Command . Item >
126+ )
127+ } ,
128+ ( prev , next ) =>
129+ prev . value === next . value &&
130+ prev . icon === next . icon &&
131+ prev . name === next . name &&
132+ prev . shortcut === next . shortcut &&
133+ prev . query === next . query
45134)
46135
47136export const MemoizedWorkflowItem = memo (
@@ -51,20 +140,24 @@ export const MemoizedWorkflowItem = memo(
51140 name,
52141 folderPath,
53142 isCurrent,
143+ query,
54144 } : {
55145 value : string
56146 onSelect : ( ) => void
57147 name : string
58148 folderPath ?: string [ ]
59149 isCurrent ?: boolean
150+ query ?: string
60151 } ) {
61152 return (
62153 < Command . Item value = { value } onSelect = { onSelect } className = { COMMAND_ITEM_CLASSNAME } >
63154 < div className = 'relative flex size-[16px] flex-shrink-0 items-center justify-center' >
64155 < Workflow className = 'size-[14px] text-[var(--text-icon)]' />
65156 </ div >
66157 < span className = 'flex min-w-0 max-w-[75%] flex-shrink-0 text-[var(--text-body)]' >
67- < span className = 'truncate' > { name } </ span >
158+ < span className = 'truncate' >
159+ < HighlightedText text = { name } query = { query } />
160+ </ span >
68161 { isCurrent && < span className = 'flex-shrink-0 whitespace-pre' > (current)</ span > }
69162 </ span >
70163 { folderPath && folderPath . length > 0 && (
@@ -87,6 +180,7 @@ export const MemoizedWorkflowItem = memo(
87180 prev . value === next . value &&
88181 prev . name === next . name &&
89182 prev . isCurrent === next . isCurrent &&
183+ prev . query === next . query &&
90184 ( prev . folderPath === next . folderPath ||
91185 ( prev . folderPath ?. length === next . folderPath ?. length &&
92186 ( prev . folderPath ?? [ ] ) . every ( ( segment , i ) => segment === next . folderPath ?. [ i ] ) ) )
@@ -98,19 +192,23 @@ export const MemoizedFileItem = memo(
98192 onSelect,
99193 name,
100194 folderPath,
195+ query,
101196 } : {
102197 value : string
103198 onSelect : ( ) => void
104199 name : string
105200 folderPath ?: string [ ]
201+ query ?: string
106202 } ) {
107203 return (
108204 < Command . Item value = { value } onSelect = { onSelect } className = { COMMAND_ITEM_CLASSNAME } >
109205 < div className = 'relative flex size-[16px] flex-shrink-0 items-center justify-center' >
110206 < File className = 'size-[14px] text-[var(--text-icon)]' />
111207 </ div >
112208 < span className = 'flex min-w-0 max-w-[75%] flex-shrink-0 font-base text-[var(--text-body)]' >
113- < span className = 'truncate' > { name } </ span >
209+ < span className = 'truncate' >
210+ < HighlightedText text = { name } query = { query } />
211+ </ span >
114212 </ span >
115213 { folderPath && folderPath . length > 0 && (
116214 < span className = 'ml-auto flex min-w-0 pl-2 font-base text-[var(--text-subtle)] text-small' >
@@ -131,6 +229,7 @@ export const MemoizedFileItem = memo(
131229 ( prev , next ) =>
132230 prev . value === next . value &&
133231 prev . name === next . name &&
232+ prev . query === next . query &&
134233 ( prev . folderPath === next . folderPath ||
135234 ( prev . folderPath ?. length === next . folderPath ?. length &&
136235 ( prev . folderPath ?? [ ] ) . every ( ( segment , i ) => segment === next . folderPath ?. [ i ] ) ) )
@@ -141,18 +240,22 @@ export const MemoizedTaskItem = memo(
141240 value,
142241 onSelect,
143242 name,
243+ query,
144244 } : {
145245 value : string
146246 onSelect : ( ) => void
147247 name : string
248+ query ?: string
148249 } ) {
149250 return (
150251 < Command . Item value = { value } onSelect = { onSelect } className = { COMMAND_ITEM_CLASSNAME } >
151- < span className = 'truncate text-[var(--text-body)]' > { name } </ span >
252+ < span className = 'truncate text-[var(--text-body)]' >
253+ < HighlightedText text = { name } query = { query } />
254+ </ span >
152255 </ Command . Item >
153256 )
154257 } ,
155- ( prev , next ) => prev . value === next . value && prev . name === next . name
258+ ( prev , next ) => prev . value === next . value && prev . name === next . name && prev . query === next . query
156259)
157260
158261export const MemoizedWorkspaceItem = memo (
@@ -161,23 +264,30 @@ export const MemoizedWorkspaceItem = memo(
161264 onSelect,
162265 name,
163266 isCurrent,
267+ query,
164268 } : {
165269 value : string
166270 onSelect : ( ) => void
167271 name : string
168272 isCurrent ?: boolean
273+ query ?: string
169274 } ) {
170275 return (
171276 < Command . Item value = { value } onSelect = { onSelect } className = { COMMAND_ITEM_CLASSNAME } >
172277 < span className = 'flex min-w-0 text-[var(--text-body)]' >
173- < span className = 'truncate' > { name } </ span >
278+ < span className = 'truncate' >
279+ < HighlightedText text = { name } query = { query } />
280+ </ span >
174281 { isCurrent && < span className = 'flex-shrink-0 whitespace-pre' > (current)</ span > }
175282 </ span >
176283 </ Command . Item >
177284 )
178285 } ,
179286 ( prev , next ) =>
180- prev . value === next . value && prev . name === next . name && prev . isCurrent === next . isCurrent
287+ prev . value === next . value &&
288+ prev . name === next . name &&
289+ prev . isCurrent === next . isCurrent &&
290+ prev . query === next . query
181291)
182292
183293export const MemoizedPageItem = memo (
@@ -187,17 +297,21 @@ export const MemoizedPageItem = memo(
187297 icon : Icon ,
188298 name,
189299 shortcut,
300+ query,
190301 } : {
191302 value : string
192303 onSelect : ( ) => void
193304 icon : ComponentType < { className ?: string } >
194305 name : string
195306 shortcut ?: string
307+ query ?: string
196308 } ) {
197309 return (
198310 < Command . Item value = { value } onSelect = { onSelect } className = { COMMAND_ITEM_CLASSNAME } >
199311 < Icon className = 'size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
200- < span className = 'truncate text-[var(--text-body)]' > { name } </ span >
312+ < span className = 'truncate text-[var(--text-body)]' >
313+ < HighlightedText text = { name } query = { query } />
314+ </ span >
201315 { shortcut && (
202316 < span className = 'ml-auto flex-shrink-0 text-[var(--text-subtle)] text-small' >
203317 { shortcut }
@@ -210,7 +324,8 @@ export const MemoizedPageItem = memo(
210324 prev . value === next . value &&
211325 prev . icon === next . icon &&
212326 prev . name === next . name &&
213- prev . shortcut === next . shortcut
327+ prev . shortcut === next . shortcut &&
328+ prev . query === next . query
214329)
215330
216331export const MemoizedIconItem = memo (
@@ -219,18 +334,26 @@ export const MemoizedIconItem = memo(
219334 onSelect,
220335 name,
221336 icon : Icon ,
337+ query,
222338 } : {
223339 value : string
224340 onSelect : ( ) => void
225341 name : string
226342 icon : ComponentType < { className ?: string } >
343+ query ?: string
227344 } ) {
228345 return (
229346 < Command . Item value = { value } onSelect = { onSelect } className = { COMMAND_ITEM_CLASSNAME } >
230347 < Icon className = 'size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
231- < span className = 'truncate text-[var(--text-body)]' > { name } </ span >
348+ < span className = 'truncate text-[var(--text-body)]' >
349+ < HighlightedText text = { name } query = { query } />
350+ </ span >
232351 </ Command . Item >
233352 )
234353 } ,
235- ( prev , next ) => prev . value === next . value && prev . name === next . name && prev . icon === next . icon
354+ ( prev , next ) =>
355+ prev . value === next . value &&
356+ prev . name === next . name &&
357+ prev . icon === next . icon &&
358+ prev . query === next . query
236359)
0 commit comments