@@ -9,6 +9,7 @@ import { sep } from 'vs/base/common/path';
99import { isWindows , isLinux } from 'vs/base/common/platform' ;
1010import { stripWildcards , equalsIgnoreCase } from 'vs/base/common/strings' ;
1111import { CharCode } from 'vs/base/common/charCode' ;
12+ import { distinctES6 } from 'vs/base/common/arrays' ;
1213
1314export type Score = [ number /* score */ , number [ ] /* match positions */ ] ;
1415export type ScorerCache = { [ key : string ] : IItemScore } ;
@@ -19,7 +20,40 @@ const NO_SCORE: Score = [NO_MATCH, []];
1920// const DEBUG = false;
2021// const DEBUG_MATRIX = false;
2122
22- export function score ( target : string , query : string , queryLower : string , fuzzy : boolean ) : Score {
23+ export function score ( target : string , query : IPreparedQuery , fuzzy : boolean ) : Score {
24+ if ( query . values && query . values . length > 1 ) {
25+ return scoreMultiple ( target , query . values , fuzzy ) ;
26+ }
27+
28+ return scoreSingle ( target , query . value , query . valueLowercase , fuzzy ) ;
29+ }
30+
31+ function scoreMultiple ( target : string , query : IPreparedQueryPiece [ ] , fuzzy : boolean ) : Score {
32+ let totalScore = NO_MATCH ;
33+ const totalPositions : number [ ] = [ ] ;
34+
35+ for ( const { value, valueLowercase } of query ) {
36+ const [ scoreValue , positions ] = scoreSingle ( target , value , valueLowercase , fuzzy ) ;
37+ if ( scoreValue === NO_MATCH ) {
38+ // if a single query value does not match, return with
39+ // no score entirely, we require all queries to match
40+ return NO_SCORE ;
41+ }
42+
43+ totalScore += scoreValue ;
44+ totalPositions . push ( ...positions ) ;
45+ }
46+
47+ if ( totalScore === NO_MATCH ) {
48+ return NO_SCORE ;
49+ }
50+
51+ // if we have a score, ensure that the positions are
52+ // sorted in ascending order and distinct
53+ return [ totalScore , distinctES6 ( totalPositions ) . sort ( ( a , b ) => a - b ) ] ;
54+ }
55+
56+ function scoreSingle ( target : string , query : string , queryLower : string , fuzzy : boolean ) : Score {
2357 if ( ! target || ! query ) {
2458 return NO_SCORE ; // return early if target or query are undefined
2559 }
@@ -303,32 +337,70 @@ const LABEL_PREFIX_SCORE = 1 << 17;
303337const LABEL_CAMELCASE_SCORE = 1 << 16 ;
304338const LABEL_SCORE_THRESHOLD = 1 << 15 ;
305339
306- export interface IPreparedQuery {
340+ export interface IPreparedQueryPiece {
307341 original : string ;
342+ originalLowercase : string ;
343+
308344 value : string ;
309- lowercase : string ;
345+ valueLowercase : string ;
346+ }
347+
348+ export interface IPreparedQuery extends IPreparedQueryPiece {
349+
350+ // Split by spaces
351+ values : IPreparedQueryPiece [ ] | undefined ;
352+
310353 containsPathSeparator : boolean ;
311354}
312355
313356/**
314- * Helper function to prepare a search value for scoring by removing unwanted characters.
357+ * Helper function to prepare a search value for scoring by removing unwanted characters
358+ * and allowing to score on multiple pieces separated by whitespace character.
315359 */
360+ const MULTIPL_QUERY_VALUES_SEPARATOR = ' ' ;
316361export function prepareQuery ( original : string ) : IPreparedQuery {
317- if ( ! original ) {
362+ if ( typeof original !== 'string' ) {
318363 original = '' ;
319364 }
320365
366+ const originalLowercase = original . toLowerCase ( ) ;
367+ const value = prepareQueryValue ( original ) ;
368+ const valueLowercase = value . toLowerCase ( ) ;
369+ const containsPathSeparator = value . indexOf ( sep ) >= 0 ;
370+
371+ let values : IPreparedQueryPiece [ ] | undefined = undefined ;
372+
373+ const originalSplit = original . split ( MULTIPL_QUERY_VALUES_SEPARATOR ) ;
374+ if ( originalSplit . length > 1 ) {
375+ for ( const originalPiece of originalSplit ) {
376+ const valuePiece = prepareQueryValue ( originalPiece ) ;
377+ if ( valuePiece ) {
378+ if ( ! values ) {
379+ values = [ ] ;
380+ }
381+
382+ values . push ( {
383+ original : originalPiece ,
384+ originalLowercase : originalPiece . toLowerCase ( ) ,
385+ value : valuePiece ,
386+ valueLowercase : valuePiece . toLowerCase ( )
387+ } ) ;
388+ }
389+ }
390+ }
391+
392+ return { original, originalLowercase, value, valueLowercase, values, containsPathSeparator } ;
393+ }
394+
395+ function prepareQueryValue ( original : string ) : string {
321396 let value = stripWildcards ( original ) . replace ( / \s / g, '' ) ; // get rid of all wildcards and whitespace
322397 if ( isWindows ) {
323398 value = value . replace ( / \/ / g, sep ) ; // Help Windows users to search for paths when using slash
324399 } else {
325400 value = value . replace ( / \\ / g, sep ) ; // Help macOS/Linux users to search for paths when using backslash
326401 }
327402
328- const lowercase = value . toLowerCase ( ) ;
329- const containsPathSeparator = value . indexOf ( sep ) >= 0 ;
330-
331- return { original, value, lowercase, containsPathSeparator } ;
403+ return value ;
332404}
333405
334406export function scoreItem < T > ( item : T , query : IPreparedQuery , fuzzy : boolean , accessor : IItemAccessor < T > , cache : ScorerCache ) : IItemScore {
@@ -404,7 +476,7 @@ function doScoreItem(label: string, description: string | undefined, path: strin
404476 }
405477
406478 // 4.) prefer scores on the label if any
407- const [ labelScore , labelPositions ] = score ( label , query . value , query . lowercase , fuzzy ) ;
479+ const [ labelScore , labelPositions ] = score ( label , query , fuzzy ) ;
408480 if ( labelScore ) {
409481 return { score : labelScore + LABEL_SCORE_THRESHOLD , labelMatch : createMatches ( labelPositions ) } ;
410482 }
@@ -420,7 +492,7 @@ function doScoreItem(label: string, description: string | undefined, path: strin
420492 const descriptionPrefixLength = descriptionPrefix . length ;
421493 const descriptionAndLabel = `${ descriptionPrefix } ${ label } ` ;
422494
423- const [ labelDescriptionScore , labelDescriptionPositions ] = score ( descriptionAndLabel , query . value , query . lowercase , fuzzy ) ;
495+ const [ labelDescriptionScore , labelDescriptionPositions ] = score ( descriptionAndLabel , query , fuzzy ) ;
424496 if ( labelDescriptionScore ) {
425497 const labelDescriptionMatches = createMatches ( labelDescriptionPositions ) ;
426498 const labelMatch : IMatch [ ] = [ ] ;
0 commit comments