Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/vs/base/common/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1205,6 +1205,20 @@ function getOffsetBeforeLastEmojiComponent(initialOffset: number, str: string):

let resultOffset = iterator.offset;

if (isRegionalIndicator(codePoint)) {
// Regional indicators combine in pairs to form a single flag emoji
// (Unicode UAX #29 GB12/GB13: do not break within an emoji flag
// sequence). If this regional indicator completes such a pair,
// delete both code points together; otherwise it is a lone,
// unpaired indicator and only it should be deleted.
Comment on lines +1209 to +1213
if (isRegionalIndicatorPaired(str, resultOffset)) {
const pairIterator = new CodePointIterator(str, resultOffset);
pairIterator.prevCodePoint();
resultOffset = pairIterator.offset;
}
return resultOffset;
}

if (resultOffset > 0) {
// Skip optional ZWJ code points that combine multiple emojis.
// In theory, we should check if that ZWJ actually combines multiple emojis
Expand All @@ -1222,6 +1236,30 @@ function isEmojiModifier(codePoint: number): boolean {
return 0x1F3FB <= codePoint && codePoint <= 0x1F3FF;
}

function isRegionalIndicator(codePoint: number): boolean {
return 0x1F1E6 <= codePoint && codePoint <= 0x1F1FF;
}

/**
* Determines whether the regional indicator immediately preceding `offset`
* (already accounted for by the caller) is paired with the one before it.
*
* Regional indicators combine greedily in pairs from the start of a maximal
* run of consecutive regional indicators, so the *last* indicator of a run
* is paired if and only if the run's length is even.
*/
function isRegionalIndicatorPaired(str: string, offset: number): boolean {
let runLength = 1;
const scan = new CodePointIterator(str, offset);
while (scan.offset > 0) {
if (!isRegionalIndicator(scan.prevCodePoint())) {
break;
}
runLength++;
}
return runLength % 2 === 0;
}

const enum CodePoint {
zwj = 0x200D,

Expand Down
45 changes: 45 additions & 0 deletions src/vs/base/test/common/strings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,51 @@ suite('Strings', () => {
assert.ok(strings.multibyteAwareBtoa(new Array(100000).fill('vs').join('')).length > 0); // https://github.com/microsoft/vscode/issues/112013
});

suite('getLeftDeleteOffset', () => {
function backspaceSteps(str: string): string[] {
const steps: string[] = [];
let s = str;
let offset = s.length;
while (offset > 0) {
const newOffset = strings.getLeftDeleteOffset(offset, s);
s = s.substring(0, newOffset) + s.substring(offset);
offset = newOffset;
steps.push(s);
}
return steps;
}

test('deletes an emoji modifier sequence as a single unit', () => {
// Skin tone modifier: baby + Fitzpatrick type-5 modifier.
assert.deepStrictEqual(backspaceSteps('\u{1F476}\u{1F3FE}'), ['']);
});

test('deletes a ZWJ emoji sequence component by component', () => {
// Family emoji: man ZWJ man ZWJ girl ZWJ girl.
assert.deepStrictEqual(
backspaceSteps('\u{1F468}\u{200D}\u{1F468}\u{200D}\u{1F467}\u{200D}\u{1F467}'),
['\u{1F468}\u{200D}\u{1F468}\u{200D}\u{1F467}', '\u{1F468}\u{200D}\u{1F468}', '\u{1F468}', '']
);
});

test('issue: deletes a complete flag emoji as a single unit, without leaving a dangling regional indicator', () => {
// Flag emoji are two regional indicator code points (UAX #29 GB12/GB13); they must not be split.
assert.deepStrictEqual(backspaceSteps('\u{1F1FA}\u{1F1F8}'), ['']); // πŸ‡ΊπŸ‡Έ
});

test('issue: deletes adjacent flag emoji one flag at a time, not one indicator at a time', () => {
assert.deepStrictEqual(backspaceSteps('\u{1F1FA}\u{1F1F8}\u{1F1EC}\u{1F1E7}'), ['\u{1F1FA}\u{1F1F8}', '']); // πŸ‡ΊπŸ‡ΈπŸ‡¬πŸ‡§
});

test('deletes a trailing unpaired regional indicator alone, then the completed flag as a unit', () => {
assert.deepStrictEqual(backspaceSteps('\u{1F1FA}\u{1F1F8}\u{1F1EC}'), ['\u{1F1FA}\u{1F1F8}', '']); // πŸ‡ΊπŸ‡ΈπŸ‡¬
});

test('leaves non-emoji text untouched (single code point per backspace)', () => {
assert.deepStrictEqual(backspaceSteps('ab'), ['a', '']);
});
});

ensureNoDisposablesAreLeakedInTestSuite();
});

Expand Down