mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-31 21:17:47 +00:00
Highlight matched letters in quick bar
This commit is contained in:
parent
6de8b4e35f
commit
56d4fbab86
File diff suppressed because it is too large
Load Diff
399
src/common/string/filter/linked-map.ts
Normal file
399
src/common/string/filter/linked-map.ts
Normal file
@ -0,0 +1,399 @@
|
|||||||
|
export enum EntityTouch {
|
||||||
|
None = 0,
|
||||||
|
AsOld = 1,
|
||||||
|
AsNew = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Item<K, V> {
|
||||||
|
previous: Item<K, V> | undefined;
|
||||||
|
next: Item<K, V> | undefined;
|
||||||
|
key: K;
|
||||||
|
value: V;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LinkedMap<K, V> implements Map<K, V> {
|
||||||
|
readonly [Symbol.toStringTag] = "LinkedMap";
|
||||||
|
|
||||||
|
private _map: Map<K, Item<K, V>>;
|
||||||
|
|
||||||
|
private _head: Item<K, V> | undefined;
|
||||||
|
|
||||||
|
private _tail: Item<K, V> | undefined;
|
||||||
|
|
||||||
|
private _size: number;
|
||||||
|
|
||||||
|
private _state: number;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._map = new Map<K, Item<K, V>>();
|
||||||
|
this._head = undefined;
|
||||||
|
this._tail = undefined;
|
||||||
|
this._size = 0;
|
||||||
|
this._state = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this._map.clear();
|
||||||
|
this._head = undefined;
|
||||||
|
this._tail = undefined;
|
||||||
|
this._size = 0;
|
||||||
|
this._state++;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty(): boolean {
|
||||||
|
return !this._head && !this._tail;
|
||||||
|
}
|
||||||
|
|
||||||
|
get size(): number {
|
||||||
|
return this._size;
|
||||||
|
}
|
||||||
|
|
||||||
|
get first(): V | undefined {
|
||||||
|
return this._head?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get last(): V | undefined {
|
||||||
|
return this._tail?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
has(key: K): boolean {
|
||||||
|
return this._map.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: K, touch: EntityTouch = EntityTouch.None): V | undefined {
|
||||||
|
const item = this._map.get(key);
|
||||||
|
if (!item) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (touch !== EntityTouch.None) {
|
||||||
|
this.touch(item, touch);
|
||||||
|
}
|
||||||
|
return item.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: K, value: V, touch: EntityTouch = EntityTouch.None): this {
|
||||||
|
let item = this._map.get(key);
|
||||||
|
if (item) {
|
||||||
|
item.value = value;
|
||||||
|
if (touch !== EntityTouch.None) {
|
||||||
|
this.touch(item, touch);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item = { key, value, next: undefined, previous: undefined };
|
||||||
|
switch (touch) {
|
||||||
|
case EntityTouch.None:
|
||||||
|
this.addItemLast(item);
|
||||||
|
break;
|
||||||
|
case EntityTouch.AsOld:
|
||||||
|
this.addItemFirst(item);
|
||||||
|
break;
|
||||||
|
case EntityTouch.AsNew:
|
||||||
|
this.addItemLast(item);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.addItemLast(item);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this._map.set(key, item);
|
||||||
|
this._size++;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(key: K): boolean {
|
||||||
|
return !!this.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(key: K): V | undefined {
|
||||||
|
const item = this._map.get(key);
|
||||||
|
if (!item) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
this._map.delete(key);
|
||||||
|
this.removeItem(item);
|
||||||
|
this._size--;
|
||||||
|
return item.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
shift(): V | undefined {
|
||||||
|
if (!this._head && !this._tail) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!this._head || !this._tail) {
|
||||||
|
throw new Error("Invalid list");
|
||||||
|
}
|
||||||
|
const item = this._head;
|
||||||
|
this._map.delete(item.key);
|
||||||
|
this.removeItem(item);
|
||||||
|
this._size--;
|
||||||
|
return item.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
forEach(
|
||||||
|
callbackfn: (value: V, key: K, map: LinkedMap<K, V>) => void,
|
||||||
|
thisArg?: any
|
||||||
|
): void {
|
||||||
|
const state = this._state;
|
||||||
|
let current = this._head;
|
||||||
|
while (current) {
|
||||||
|
if (thisArg) {
|
||||||
|
callbackfn.bind(thisArg)(current.value, current.key, this);
|
||||||
|
} else {
|
||||||
|
callbackfn(current.value, current.key, this);
|
||||||
|
}
|
||||||
|
if (this._state !== state) {
|
||||||
|
throw new Error(`LinkedMap got modified during iteration.`);
|
||||||
|
}
|
||||||
|
current = current.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys(): IterableIterator<K> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
|
const map = this;
|
||||||
|
const state = this._state;
|
||||||
|
let current = this._head;
|
||||||
|
const iterator: IterableIterator<K> = {
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return iterator;
|
||||||
|
},
|
||||||
|
next(): IteratorResult<K> {
|
||||||
|
if (map._state !== state) {
|
||||||
|
throw new Error(`LinkedMap got modified during iteration.`);
|
||||||
|
}
|
||||||
|
if (current) {
|
||||||
|
const result = { value: current.key, done: false };
|
||||||
|
current = current.next;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return { value: undefined, done: true };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return iterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
values(): IterableIterator<V> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
|
const map = this;
|
||||||
|
const state = this._state;
|
||||||
|
let current = this._head;
|
||||||
|
const iterator: IterableIterator<V> = {
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return iterator;
|
||||||
|
},
|
||||||
|
next(): IteratorResult<V> {
|
||||||
|
if (map._state !== state) {
|
||||||
|
throw new Error(`LinkedMap got modified during iteration.`);
|
||||||
|
}
|
||||||
|
if (current) {
|
||||||
|
const result = {
|
||||||
|
value: current.value,
|
||||||
|
done: false,
|
||||||
|
};
|
||||||
|
current = current.next;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return { value: undefined, done: true };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return iterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries(): IterableIterator<[K, V]> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
|
const map = this;
|
||||||
|
const state = this._state;
|
||||||
|
let current = this._head;
|
||||||
|
const iterator: IterableIterator<[K, V]> = {
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return iterator;
|
||||||
|
},
|
||||||
|
next(): IteratorResult<[K, V]> {
|
||||||
|
if (map._state !== state) {
|
||||||
|
throw new Error(`LinkedMap got modified during iteration.`);
|
||||||
|
}
|
||||||
|
if (current) {
|
||||||
|
const result: IteratorResult<[K, V]> = {
|
||||||
|
value: [current.key, current.value],
|
||||||
|
done: false,
|
||||||
|
};
|
||||||
|
current = current.next;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value: undefined,
|
||||||
|
done: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return iterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator](): IterableIterator<[K, V]> {
|
||||||
|
return this.entries();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected trimOld(newSize: number) {
|
||||||
|
if (newSize >= this.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newSize === 0) {
|
||||||
|
this.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let current = this._head;
|
||||||
|
let currentSize = this.size;
|
||||||
|
while (current && currentSize > newSize) {
|
||||||
|
this._map.delete(current.key);
|
||||||
|
current = current.next;
|
||||||
|
currentSize--;
|
||||||
|
}
|
||||||
|
this._head = current;
|
||||||
|
this._size = currentSize;
|
||||||
|
if (current) {
|
||||||
|
current.previous = undefined;
|
||||||
|
}
|
||||||
|
this._state++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private addItemFirst(item: Item<K, V>): void {
|
||||||
|
// First time Insert
|
||||||
|
if (!this._head && !this._tail) {
|
||||||
|
this._tail = item;
|
||||||
|
} else if (!this._head) {
|
||||||
|
throw new Error("Invalid list");
|
||||||
|
} else {
|
||||||
|
item.next = this._head;
|
||||||
|
this._head.previous = item;
|
||||||
|
}
|
||||||
|
this._head = item;
|
||||||
|
this._state++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private addItemLast(item: Item<K, V>): void {
|
||||||
|
// First time Insert
|
||||||
|
if (!this._head && !this._tail) {
|
||||||
|
this._head = item;
|
||||||
|
} else if (!this._tail) {
|
||||||
|
throw new Error("Invalid list");
|
||||||
|
} else {
|
||||||
|
item.previous = this._tail;
|
||||||
|
this._tail.next = item;
|
||||||
|
}
|
||||||
|
this._tail = item;
|
||||||
|
this._state++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeItem(item: Item<K, V>): void {
|
||||||
|
if (item === this._head && item === this._tail) {
|
||||||
|
this._head = undefined;
|
||||||
|
this._tail = undefined;
|
||||||
|
} else if (item === this._head) {
|
||||||
|
// This can only happend if size === 1 which is handle
|
||||||
|
// by the case above.
|
||||||
|
if (!item.next) {
|
||||||
|
throw new Error("Invalid list");
|
||||||
|
}
|
||||||
|
item.next.previous = undefined;
|
||||||
|
this._head = item.next;
|
||||||
|
} else if (item === this._tail) {
|
||||||
|
// This can only happend if size === 1 which is handle
|
||||||
|
// by the case above.
|
||||||
|
if (!item.previous) {
|
||||||
|
throw new Error("Invalid list");
|
||||||
|
}
|
||||||
|
item.previous.next = undefined;
|
||||||
|
this._tail = item.previous;
|
||||||
|
} else {
|
||||||
|
const next = item.next;
|
||||||
|
const previous = item.previous;
|
||||||
|
if (!next || !previous) {
|
||||||
|
throw new Error("Invalid list");
|
||||||
|
}
|
||||||
|
next.previous = previous;
|
||||||
|
previous.next = next;
|
||||||
|
}
|
||||||
|
item.next = undefined;
|
||||||
|
item.previous = undefined;
|
||||||
|
this._state++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private touch(item: Item<K, V>, touch: EntityTouch): void {
|
||||||
|
if (!this._head || !this._tail) {
|
||||||
|
throw new Error("Invalid list");
|
||||||
|
}
|
||||||
|
if (touch !== EntityTouch.AsOld && touch !== EntityTouch.AsNew) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (touch === EntityTouch.AsOld) {
|
||||||
|
if (item === this._head) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = item.next;
|
||||||
|
const previous = item.previous;
|
||||||
|
|
||||||
|
// Unlink the item
|
||||||
|
if (item === this._tail) {
|
||||||
|
// previous must be defined since item was not head but is tail
|
||||||
|
// So there are more than on item in the map
|
||||||
|
previous!.next = undefined;
|
||||||
|
this._tail = previous;
|
||||||
|
} else {
|
||||||
|
// Both next and previous are not undefined since item was neither head nor tail.
|
||||||
|
next!.previous = previous;
|
||||||
|
previous!.next = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the node at head
|
||||||
|
item.previous = undefined;
|
||||||
|
item.next = this._head;
|
||||||
|
this._head.previous = item;
|
||||||
|
this._head = item;
|
||||||
|
this._state++;
|
||||||
|
} else if (touch === EntityTouch.AsNew) {
|
||||||
|
if (item === this._tail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = item.next;
|
||||||
|
const previous = item.previous;
|
||||||
|
|
||||||
|
// Unlink the item.
|
||||||
|
if (item === this._head) {
|
||||||
|
// next must be defined since item was not tail but is head
|
||||||
|
// So there are more than on item in the map
|
||||||
|
next!.previous = undefined;
|
||||||
|
this._head = next;
|
||||||
|
} else {
|
||||||
|
// Both next and previous are not undefined since item was neither head nor tail.
|
||||||
|
next!.previous = previous;
|
||||||
|
previous!.next = next;
|
||||||
|
}
|
||||||
|
item.next = undefined;
|
||||||
|
item.previous = this._tail;
|
||||||
|
this._tail.next = item;
|
||||||
|
this._tail = item;
|
||||||
|
this._state++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): [K, V][] {
|
||||||
|
const data: [K, V][] = [];
|
||||||
|
|
||||||
|
this.forEach((value, key) => {
|
||||||
|
data.push([key, value]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
fromJSON(data: [K, V][]): void {
|
||||||
|
this.clear();
|
||||||
|
|
||||||
|
for (const [key, value] of data) {
|
||||||
|
this.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
src/common/string/filter/lru-cache.ts
Normal file
51
src/common/string/filter/lru-cache.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { EntityTouch, LinkedMap } from "./linked-map";
|
||||||
|
|
||||||
|
export class LRUCache<K, V> extends LinkedMap<K, V> {
|
||||||
|
private _limit: number;
|
||||||
|
|
||||||
|
private _ratio: number;
|
||||||
|
|
||||||
|
constructor(limit: number, ratio = 1) {
|
||||||
|
super();
|
||||||
|
this._limit = limit;
|
||||||
|
this._ratio = Math.min(Math.max(0, ratio), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
get limit(): number {
|
||||||
|
return this._limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
set limit(limit: number) {
|
||||||
|
this._limit = limit;
|
||||||
|
this.checkTrim();
|
||||||
|
}
|
||||||
|
|
||||||
|
get ratio(): number {
|
||||||
|
return this._ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
set ratio(ratio: number) {
|
||||||
|
this._ratio = Math.min(Math.max(0, ratio), 1);
|
||||||
|
this.checkTrim();
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: K, touch: EntityTouch = EntityTouch.AsNew): V | undefined {
|
||||||
|
return super.get(key, touch);
|
||||||
|
}
|
||||||
|
|
||||||
|
peek(key: K): V | undefined {
|
||||||
|
return super.get(key, EntityTouch.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: K, value: V): this {
|
||||||
|
super.set(key, value, EntityTouch.AsNew);
|
||||||
|
this.checkTrim();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkTrim() {
|
||||||
|
if (this.size > this._limit) {
|
||||||
|
this.trimOld(Math.round(this._limit * this._ratio));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { fuzzyScore } from "./filter";
|
import { createMatches, FuzzyScore, fuzzyScore } from "./filter";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether a sequence of letters exists in another string,
|
* Determine whether a sequence of letters exists in another string,
|
||||||
@ -11,7 +11,8 @@ import { fuzzyScore } from "./filter";
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
|
export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
|
||||||
let topScore = 0;
|
let topScore = Number.NEGATIVE_INFINITY;
|
||||||
|
let topScores: FuzzyScore | undefined;
|
||||||
|
|
||||||
for (const word of words) {
|
for (const word of words) {
|
||||||
const scores = fuzzyScore(
|
const scores = fuzzyScore(
|
||||||
@ -31,19 +32,29 @@ export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
|
|||||||
// The VS Code implementation of filter treats a score of "0" as just barely a match
|
// The VS Code implementation of filter treats a score of "0" as just barely a match
|
||||||
// But we will typically use this matcher in a .filter(), which interprets 0 as a failure.
|
// But we will typically use this matcher in a .filter(), which interprets 0 as a failure.
|
||||||
// By shifting all scores up by 1, we allow "0" matches, while retaining score precedence
|
// By shifting all scores up by 1, we allow "0" matches, while retaining score precedence
|
||||||
const score = scores[0] + 1;
|
const score = scores[0] === 0 ? 1 : scores[0];
|
||||||
|
|
||||||
if (score > topScore) {
|
if (score > topScore) {
|
||||||
topScore = score;
|
topScore = score;
|
||||||
|
topScores = scores;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return topScore;
|
|
||||||
|
if (topScore === Number.NEGATIVE_INFINITY) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
score: topScore,
|
||||||
|
decoratedText: getDecoratedText(filter, words[0]), // Need to change this to account for any N words
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ScorableTextItem {
|
export interface ScorableTextItem {
|
||||||
score?: number;
|
score?: number;
|
||||||
filterText: string;
|
text: string;
|
||||||
altText?: string;
|
altText?: string;
|
||||||
|
decoratedText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type FuzzyFilterSort = <T extends ScorableTextItem>(
|
type FuzzyFilterSort = <T extends ScorableTextItem>(
|
||||||
@ -54,13 +65,45 @@ type FuzzyFilterSort = <T extends ScorableTextItem>(
|
|||||||
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
|
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
|
||||||
return items
|
return items
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
item.score = item.altText
|
const match = item.altText
|
||||||
? fuzzySequentialMatch(filter, item.filterText, item.altText)
|
? fuzzySequentialMatch(filter, item.text, item.altText)
|
||||||
: fuzzySequentialMatch(filter, item.filterText);
|
: fuzzySequentialMatch(filter, item.text);
|
||||||
|
|
||||||
|
item.score = match?.score;
|
||||||
|
item.decoratedText = match?.decoratedText;
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
})
|
})
|
||||||
.filter((item) => item.score !== undefined && item.score > 0)
|
.filter((item) => item.score !== undefined)
|
||||||
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
|
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
|
||||||
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
|
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDecoratedText = (pattern: string, word: string) => {
|
||||||
|
const r = fuzzyScore(
|
||||||
|
pattern,
|
||||||
|
pattern.toLowerCase(),
|
||||||
|
0,
|
||||||
|
word,
|
||||||
|
word.toLowerCase(),
|
||||||
|
0,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
if (r) {
|
||||||
|
const matches = createMatches(r);
|
||||||
|
let actualWord = "";
|
||||||
|
let pos = 0;
|
||||||
|
for (const match of matches) {
|
||||||
|
actualWord += word.substring(pos, match.start);
|
||||||
|
actualWord +=
|
||||||
|
"^" + word.substring(match.start, match.end).split("").join("^");
|
||||||
|
pos = match.end;
|
||||||
|
}
|
||||||
|
actualWord += word.substring(pos);
|
||||||
|
console.log(actualWord);
|
||||||
|
return actualWord;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
@ -3,7 +3,7 @@ import type { List } from "@material/mwc-list/mwc-list";
|
|||||||
import { SingleSelectedEvent } from "@material/mwc-list/mwc-list-foundation";
|
import { SingleSelectedEvent } from "@material/mwc-list/mwc-list-foundation";
|
||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import type { ListItem } from "@material/mwc-list/mwc-list-item";
|
import type { ListItem } from "@material/mwc-list/mwc-list-item";
|
||||||
import { mdiConsoleLine, mdiEarth, mdiReload, mdiServerNetwork } from "@mdi/js";
|
import { mdiConsoleLine } from "@mdi/js";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
customElement,
|
customElement,
|
||||||
@ -13,6 +13,7 @@ import {
|
|||||||
property,
|
property,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
query,
|
query,
|
||||||
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { ifDefined } from "lit-html/directives/if-defined";
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
import { styleMap } from "lit-html/directives/style-map";
|
import { styleMap } from "lit-html/directives/style-map";
|
||||||
@ -36,7 +37,7 @@ import "../../components/ha-circular-progress";
|
|||||||
import "../../components/ha-dialog";
|
import "../../components/ha-dialog";
|
||||||
import "../../components/ha-header-bar";
|
import "../../components/ha-header-bar";
|
||||||
import { domainToName } from "../../data/integration";
|
import { domainToName } from "../../data/integration";
|
||||||
import { getPanelNameTranslationKey } from "../../data/panel";
|
import { getPanelIcon, getPanelNameTranslationKey } from "../../data/panel";
|
||||||
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||||
import { configSections } from "../../panels/config/ha-panel-config";
|
import { configSections } from "../../panels/config/ha-panel-config";
|
||||||
import { haStyleDialog } from "../../resources/styles";
|
import { haStyleDialog } from "../../resources/styles";
|
||||||
@ -46,44 +47,31 @@ import {
|
|||||||
showConfirmationDialog,
|
showConfirmationDialog,
|
||||||
} from "../generic/show-dialog-box";
|
} from "../generic/show-dialog-box";
|
||||||
import { QuickBarParams } from "./show-dialog-quick-bar";
|
import { QuickBarParams } from "./show-dialog-quick-bar";
|
||||||
import "../../components/ha-chip";
|
|
||||||
|
const DEFAULT_NAVIGATION_ICON = "hass:arrow-right-circle";
|
||||||
|
const DEFAULT_SERVER_ICON = "hass:server";
|
||||||
|
|
||||||
interface QuickBarItem extends ScorableTextItem {
|
interface QuickBarItem extends ScorableTextItem {
|
||||||
primaryText: string;
|
icon?: string;
|
||||||
iconPath?: string;
|
iconPath?: string;
|
||||||
action(data?: any): void;
|
action(data?: any): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CommandItem extends QuickBarItem {
|
interface QuickBarNavigationItem extends QuickBarItem {
|
||||||
categoryKey: "reload" | "navigation" | "server_control";
|
|
||||||
categoryText: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EntityItem extends QuickBarItem {
|
|
||||||
icon?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCommandItem = (item: EntityItem | CommandItem): item is CommandItem => {
|
|
||||||
return (item as CommandItem).categoryKey !== undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface QuickBarNavigationItem extends CommandItem {
|
|
||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type NavigationInfo = PageNavigation & Pick<QuickBarItem, "primaryText">;
|
interface NavigationInfo extends PageNavigation {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
type BaseNavigationCommand = Pick<
|
|
||||||
QuickBarNavigationItem,
|
|
||||||
"primaryText" | "path"
|
|
||||||
>;
|
|
||||||
@customElement("ha-quick-bar")
|
@customElement("ha-quick-bar")
|
||||||
export class QuickBar extends LitElement {
|
export class QuickBar extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@internalProperty() private _commandItems?: CommandItem[];
|
@internalProperty() private _commandItems?: QuickBarItem[];
|
||||||
|
|
||||||
@internalProperty() private _entityItems?: EntityItem[];
|
@internalProperty() private _entityItems?: QuickBarItem[];
|
||||||
|
|
||||||
@internalProperty() private _items?: QuickBarItem[] = [];
|
@internalProperty() private _items?: QuickBarItem[] = [];
|
||||||
|
|
||||||
@ -214,12 +202,6 @@ export class QuickBar extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _renderItem(item: QuickBarItem, index?: number) {
|
private _renderItem(item: QuickBarItem, index?: number) {
|
||||||
return isCommandItem(item)
|
|
||||||
? this._renderCommandItem(item, index)
|
|
||||||
: this._renderEntityItem(item, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderEntityItem(item: EntityItem, index?: number) {
|
|
||||||
return html`
|
return html`
|
||||||
<mwc-list-item
|
<mwc-list-item
|
||||||
.twoline=${Boolean(item.altText)}
|
.twoline=${Boolean(item.altText)}
|
||||||
@ -232,19 +214,16 @@ export class QuickBar extends LitElement {
|
|||||||
${item.iconPath
|
${item.iconPath
|
||||||
? html`<ha-svg-icon
|
? html`<ha-svg-icon
|
||||||
.path=${item.iconPath}
|
.path=${item.iconPath}
|
||||||
class="entity"
|
|
||||||
slot="graphic"
|
slot="graphic"
|
||||||
></ha-svg-icon>`
|
></ha-svg-icon>`
|
||||||
: html`<ha-icon
|
: html`<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>`}
|
||||||
.icon=${item.icon}
|
${item.decoratedText
|
||||||
class="entity"
|
? this._renderDecoratedText(item.decoratedText)
|
||||||
slot="graphic"
|
: item.text}
|
||||||
></ha-icon>`}
|
|
||||||
<span>${item.primaryText}</span>
|
|
||||||
${item.altText
|
${item.altText
|
||||||
? html`
|
? html`
|
||||||
<span slot="secondary" class="item-text secondary"
|
<span slot="secondary" class="item-text secondary"
|
||||||
>${item.altText}</span
|
>${this._renderDecoratedText(item.altText)}</span
|
||||||
>
|
>
|
||||||
`
|
`
|
||||||
: null}
|
: null}
|
||||||
@ -252,39 +231,18 @@ export class QuickBar extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderCommandItem(item: CommandItem, index?: number) {
|
private _renderDecoratedText(text: string) {
|
||||||
return html`
|
const decoratedText: TemplateResult[] = [];
|
||||||
<mwc-list-item
|
|
||||||
.twoline=${Boolean(item.altText)}
|
|
||||||
.item=${item}
|
|
||||||
index=${ifDefined(index)}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<ha-chip
|
|
||||||
.label="${item.categoryText}"
|
|
||||||
hasIcon
|
|
||||||
class="command-category ${item.categoryKey}"
|
|
||||||
>
|
|
||||||
${item.iconPath
|
|
||||||
? html`<ha-svg-icon
|
|
||||||
.path=${item.iconPath}
|
|
||||||
slot="icon"
|
|
||||||
></ha-svg-icon>`
|
|
||||||
: ""}
|
|
||||||
${item.categoryText}</ha-chip
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span>${item.primaryText}</span>
|
for (let i = 0; i < text.length; i++) {
|
||||||
${item.altText
|
if (text[i] === "^") {
|
||||||
? html`
|
decoratedText.push(html`<b>${text[i + 1]}</b>`);
|
||||||
<span slot="secondary" class="item-text secondary"
|
i++;
|
||||||
>${item.altText}</span
|
} else {
|
||||||
>
|
decoratedText.push(html`${text[i]}`);
|
||||||
`
|
}
|
||||||
: null}
|
}
|
||||||
</mwc-list-item>
|
return decoratedText;
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processItemAndCloseDialog(item: QuickBarItem, index: number) {
|
private async processItemAndCloseDialog(item: QuickBarItem, index: number) {
|
||||||
@ -374,112 +332,96 @@ export class QuickBar extends LitElement {
|
|||||||
|
|
||||||
private _generateEntityItems(): QuickBarItem[] {
|
private _generateEntityItems(): QuickBarItem[] {
|
||||||
return Object.keys(this.hass.states)
|
return Object.keys(this.hass.states)
|
||||||
.map((entityId) => {
|
.map((entityId) => ({
|
||||||
const primaryText = computeStateName(this.hass.states[entityId]);
|
text: computeStateName(this.hass.states[entityId]),
|
||||||
return {
|
altText: entityId,
|
||||||
primaryText,
|
icon: domainIcon(computeDomain(entityId), this.hass.states[entityId]),
|
||||||
filterText: primaryText,
|
action: () => fireEvent(this, "hass-more-info", { entityId }),
|
||||||
altText: entityId,
|
}))
|
||||||
icon: domainIcon(computeDomain(entityId), this.hass.states[entityId]),
|
.sort((a, b) => compare(a.text.toLowerCase(), b.text.toLowerCase()));
|
||||||
action: () => fireEvent(this, "hass-more-info", { entityId }),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.sort((a, b) =>
|
|
||||||
compare(a.primaryText.toLowerCase(), b.primaryText.toLowerCase())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _generateCommandItems(): CommandItem[] {
|
private _generateCommandItems(): QuickBarItem[] {
|
||||||
return [
|
return [
|
||||||
...this._generateReloadCommands(),
|
...this._generateReloadCommands(),
|
||||||
...this._generateServerControlCommands(),
|
...this._generateServerControlCommands(),
|
||||||
...this._generateNavigationCommands(),
|
...this._generateNavigationCommands(),
|
||||||
].sort((a, b) =>
|
]
|
||||||
compare(a.filterText.toLowerCase(), b.filterText.toLowerCase())
|
.sort((a, b) => compare(a.text.toLowerCase(), b.text.toLowerCase()))
|
||||||
);
|
.filter((item) => !item.text.includes("x"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _generateReloadCommands(): CommandItem[] {
|
private _generateReloadCommands(): QuickBarItem[] {
|
||||||
const reloadableDomains = componentsWithService(this.hass, "reload").sort();
|
const reloadableDomains = componentsWithService(this.hass, "reload").sort();
|
||||||
|
|
||||||
return reloadableDomains.map((domain) => {
|
return reloadableDomains.map((domain) => ({
|
||||||
const categoryText = this.hass.localize(
|
text:
|
||||||
`ui.dialogs.quick-bar.commands.types.reload`
|
|
||||||
);
|
|
||||||
const primaryText =
|
|
||||||
this.hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
|
this.hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
|
||||||
this.hass.localize(
|
this.hass.localize(
|
||||||
"ui.dialogs.quick-bar.commands.reload.reload",
|
"ui.dialogs.quick-bar.commands.reload.reload",
|
||||||
"domain",
|
"domain",
|
||||||
domainToName(this.hass.localize, domain)
|
domainToName(this.hass.localize, domain)
|
||||||
);
|
),
|
||||||
|
icon: domainIcon(domain),
|
||||||
return {
|
action: () => this.hass.callService(domain, "reload"),
|
||||||
primaryText,
|
}));
|
||||||
filterText: `${categoryText} ${primaryText}`,
|
|
||||||
action: () => this.hass.callService(domain, "reload"),
|
|
||||||
categoryKey: "reload",
|
|
||||||
iconPath: mdiReload,
|
|
||||||
categoryText,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _generateServerControlCommands(): CommandItem[] {
|
private _generateServerControlCommands(): QuickBarItem[] {
|
||||||
const serverActions = ["restart", "stop"];
|
const serverActions = ["restart", "stop"];
|
||||||
|
|
||||||
return serverActions.map((action) => {
|
return serverActions.map((action) =>
|
||||||
const categoryKey = "server_control";
|
this._generateConfirmationCommand(
|
||||||
const categoryText = this.hass.localize(
|
|
||||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
|
||||||
);
|
|
||||||
const primaryText = this.hass.localize(
|
|
||||||
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
|
||||||
"action",
|
|
||||||
this.hass.localize(
|
|
||||||
`ui.dialogs.quick-bar.commands.server_control.${action}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return this._generateConfirmationCommand(
|
|
||||||
{
|
{
|
||||||
primaryText,
|
text: this.hass.localize(
|
||||||
filterText: `${categoryText} ${primaryText}`,
|
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
||||||
categoryKey,
|
"action",
|
||||||
iconPath: mdiServerNetwork,
|
this.hass.localize(
|
||||||
categoryText,
|
`ui.dialogs.quick-bar.commands.server_control.${action}`
|
||||||
|
)
|
||||||
|
),
|
||||||
|
icon: DEFAULT_SERVER_ICON,
|
||||||
action: () => this.hass.callService("homeassistant", action),
|
action: () => this.hass.callService("homeassistant", action),
|
||||||
},
|
},
|
||||||
this.hass.localize("ui.dialogs.generic.ok")
|
this.hass.localize("ui.dialogs.generic.ok")
|
||||||
);
|
)
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _generateNavigationCommands(): CommandItem[] {
|
private _generateNavigationCommands(): QuickBarItem[] {
|
||||||
const panelItems = this._generateNavigationPanelCommands();
|
const panelItems = this._generateNavigationPanelCommands();
|
||||||
const sectionItems = this._generateNavigationConfigSectionCommands();
|
const sectionItems = this._generateNavigationConfigSectionCommands();
|
||||||
|
|
||||||
return this._finalizeNavigationCommands([...panelItems, ...sectionItems]);
|
return this._withNavigationActions([...panelItems, ...sectionItems]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _generateNavigationPanelCommands(): BaseNavigationCommand[] {
|
private _generateNavigationPanelCommands(): Omit<
|
||||||
|
QuickBarNavigationItem,
|
||||||
|
"action"
|
||||||
|
>[] {
|
||||||
return Object.keys(this.hass.panels)
|
return Object.keys(this.hass.panels)
|
||||||
.filter((panelKey) => panelKey !== "_my_redirect")
|
.filter((panelKey) => panelKey !== "_my_redirect")
|
||||||
.map((panelKey) => {
|
.map((panelKey) => {
|
||||||
const panel = this.hass.panels[panelKey];
|
const panel = this.hass.panels[panelKey];
|
||||||
const translationKey = getPanelNameTranslationKey(panel);
|
const translationKey = getPanelNameTranslationKey(panel);
|
||||||
|
|
||||||
const primaryText =
|
const text = this.hass.localize(
|
||||||
this.hass.localize(translationKey) || panel.title || panel.url_path;
|
"ui.dialogs.quick-bar.commands.navigation.navigate_to",
|
||||||
|
"panel",
|
||||||
|
this.hass.localize(translationKey) || panel.title || panel.url_path
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
primaryText,
|
text,
|
||||||
|
icon: getPanelIcon(panel) || DEFAULT_NAVIGATION_ICON,
|
||||||
path: `/${panel.url_path}`,
|
path: `/${panel.url_path}`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _generateNavigationConfigSectionCommands(): BaseNavigationCommand[] {
|
private _generateNavigationConfigSectionCommands(): Partial<
|
||||||
|
QuickBarNavigationItem
|
||||||
|
>[] {
|
||||||
const items: NavigationInfo[] = [];
|
const items: NavigationInfo[] = [];
|
||||||
|
|
||||||
for (const sectionKey of Object.keys(configSections)) {
|
for (const sectionKey of Object.keys(configSections)) {
|
||||||
@ -503,12 +445,18 @@ export class QuickBar extends LitElement {
|
|||||||
page: PageNavigation
|
page: PageNavigation
|
||||||
): NavigationInfo | undefined {
|
): NavigationInfo | undefined {
|
||||||
if (page.component) {
|
if (page.component) {
|
||||||
const caption = this.hass.localize(
|
const shortCaption = this.hass.localize(
|
||||||
`ui.dialogs.quick-bar.commands.navigation.${page.component}`
|
`ui.dialogs.quick-bar.commands.navigation.${page.component}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (page.translationKey && caption) {
|
if (page.translationKey && shortCaption) {
|
||||||
return { ...page, primaryText: caption };
|
const caption = this.hass.localize(
|
||||||
|
"ui.dialogs.quick-bar.commands.navigation.navigate_to",
|
||||||
|
"panel",
|
||||||
|
shortCaption
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ...page, text: caption };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -516,9 +464,9 @@ export class QuickBar extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _generateConfirmationCommand(
|
private _generateConfirmationCommand(
|
||||||
item: CommandItem,
|
item: QuickBarItem,
|
||||||
confirmText: ConfirmationDialogParams["confirmText"]
|
confirmText: ConfirmationDialogParams["confirmText"]
|
||||||
): CommandItem {
|
): QuickBarItem {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
action: () =>
|
action: () =>
|
||||||
@ -529,24 +477,13 @@ export class QuickBar extends LitElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _finalizeNavigationCommands(
|
private _withNavigationActions(items) {
|
||||||
items: BaseNavigationCommand[]
|
return items.map(({ text, icon, iconPath, path }) => ({
|
||||||
): CommandItem[] {
|
text,
|
||||||
return items.map((item) => {
|
icon,
|
||||||
const categoryKey = "navigation";
|
iconPath,
|
||||||
const categoryText = this.hass.localize(
|
action: () => navigate(this, path),
|
||||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
categoryKey,
|
|
||||||
iconPath: mdiEarth,
|
|
||||||
categoryText,
|
|
||||||
filterText: `${categoryText} ${item.primaryText}`,
|
|
||||||
action: () => navigate(this, item.path),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _toggleIfAlreadyOpened() {
|
private _toggleIfAlreadyOpened() {
|
||||||
@ -560,10 +497,10 @@ export class QuickBar extends LitElement {
|
|||||||
: items;
|
: items;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _filterItems = memoizeOne(
|
private _filterItems = (
|
||||||
(items: QuickBarItem[], filter: string): QuickBarItem[] =>
|
items: QuickBarItem[],
|
||||||
fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
|
filter: string
|
||||||
);
|
): QuickBarItem[] => fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items);
|
||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return [
|
return [
|
||||||
@ -588,8 +525,8 @@ export class QuickBar extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-icon.entity,
|
ha-icon,
|
||||||
ha-svg-icon.entity {
|
ha-svg-icon {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -598,29 +535,6 @@ export class QuickBar extends LitElement {
|
|||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
span.command-category {
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 3px;
|
|
||||||
display: inline-flex;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.command-category.reload {
|
|
||||||
--ha-chip-background-color: #cddc39;
|
|
||||||
--ha-chip-text-color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.command-category.navigation {
|
|
||||||
--ha-chip-background-color: var(--light-primary-color);
|
|
||||||
--ha-chip-text-color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.command-category.server_control {
|
|
||||||
--ha-chip-background-color: var(--warning-color);
|
|
||||||
--ha-chip-text-color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uni-virtualizer-host {
|
.uni-virtualizer-host {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -636,10 +550,6 @@ export class QuickBar extends LitElement {
|
|||||||
mwc-list-item {
|
mwc-list-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
mwc-list-item.command-item {
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import { assert } from "chai";
|
import { assert } from "chai";
|
||||||
|
import {
|
||||||
|
createMatches,
|
||||||
|
fuzzyScore,
|
||||||
|
FuzzyScorer,
|
||||||
|
} from "../../../src/common/string/filter/filter";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fuzzyFilterSort,
|
fuzzyFilterSort,
|
||||||
@ -80,14 +85,10 @@ describe("fuzzySequentialMatch", () => {
|
|||||||
|
|
||||||
describe("fuzzyFilterSort", () => {
|
describe("fuzzyFilterSort", () => {
|
||||||
const filter = "ticker";
|
const filter = "ticker";
|
||||||
const item1 = {
|
const item1 = { text: "automation.ticker", altText: "Stocks", score: 0 };
|
||||||
filterText: "automation.ticker",
|
const item2 = { text: "sensor.ticker", altText: "Stocks up", score: 0 };
|
||||||
altText: "Stocks",
|
|
||||||
score: 0,
|
|
||||||
};
|
|
||||||
const item2 = { filterText: "sensor.ticker", altText: "Stocks up", score: 0 };
|
|
||||||
const item3 = {
|
const item3 = {
|
||||||
filterText: "automation.check_router",
|
text: "automation.check_router",
|
||||||
altText: "Timer Check Router",
|
altText: "Timer Check Router",
|
||||||
score: 0,
|
score: 0,
|
||||||
};
|
};
|
||||||
@ -105,3 +106,45 @@ describe("fuzzyFilterSort", () => {
|
|||||||
assert.deepEqual(res, expectedItemsAfterFilter);
|
assert.deepEqual(res, expectedItemsAfterFilter);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("createMatches", () => {
|
||||||
|
it(`sorts correctly`, () => {
|
||||||
|
assertMatches("tit", "win.tit", "win.^t^i^t", fuzzyScore);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function assertMatches(
|
||||||
|
pattern: string,
|
||||||
|
word: string,
|
||||||
|
decoratedWord: string | undefined,
|
||||||
|
filter: FuzzyScorer,
|
||||||
|
opts: {
|
||||||
|
patternPos?: number;
|
||||||
|
wordPos?: number;
|
||||||
|
firstMatchCanBeWeak?: boolean;
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
const r = filter(
|
||||||
|
pattern,
|
||||||
|
pattern.toLowerCase(),
|
||||||
|
opts.patternPos || 0,
|
||||||
|
word,
|
||||||
|
word.toLowerCase(),
|
||||||
|
opts.wordPos || 0,
|
||||||
|
opts.firstMatchCanBeWeak || false
|
||||||
|
);
|
||||||
|
assert.ok(!decoratedWord === !r);
|
||||||
|
if (r) {
|
||||||
|
const matches = createMatches(r);
|
||||||
|
let actualWord = "";
|
||||||
|
let pos = 0;
|
||||||
|
for (const match of matches) {
|
||||||
|
actualWord += word.substring(pos, match.start);
|
||||||
|
actualWord +=
|
||||||
|
"^" + word.substring(match.start, match.end).split("").join("^");
|
||||||
|
pos = match.end;
|
||||||
|
}
|
||||||
|
actualWord += word.substring(pos);
|
||||||
|
assert.equal(actualWord, decoratedWord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user