@@ -5,6 +5,7 @@ final class ViewState<T: Selectable> {
5
5
var current : Int ?
6
6
7
7
private let choiceFilter : ChoiceFilter < T >
8
+ private let orderMatchesByScore : Bool
8
9
private let outputStream : AsyncStream < Void >
9
10
private let reverse : Bool
10
11
@@ -19,13 +20,15 @@ final class ViewState<T: Selectable> {
19
20
init (
20
21
choices: [ T ] ,
21
22
matchCaseSensitivity: MatchCaseSensitivity ,
23
+ orderMatchesByScore: Bool ,
22
24
reverse: Bool ,
23
25
size: TerminalSize
24
26
) {
25
27
self . choices = choices. enumerated ( ) . map ( FilteredChoiceItem . init ( index: choice: ) )
26
28
self . unfilteredChoices = choices
27
29
self . choiceFilter = ChoiceFilter ( matchCaseSensitivity: matchCaseSensitivity)
28
30
self . current = choices. isEmpty ? nil : choices. count - 1
31
+ self . orderMatchesByScore = orderMatchesByScore
29
32
self . reverse = reverse
30
33
self . size = size
31
34
self . visibleLines = max ( choices. count - size. height + 2 , 0 ) ... max ( choices. count - 1 , 0 )
@@ -71,7 +74,14 @@ final class ViewState<T: Selectable> {
71
74
72
75
func addChoices( _ choices: [ T ] ) {
73
76
self . unfilteredChoices. append ( contentsOf: choices)
74
- self . choiceFilter. addJob ( . init( choices: self . unfilteredChoices, filter: self . filter, reverse: self . reverse) )
77
+ self . choiceFilter. addJob (
78
+ . init(
79
+ choices: self . unfilteredChoices,
80
+ filter: self . filter,
81
+ orderByScore: self . orderMatchesByScore,
82
+ reverse: self . reverse
83
+ )
84
+ )
75
85
}
76
86
77
87
func editFilter( _ action: EditAction ) {
@@ -134,7 +144,14 @@ final class ViewState<T: Selectable> {
134
144
get { self . _filter }
135
145
set {
136
146
self . _filter = newValue
137
- self . choiceFilter. addJob ( . init( choices: self . unfilteredChoices, filter: newValue, reverse: self . reverse) )
147
+ self . choiceFilter. addJob (
148
+ . init(
149
+ choices: self . unfilteredChoices,
150
+ filter: newValue,
151
+ orderByScore: self . orderMatchesByScore,
152
+ reverse: self . reverse
153
+ )
154
+ )
138
155
}
139
156
}
140
157
@@ -257,6 +274,7 @@ private actor ChoiceFilter<T: Selectable> {
257
274
struct Job {
258
275
var choices : [ T ]
259
276
var filter : String
277
+ var orderByScore : Bool
260
278
var reverse : Bool
261
279
}
262
280
@@ -320,14 +338,27 @@ extension ChoiceFilter {
320
338
}
321
339
322
340
let enumeratedChoices = job. choices. enumerated ( )
323
- if job. reverse {
324
- return self . runFilter ( enumeratedChoices. reversed ( ) , filter: job. filter, caseSensitive: caseSensitive)
325
- } else {
326
- return self . runFilter ( enumeratedChoices, filter: job. filter, caseSensitive: caseSensitive)
341
+ switch ( job. reverse, job. orderByScore) {
342
+ case ( true , false ) :
343
+ return self . runOrderPreservingFilter (
344
+ enumeratedChoices. reversed ( ) , filter: job. filter, caseSensitive: caseSensitive
345
+ )
346
+ case ( false , false ) :
347
+ return self . runOrderPreservingFilter (
348
+ enumeratedChoices, filter: job. filter, caseSensitive: caseSensitive
349
+ )
350
+ case ( true , true ) :
351
+ return self . runScoringFilter (
352
+ enumeratedChoices. reversed ( ) , filter: job. filter, caseSensitive: caseSensitive
353
+ )
354
+ case ( false , true ) :
355
+ return self . runScoringFilter (
356
+ enumeratedChoices, filter: job. filter, caseSensitive: caseSensitive
357
+ )
327
358
}
328
359
}
329
360
330
- private func runFilter < S: Sequence > (
361
+ private func runOrderPreservingFilter < S: Sequence > (
331
362
_ choices: S ,
332
363
filter: String ,
333
364
caseSensitive: Bool
@@ -336,6 +367,42 @@ extension ChoiceFilter {
336
367
isMatch ( $1. description, filter: filter, caseSensitive: caseSensitive)
337
368
} . map ( FilteredChoiceItem . init ( index: choice: ) )
338
369
}
370
+
371
+ private func runScoringFilter< S: Sequence > (
372
+ _ choices: S ,
373
+ filter: String ,
374
+ caseSensitive: Bool
375
+ ) -> [ FilteredChoiceItem < T > ] where S. Element == ( offset: Int , element: T ) {
376
+ return choices. compactMap { ( choice: S . Element ) -> ( Int , S . Element ) ? in
377
+ switch scoreMatch ( choice. element. description, filter: filter, caseSensitive: caseSensitive) {
378
+ case . noMatch: return nil
379
+ case let . match( score: score) : return ( score, choice)
380
+ }
381
+ } . sorted { ( lhs: ( Int , ( offset: Int , element: T ) ) , rhs: ( Int , ( offset: Int , element: T ) ) ) in
382
+ lhs. 0 > rhs. 0
383
+ } . map { _, choice in
384
+ FilteredChoiceItem . init ( index: choice. offset, choice: choice. element)
385
+ }
386
+ }
387
+ }
388
+
389
+ enum ScoredMatchResult : Equatable {
390
+ case noMatch
391
+ case match( score: Int )
392
+ }
393
+
394
+ func scoreMatch( _ string: String , filter: String , caseSensitive: Bool ) -> ScoredMatchResult {
395
+ var characters = Array ( caseSensitive ? string : string. lowercased ( ) )
396
+ let filterCharacters = Array ( caseSensitive ? filter : filter. lowercased ( ) )
397
+ var score = 0
398
+ for filterCharacter in filterCharacters {
399
+ guard let index = characters. firstIndex ( of: filterCharacter) else {
400
+ return . noMatch
401
+ }
402
+ score += index
403
+ characters. removeFirst ( index + 1 )
404
+ }
405
+ return . match( score: score)
339
406
}
340
407
341
408
func isMatch( _ string: String , filter: String , caseSensitive: Bool ) -> Bool {
0 commit comments