@@ -4,6 +4,12 @@ import type { Dirent } from "node:fs";
44import { Cache , Duration , Effect , Exit , Layer , Option , Path } from "effect" ;
55
66import { type ProjectEntry } from "@t3tools/contracts" ;
7+ import {
8+ insertRankedSearchResult ,
9+ normalizeSearchQuery ,
10+ scoreQueryMatch ,
11+ type RankedSearchResult ,
12+ } from "@t3tools/shared/searchRanking" ;
713
814import { GitCore } from "../../git/Services/GitCore.ts" ;
915import {
@@ -40,10 +46,7 @@ interface SearchableWorkspaceEntry extends ProjectEntry {
4046 normalizedName : string ;
4147}
4248
43- interface RankedWorkspaceEntry {
44- entry : SearchableWorkspaceEntry ;
45- score : number ;
46- }
49+ type RankedWorkspaceEntry = RankedSearchResult < SearchableWorkspaceEntry > ;
4750
4851function toPosixPath ( input : string ) : string {
4952 return input . replaceAll ( "\\" , "/" ) ;
@@ -74,127 +77,39 @@ function toSearchableWorkspaceEntry(entry: ProjectEntry): SearchableWorkspaceEnt
7477 } ;
7578}
7679
77- function normalizeQuery ( input : string ) : string {
78- return input
79- . trim ( )
80- . replace ( / ^ [ @ . / ] + / , "" )
81- . toLowerCase ( ) ;
82- }
83-
84- function scoreSubsequenceMatch ( value : string , query : string ) : number | null {
85- if ( ! query ) return 0 ;
86-
87- let queryIndex = 0 ;
88- let firstMatchIndex = - 1 ;
89- let previousMatchIndex = - 1 ;
90- let gapPenalty = 0 ;
91-
92- for ( let valueIndex = 0 ; valueIndex < value . length ; valueIndex += 1 ) {
93- if ( value [ valueIndex ] !== query [ queryIndex ] ) {
94- continue ;
95- }
96-
97- if ( firstMatchIndex === - 1 ) {
98- firstMatchIndex = valueIndex ;
99- }
100- if ( previousMatchIndex !== - 1 ) {
101- gapPenalty += valueIndex - previousMatchIndex - 1 ;
102- }
103-
104- previousMatchIndex = valueIndex ;
105- queryIndex += 1 ;
106- if ( queryIndex === query . length ) {
107- const spanPenalty = valueIndex - firstMatchIndex + 1 - query . length ;
108- const lengthPenalty = Math . min ( 64 , value . length - query . length ) ;
109- return firstMatchIndex * 2 + gapPenalty * 3 + spanPenalty + lengthPenalty ;
110- }
111- }
112-
113- return null ;
114- }
115-
11680function scoreEntry ( entry : SearchableWorkspaceEntry , query : string ) : number | null {
11781 if ( ! query ) {
11882 return entry . kind === "directory" ? 0 : 1 ;
11983 }
12084
12185 const { normalizedPath, normalizedName } = entry ;
12286
123- if ( normalizedName === query ) return 0 ;
124- if ( normalizedPath === query ) return 1 ;
125- if ( normalizedName . startsWith ( query ) ) return 2 ;
126- if ( normalizedPath . startsWith ( query ) ) return 3 ;
127- if ( normalizedPath . includes ( `/${ query } ` ) ) return 4 ;
128- if ( normalizedName . includes ( query ) ) return 5 ;
129- if ( normalizedPath . includes ( query ) ) return 6 ;
130-
131- const nameFuzzyScore = scoreSubsequenceMatch ( normalizedName , query ) ;
132- if ( nameFuzzyScore !== null ) {
133- return 100 + nameFuzzyScore ;
134- }
135-
136- const pathFuzzyScore = scoreSubsequenceMatch ( normalizedPath , query ) ;
137- if ( pathFuzzyScore !== null ) {
138- return 200 + pathFuzzyScore ;
87+ const scores = [
88+ scoreQueryMatch ( {
89+ value : normalizedName ,
90+ query,
91+ exactBase : 0 ,
92+ prefixBase : 2 ,
93+ includesBase : 5 ,
94+ fuzzyBase : 100 ,
95+ } ) ,
96+ scoreQueryMatch ( {
97+ value : normalizedPath ,
98+ query,
99+ exactBase : 1 ,
100+ prefixBase : 3 ,
101+ boundaryBase : 4 ,
102+ includesBase : 6 ,
103+ fuzzyBase : 200 ,
104+ boundaryMarkers : [ "/" ] ,
105+ } ) ,
106+ ] . filter ( ( score ) : score is number => score !== null ) ;
107+
108+ if ( scores . length === 0 ) {
109+ return null ;
139110 }
140111
141- return null ;
142- }
143-
144- function compareRankedWorkspaceEntries (
145- left : RankedWorkspaceEntry ,
146- right : RankedWorkspaceEntry ,
147- ) : number {
148- const scoreDelta = left . score - right . score ;
149- if ( scoreDelta !== 0 ) return scoreDelta ;
150- return left . entry . path . localeCompare ( right . entry . path ) ;
151- }
152-
153- function findInsertionIndex (
154- rankedEntries : RankedWorkspaceEntry [ ] ,
155- candidate : RankedWorkspaceEntry ,
156- ) : number {
157- let low = 0 ;
158- let high = rankedEntries . length ;
159-
160- while ( low < high ) {
161- const middle = low + Math . floor ( ( high - low ) / 2 ) ;
162- const current = rankedEntries [ middle ] ;
163- if ( ! current ) {
164- break ;
165- }
166-
167- if ( compareRankedWorkspaceEntries ( candidate , current ) < 0 ) {
168- high = middle ;
169- } else {
170- low = middle + 1 ;
171- }
172- }
173-
174- return low ;
175- }
176-
177- function insertRankedEntry (
178- rankedEntries : RankedWorkspaceEntry [ ] ,
179- candidate : RankedWorkspaceEntry ,
180- limit : number ,
181- ) : void {
182- if ( limit <= 0 ) {
183- return ;
184- }
185-
186- const insertionIndex = findInsertionIndex ( rankedEntries , candidate ) ;
187- if ( rankedEntries . length < limit ) {
188- rankedEntries . splice ( insertionIndex , 0 , candidate ) ;
189- return ;
190- }
191-
192- if ( insertionIndex >= limit ) {
193- return ;
194- }
195-
196- rankedEntries . splice ( insertionIndex , 0 , candidate ) ;
197- rankedEntries . pop ( ) ;
112+ return Math . min ( ...scores ) ;
198113}
199114
200115function isPathInIgnoredDirectory ( relativePath : string ) : boolean {
@@ -469,7 +384,9 @@ export const makeWorkspaceEntries = Effect.gen(function* () {
469384 const normalizedCwd = yield * normalizeWorkspaceRoot ( input . cwd ) ;
470385 return yield * Cache . get ( workspaceIndexCache , normalizedCwd ) . pipe (
471386 Effect . map ( ( index ) => {
472- const normalizedQuery = normalizeQuery ( input . query ) ;
387+ const normalizedQuery = normalizeSearchQuery ( input . query , {
388+ trimLeadingPattern : / ^ [ @ . / ] + / ,
389+ } ) ;
473390 const limit = Math . max ( 0 , Math . floor ( input . limit ) ) ;
474391 const rankedEntries : RankedWorkspaceEntry [ ] = [ ] ;
475392 let matchedEntryCount = 0 ;
@@ -481,11 +398,15 @@ export const makeWorkspaceEntries = Effect.gen(function* () {
481398 }
482399
483400 matchedEntryCount += 1 ;
484- insertRankedEntry ( rankedEntries , { entry, score } , limit ) ;
401+ insertRankedSearchResult (
402+ rankedEntries ,
403+ { item : entry , score, tieBreaker : entry . path } ,
404+ limit ,
405+ ) ;
485406 }
486407
487408 return {
488- entries : rankedEntries . map ( ( candidate ) => candidate . entry ) ,
409+ entries : rankedEntries . map ( ( candidate ) => candidate . item ) ,
489410 truncated : index . truncated || matchedEntryCount > limit ,
490411 } ;
491412 } ) ,
0 commit comments