mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-23 14:42:03 +03:00
chore: upgrade cmdk to 1.0.0 (#6401)
Also include the command score into our own repo for some tweaks. Might fix https://github.com/toeverything/AFFiNE/issues/6322
This commit is contained in:
parent
822bbb54a4
commit
f41d587d65
File diff suppressed because one or more lines are too long
@ -51,7 +51,7 @@
|
||||
"async-call-rpc": "^6.4.0",
|
||||
"bytes": "^3.1.2",
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "patch:cmdk@npm%3A0.2.0#~/.yarn/patches/cmdk-npm-0.2.0-302237a911.patch",
|
||||
"cmdk": "^1.0.0",
|
||||
"css-spring": "^4.1.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"foxact": "^0.2.31",
|
||||
|
@ -0,0 +1,99 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { commandScore } from '../command-score';
|
||||
|
||||
describe('commandScore', function () {
|
||||
it('should match exact strings exactly', function () {
|
||||
expect(commandScore('hello', 'hello')).to.equal(1);
|
||||
});
|
||||
|
||||
it('should prefer case-sensitive matches', function () {
|
||||
expect(commandScore('Hello', 'Hello')).to.be.greaterThan(
|
||||
commandScore('Hello', 'hello')
|
||||
);
|
||||
});
|
||||
|
||||
it('should mark down prefixes', function () {
|
||||
expect(commandScore('hello', 'hello')).to.be.greaterThan(
|
||||
commandScore('hello', 'he')
|
||||
);
|
||||
});
|
||||
|
||||
it('should score all prefixes the same', function () {
|
||||
expect(commandScore('help', 'he')).to.equal(commandScore('hello', 'he'));
|
||||
});
|
||||
|
||||
it('should mark down word jumps', function () {
|
||||
expect(commandScore('hello world', 'hello')).to.be.greaterThan(
|
||||
commandScore('hello world', 'hewo')
|
||||
);
|
||||
});
|
||||
|
||||
it('should score similar word jumps the same', function () {
|
||||
expect(commandScore('hello world', 'hewo')).to.equal(
|
||||
commandScore('hey world', 'hewo')
|
||||
);
|
||||
});
|
||||
|
||||
it('should penalize long word jumps', function () {
|
||||
expect(commandScore('hello world', 'hewo')).to.be.greaterThan(
|
||||
commandScore('hello kind world', 'hewo')
|
||||
);
|
||||
});
|
||||
|
||||
it('should match missing characters', function () {
|
||||
expect(commandScore('hello', 'hl')).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it('should penalize more for more missing characters', function () {
|
||||
expect(commandScore('hello', 'hllo')).to.be.greaterThan(
|
||||
commandScore('hello', 'hlo')
|
||||
);
|
||||
});
|
||||
|
||||
it('should penalize more for missing characters than case', function () {
|
||||
expect(commandScore('go to Inbox', 'in')).to.be.greaterThan(
|
||||
commandScore('go to Unversity/Societies/CUE/info@cue.org.uk', 'in')
|
||||
);
|
||||
});
|
||||
|
||||
it('should match transpotisions', function () {
|
||||
expect(commandScore('hello', 'hle')).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it('should not match with a trailing letter', function () {
|
||||
expect(commandScore('ss', 'sss')).to.equal(0.1);
|
||||
});
|
||||
|
||||
it('should match long jumps', function () {
|
||||
expect(commandScore('go to @QuickFix', 'fix')).to.be.greaterThan(0);
|
||||
expect(commandScore('go to Quick Fix', 'fix')).to.be.greaterThan(
|
||||
commandScore('go to @QuickFix', 'fix')
|
||||
);
|
||||
});
|
||||
|
||||
it('should work well with the presence of an m-dash', function () {
|
||||
expect(commandScore('no go — Windows', 'windows')).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it('should be robust to duplicated letters', function () {
|
||||
expect(commandScore('talent', 'tall')).to.be.equal(0.099);
|
||||
});
|
||||
|
||||
it('should not allow letter insertion', function () {
|
||||
expect(commandScore('talent', 'tadlent')).to.be.equal(0);
|
||||
});
|
||||
|
||||
it('should match - with " " characters', function () {
|
||||
expect(commandScore('Auto-Advance', 'Auto Advance')).to.be.equal(0.9999);
|
||||
});
|
||||
|
||||
it('should score long strings quickly', function () {
|
||||
expect(
|
||||
commandScore(
|
||||
'go to this is a really long label that is really longthis is a really long label that is really longthis is a really long label that is really longthis is a really long label that is really long',
|
||||
'this is a'
|
||||
)
|
||||
).to.be.equal(0.891);
|
||||
});
|
||||
});
|
195
packages/frontend/core/src/components/pure/cmdk/command-score.ts
Normal file
195
packages/frontend/core/src/components/pure/cmdk/command-score.ts
Normal file
@ -0,0 +1,195 @@
|
||||
// The scores are arranged so that a continuous match of characters will
|
||||
// result in a total score of 1.
|
||||
//
|
||||
// The best case, this character is a match, and either this is the start
|
||||
// of the string, or the previous character was also a match.
|
||||
const SCORE_CONTINUE_MATCH = 1,
|
||||
// A new match at the start of a word scores better than a new match
|
||||
// elsewhere as it's more likely that the user will type the starts
|
||||
// of fragments.
|
||||
// NOTE: We score word jumps between spaces slightly higher than slashes, brackets
|
||||
// hyphens, etc.
|
||||
SCORE_SPACE_WORD_JUMP = 0.9,
|
||||
SCORE_NON_SPACE_WORD_JUMP = 0.8,
|
||||
// Any other match isn't ideal, but we include it for completeness.
|
||||
SCORE_CHARACTER_JUMP = 0.17,
|
||||
// If the user transposed two letters, it should be significantly penalized.
|
||||
//
|
||||
// i.e. "ouch" is more likely than "curtain" when "uc" is typed.
|
||||
SCORE_TRANSPOSITION = 0.1,
|
||||
// The goodness of a match should decay slightly with each missing
|
||||
// character.
|
||||
//
|
||||
// i.e. "bad" is more likely than "bard" when "bd" is typed.
|
||||
//
|
||||
// This will not change the order of suggestions based on SCORE_* until
|
||||
// 100 characters are inserted between matches.
|
||||
PENALTY_SKIPPED = 0.999,
|
||||
// The goodness of an exact-case match should be higher than a
|
||||
// case-insensitive match by a small amount.
|
||||
//
|
||||
// i.e. "HTML" is more likely than "haml" when "HM" is typed.
|
||||
//
|
||||
// This will not change the order of suggestions based on SCORE_* until
|
||||
// 1000 characters are inserted between matches.
|
||||
PENALTY_CASE_MISMATCH = 0.9999,
|
||||
// If the word has more characters than the user typed, it should
|
||||
// be penalised slightly.
|
||||
//
|
||||
// i.e. "html" is more likely than "html5" if I type "html".
|
||||
//
|
||||
// However, it may well be the case that there's a sensible secondary
|
||||
// ordering (like alphabetical) that it makes sense to rely on when
|
||||
// there are many prefix matches, so we don't make the penalty increase
|
||||
// with the number of tokens.
|
||||
PENALTY_NOT_COMPLETE = 0.99;
|
||||
|
||||
const IS_GAP_REGEXP = /[\\/_+.#"@[({&]/,
|
||||
COUNT_GAPS_REGEXP = /[\\/_+.#"@[({&]/g,
|
||||
IS_SPACE_REGEXP = /[\s-]/,
|
||||
COUNT_SPACE_REGEXP = /[\s-]/g;
|
||||
|
||||
const MAX_RECUR = 1500;
|
||||
|
||||
function commandScoreInner(
|
||||
string: string,
|
||||
abbreviation: string,
|
||||
lowerString: string,
|
||||
lowerAbbreviation: string,
|
||||
stringIndex: number,
|
||||
abbreviationIndex: number,
|
||||
memoizedResults: Record<string, number>,
|
||||
recur: number = 0
|
||||
) {
|
||||
recur += 1;
|
||||
if (abbreviationIndex === abbreviation.length) {
|
||||
if (stringIndex === string.length) {
|
||||
return SCORE_CONTINUE_MATCH;
|
||||
}
|
||||
return PENALTY_NOT_COMPLETE;
|
||||
}
|
||||
|
||||
const memoizeKey = `${stringIndex},${abbreviationIndex}`;
|
||||
if (memoizedResults[memoizeKey] !== undefined) {
|
||||
return memoizedResults[memoizeKey];
|
||||
}
|
||||
|
||||
const abbreviationChar = lowerAbbreviation.charAt(abbreviationIndex);
|
||||
let index = lowerString.indexOf(abbreviationChar, stringIndex);
|
||||
let highScore = 0;
|
||||
|
||||
let score, transposedScore, wordBreaks, spaceBreaks;
|
||||
|
||||
while (index >= 0) {
|
||||
score = commandScoreInner(
|
||||
string,
|
||||
abbreviation,
|
||||
lowerString,
|
||||
lowerAbbreviation,
|
||||
index + 1,
|
||||
abbreviationIndex + 1,
|
||||
memoizedResults,
|
||||
recur
|
||||
);
|
||||
if (score > highScore) {
|
||||
if (index === stringIndex) {
|
||||
score *= SCORE_CONTINUE_MATCH;
|
||||
} else if (IS_GAP_REGEXP.test(string.charAt(index - 1))) {
|
||||
score *= SCORE_NON_SPACE_WORD_JUMP;
|
||||
wordBreaks = string
|
||||
.slice(stringIndex, index - 1)
|
||||
.match(COUNT_GAPS_REGEXP);
|
||||
if (wordBreaks && stringIndex > 0) {
|
||||
score *= Math.pow(PENALTY_SKIPPED, wordBreaks.length);
|
||||
}
|
||||
} else if (IS_SPACE_REGEXP.test(string.charAt(index - 1))) {
|
||||
score *= SCORE_SPACE_WORD_JUMP;
|
||||
spaceBreaks = string
|
||||
.slice(stringIndex, index - 1)
|
||||
.match(COUNT_SPACE_REGEXP);
|
||||
if (spaceBreaks && stringIndex > 0) {
|
||||
score *= Math.pow(PENALTY_SKIPPED, spaceBreaks.length);
|
||||
}
|
||||
} else {
|
||||
score *= SCORE_CHARACTER_JUMP;
|
||||
if (stringIndex > 0) {
|
||||
score *= Math.pow(PENALTY_SKIPPED, index - stringIndex);
|
||||
}
|
||||
}
|
||||
|
||||
if (string.charAt(index) !== abbreviation.charAt(abbreviationIndex)) {
|
||||
score *= PENALTY_CASE_MISMATCH;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(score < SCORE_TRANSPOSITION &&
|
||||
lowerString.charAt(index - 1) ===
|
||||
lowerAbbreviation.charAt(abbreviationIndex + 1)) ||
|
||||
(lowerAbbreviation.charAt(abbreviationIndex + 1) ===
|
||||
lowerAbbreviation.charAt(abbreviationIndex) && // allow duplicate letters. Ref #7428
|
||||
lowerString.charAt(index - 1) !==
|
||||
lowerAbbreviation.charAt(abbreviationIndex))
|
||||
) {
|
||||
transposedScore = commandScoreInner(
|
||||
string,
|
||||
abbreviation,
|
||||
lowerString,
|
||||
lowerAbbreviation,
|
||||
index + 1,
|
||||
abbreviationIndex + 2,
|
||||
memoizedResults,
|
||||
recur
|
||||
);
|
||||
|
||||
if (transposedScore * SCORE_TRANSPOSITION > score) {
|
||||
score = transposedScore * SCORE_TRANSPOSITION;
|
||||
}
|
||||
}
|
||||
|
||||
if (score > highScore) {
|
||||
highScore = score;
|
||||
}
|
||||
|
||||
index = lowerString.indexOf(abbreviationChar, index + 1);
|
||||
|
||||
if (recur > MAX_RECUR || score > 0.85) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
memoizedResults[memoizeKey] = highScore;
|
||||
return highScore;
|
||||
}
|
||||
|
||||
function formatInput(string: string) {
|
||||
// convert all valid space characters to space so they match each other
|
||||
return string.toLowerCase().replace(COUNT_SPACE_REGEXP, ' ');
|
||||
}
|
||||
|
||||
export function commandScore(
|
||||
string: string,
|
||||
abbreviation: string,
|
||||
aliases?: string[]
|
||||
): number {
|
||||
/* NOTE:
|
||||
* in the original, we used to do the lower-casing on each recursive call, but this meant that toLowerCase()
|
||||
* was the dominating cost in the algorithm, passing both is a little ugly, but considerably faster.
|
||||
*/
|
||||
string =
|
||||
aliases && aliases.length > 0
|
||||
? `${string + ' ' + aliases.join(' ')}`
|
||||
: string;
|
||||
const memoizedResults = {};
|
||||
const result = commandScoreInner(
|
||||
string,
|
||||
abbreviation,
|
||||
formatInput(string),
|
||||
formatInput(abbreviation),
|
||||
0,
|
||||
0,
|
||||
memoizedResults
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import type { CommandCategory } from '@toeverything/infra';
|
||||
import { commandScore } from 'cmdk';
|
||||
import { groupBy } from 'lodash-es';
|
||||
|
||||
import { commandScore } from './command-score';
|
||||
import type { CMDKCommand } from './types';
|
||||
import { highlightTextFragments } from './use-highlight';
|
||||
|
||||
|
@ -26,7 +26,7 @@ const insertInputText = async (page: Page, text: string) => {
|
||||
const keyboardDownAndSelect = async (page: Page, label: string) => {
|
||||
await page.keyboard.press('ArrowDown');
|
||||
const selectedEl = page.locator(
|
||||
'[cmdk-item][data-selected] [data-testid="cmdk-label"]'
|
||||
'[cmdk-item][data-selected="true"] [data-testid="cmdk-label"]'
|
||||
);
|
||||
if (
|
||||
!(await selectedEl.isVisible()) ||
|
||||
|
34
yarn.lock
34
yarn.lock
@ -371,7 +371,7 @@ __metadata:
|
||||
async-call-rpc: "npm:^6.4.0"
|
||||
bytes: "npm:^3.1.2"
|
||||
clsx: "npm:^2.1.0"
|
||||
cmdk: "patch:cmdk@npm%3A0.2.0#~/.yarn/patches/cmdk-npm-0.2.0-302237a911.patch"
|
||||
cmdk: "npm:^1.0.0"
|
||||
css-spring: "npm:^4.1.0"
|
||||
dayjs: "npm:^1.11.10"
|
||||
express: "npm:^4.18.2"
|
||||
@ -18054,29 +18054,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cmdk@npm:0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "cmdk@npm:0.2.0"
|
||||
"cmdk@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "cmdk@npm:1.0.0"
|
||||
dependencies:
|
||||
"@radix-ui/react-dialog": "npm:1.0.0"
|
||||
command-score: "npm:0.1.2"
|
||||
"@radix-ui/react-dialog": "npm:1.0.5"
|
||||
"@radix-ui/react-primitive": "npm:1.0.3"
|
||||
peerDependencies:
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
checksum: 10/e178e3d3276e0b5fd158c9c99716c0405427871f48fa97c15c4be2de24be4a478cf0205ffa04244628dbe103dd8573a1bd1aa68f04f8b60633d4ffc04e5eee62
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cmdk@patch:cmdk@npm%3A0.2.0#~/.yarn/patches/cmdk-npm-0.2.0-302237a911.patch":
|
||||
version: 0.2.0
|
||||
resolution: "cmdk@patch:cmdk@npm%3A0.2.0#~/.yarn/patches/cmdk-npm-0.2.0-302237a911.patch::version=0.2.0&hash=640d85"
|
||||
dependencies:
|
||||
"@radix-ui/react-dialog": "npm:1.0.0"
|
||||
command-score: "npm:0.1.2"
|
||||
peerDependencies:
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
checksum: 10/758bacb7761a72c6fa03a1b20ea2514ff14ad6b3d00cc1d8bc6781a216b0a719f991eacded9f923ddcf1b58b8efb304209b268c17bd7d6f5671aa3352934b754
|
||||
checksum: 10/7a0675783d9b12828c30b044993d1ecf0e9230984c04f7a1714025804d34294b2b0f8958f30b26fe3b5be276b3cd874dbe1d0bc27cd25d15daa06adfcd3feb85
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -18218,13 +18205,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"command-score@npm:0.1.2":
|
||||
version: 0.1.2
|
||||
resolution: "command-score@npm:0.1.2"
|
||||
checksum: 10/84f6a69e6b215d3fc8c9ed402d109587f511e4cc84cd5da10a7857b50fb1638953e32dcce8ed8f3549b0bfe499e82601fb7fb6891c9c71b48933d4bb8bac238a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"commander@npm:11.1.0":
|
||||
version: 11.1.0
|
||||
resolution: "commander@npm:11.1.0"
|
||||
|
Loading…
Reference in New Issue
Block a user