mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 14:56:59 +08: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:
File diff suppressed because one or more lines are too long
@@ -51,7 +51,7 @@
|
|||||||
"async-call-rpc": "^6.4.0",
|
"async-call-rpc": "^6.4.0",
|
||||||
"bytes": "^3.1.2",
|
"bytes": "^3.1.2",
|
||||||
"clsx": "^2.1.0",
|
"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",
|
"css-spring": "^4.1.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"foxact": "^0.2.31",
|
"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 type { CommandCategory } from '@toeverything/infra';
|
||||||
import { commandScore } from 'cmdk';
|
|
||||||
import { groupBy } from 'lodash-es';
|
import { groupBy } from 'lodash-es';
|
||||||
|
|
||||||
|
import { commandScore } from './command-score';
|
||||||
import type { CMDKCommand } from './types';
|
import type { CMDKCommand } from './types';
|
||||||
import { highlightTextFragments } from './use-highlight';
|
import { highlightTextFragments } from './use-highlight';
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const insertInputText = async (page: Page, text: string) => {
|
|||||||
const keyboardDownAndSelect = async (page: Page, label: string) => {
|
const keyboardDownAndSelect = async (page: Page, label: string) => {
|
||||||
await page.keyboard.press('ArrowDown');
|
await page.keyboard.press('ArrowDown');
|
||||||
const selectedEl = page.locator(
|
const selectedEl = page.locator(
|
||||||
'[cmdk-item][data-selected] [data-testid="cmdk-label"]'
|
'[cmdk-item][data-selected="true"] [data-testid="cmdk-label"]'
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
!(await selectedEl.isVisible()) ||
|
!(await selectedEl.isVisible()) ||
|
||||||
|
|||||||
34
yarn.lock
34
yarn.lock
@@ -371,7 +371,7 @@ __metadata:
|
|||||||
async-call-rpc: "npm:^6.4.0"
|
async-call-rpc: "npm:^6.4.0"
|
||||||
bytes: "npm:^3.1.2"
|
bytes: "npm:^3.1.2"
|
||||||
clsx: "npm:^2.1.0"
|
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"
|
css-spring: "npm:^4.1.0"
|
||||||
dayjs: "npm:^1.11.10"
|
dayjs: "npm:^1.11.10"
|
||||||
express: "npm:^4.18.2"
|
express: "npm:^4.18.2"
|
||||||
@@ -18054,29 +18054,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"cmdk@npm:0.2.0":
|
"cmdk@npm:^1.0.0":
|
||||||
version: 0.2.0
|
version: 1.0.0
|
||||||
resolution: "cmdk@npm:0.2.0"
|
resolution: "cmdk@npm:1.0.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/react-dialog": "npm:1.0.0"
|
"@radix-ui/react-dialog": "npm:1.0.5"
|
||||||
command-score: "npm:0.1.2"
|
"@radix-ui/react-primitive": "npm:1.0.3"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0.0
|
react: ^18.0.0
|
||||||
react-dom: ^18.0.0
|
react-dom: ^18.0.0
|
||||||
checksum: 10/e178e3d3276e0b5fd158c9c99716c0405427871f48fa97c15c4be2de24be4a478cf0205ffa04244628dbe103dd8573a1bd1aa68f04f8b60633d4ffc04e5eee62
|
checksum: 10/7a0675783d9b12828c30b044993d1ecf0e9230984c04f7a1714025804d34294b2b0f8958f30b26fe3b5be276b3cd874dbe1d0bc27cd25d15daa06adfcd3feb85
|
||||||
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
|
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -18218,13 +18205,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"commander@npm:11.1.0":
|
||||||
version: 11.1.0
|
version: 11.1.0
|
||||||
resolution: "commander@npm:11.1.0"
|
resolution: "commander@npm:11.1.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user