diff --git a/src/common/string/filter/filter.ts b/src/common/string/filter/filter.ts index ff42c20393..a592a8e81e 100644 --- a/src/common/string/filter/filter.ts +++ b/src/common/string/filter/filter.ts @@ -510,13 +510,10 @@ export interface FuzzyScorer { ): FuzzyScore | undefined; } -export function createMatches(score: undefined | FuzzyScore): Match[] { - if (typeof score === "undefined") { - return []; - } +function _createMatches(score: FuzzyScore, wordPos: number) { const res: Match[] = []; - const wordPos = score[1]; - for (let i = score.length - 1; i > 1; i--) { + + for (let i = score.length - 1; i >= 0; i--) { const pos = score[i] + wordPos; const last = res[res.length - 1]; if (last && last.end === pos) { @@ -525,9 +522,64 @@ export function createMatches(score: undefined | FuzzyScore): Match[] { res.push({ start: pos, end: pos + 1 }); } } + return res; } +export function createMatches(score: undefined | FuzzyScore): Match[] { + if (typeof score === "undefined") { + return []; + } + + const wordPos = score[1]; + const _score = score.splice(2); + + return _createMatches(_score, wordPos); +} + +// The first and second elements in score represent total score, and the offset at which +// matching started. For this method, we only care about match positions, not the score +// or offset. +const findFirstOutOfRangeElement = (number, score: FuzzyScore) => + score.findIndex((num) => num < number); + +export function createMatchesFragmented( + score: undefined | FuzzyScore, + strings: string[] +): Match[][] { + if (typeof score === "undefined") { + return []; + } + + const matches: Match[][] = []; + const wordPos = score[1]; + let lengthCounter = 0; + const _score = score.splice(2); + const fragmentedScores: FuzzyScore[] = []; + + for (const string of strings) { + const prevLengthCounter = lengthCounter; + lengthCounter += string.length; + const lastIndex = findFirstOutOfRangeElement(lengthCounter, _score); + + if (lastIndex < 0) { + fragmentedScores.push([]); + continue; + } + + fragmentedScores.push( + _score.splice(lastIndex).map((pos) => pos - prevLengthCounter) + ); + } + + for (const fragmentedScore of fragmentedScores) { + const res = _createMatches(fragmentedScore, wordPos); + matches.push(res); + } + + return matches; +} + /** * A fast function (therefore imprecise) to check if code points are emojis. * Generated using https://github.com/alexdima/unicode-utils/blob/master/generate-emoji-test.js diff --git a/src/common/string/filter/sequence-matching.ts b/src/common/string/filter/sequence-matching.ts index 919d3b4dba..9278bf724c 100644 --- a/src/common/string/filter/sequence-matching.ts +++ b/src/common/string/filter/sequence-matching.ts @@ -1,4 +1,9 @@ -import { createMatches, FuzzyScore, fuzzyScore } from "./filter"; +import { + createMatches, + createMatchesFragmented, + FuzzyScore, + fuzzyScore, +} from "./filter"; /** * Determine whether a sequence of letters exists in another string, @@ -23,8 +28,11 @@ export const fuzzySequentialMatch: FuzzySequentialMatcher = ( ) => { let topScore = Number.NEGATIVE_INFINITY; const decoratedStrings: string[][] = []; + const strings = item.treatArrayAsSingleString + ? [item.strings.join("")] + : item.strings; - for (const word of item.strings) { + for (const word of strings) { const scores = fuzzyScore( filter, filter.toLowerCase(), @@ -36,7 +44,7 @@ export const fuzzySequentialMatch: FuzzySequentialMatcher = ( ); if (decorate) { - decoratedStrings.push(decorate(word, scores)); + decoratedStrings.push(decorate(word, item, scores)); } if (!scores) { @@ -81,6 +89,7 @@ export interface ScorableTextItem { score?: number; strings: string[]; decoratedStrings?: string[][]; + treatArrayAsSingleString?: boolean; } type FuzzyFilterSort = ( @@ -109,43 +118,49 @@ export const fuzzyFilterSort: FuzzyFilterSort = ( ); }; -type MatchDecorator = (word: string, scores?: FuzzyScore) => string[]; +type MatchDecorator = ( + word: string, + item: ScorableTextItem, + scores?: FuzzyScore +) => string[]; export const createMatchDecorator: ( left: string, right: string -) => MatchDecorator = (left, right) => (word, scores) => - _decorateMatch(word, [left, right], scores); +) => MatchDecorator = (left, right) => (word, item, scores) => + _decorateMatch(word, [left, right], item, scores); const _decorateMatch: ( word: string, surroundWith: [string, string], + item: ScorableTextItem, scores?: FuzzyScore -) => string[] = (word, surroundWith, scores) => { +) => string[] = (word, surroundWith, item, scores) => { if (!scores) { return [word]; } const decoratedText: string[] = []; - const matches = createMatches(scores); + const matches = item.treatArrayAsSingleString + ? createMatchesFragmented(scores, item.strings) + : [createMatches(scores)]; const [left, right] = surroundWith; - let pos = 0; - let actualWord = ""; - for (const match of matches) { - actualWord += - word.substring(pos, match.start) + - left + - word.substring(match.start, match.end) + - right; - pos = match.end; + for (let i = 0; i < matches.length; i++) { + const match = matches[i]; + const _word = item.treatArrayAsSingleString ? item.strings[i] : word; + let pos = 0; + let actualWord = ""; + for (const fragmentedMatch of match) { + actualWord += + _word.substring(pos, fragmentedMatch.start) + + left + + _word.substring(fragmentedMatch.start, fragmentedMatch.end) + + right; + pos = fragmentedMatch.end; + } + actualWord += _word.substring(pos); + + decoratedText.push(actualWord); } - actualWord += word.substring(pos); - - const fragments = actualWord.split("::"); - - for (const fragment of fragments) { - decoratedText.push(fragment); - } - return decoratedText; }; diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index fe49937964..61e2ee1ea5 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -476,7 +476,8 @@ export class QuickBar extends LitElement { return { ...commandItem, categoryKey: "reload", - strings: [`${commandItem.categoryText}::${commandItem.primaryText}`], + strings: [`${commandItem.categoryText} `, commandItem.primaryText], + treatArrayAsSingleString: true, }; }); } @@ -510,7 +511,8 @@ export class QuickBar extends LitElement { return this._generateConfirmationCommand( { ...item, - strings: [`${item.categoryText}::${item.primaryText}`], + strings: [`${item.categoryText} `, item.primaryText], + treatArrayAsSingleString: true, }, this.hass.localize("ui.dialogs.generic.ok") ); @@ -611,7 +613,8 @@ export class QuickBar extends LitElement { return { ...navItem, - strings: [`${navItem.categoryText}::${navItem.primaryText}`], + strings: [`${navItem.categoryText} `, navItem.primaryText], + treatArrayAsSingleString: true, categoryKey, }; });