Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
35 changes: 35 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.
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,27 @@ function isEmojiModifier(codePoint: number): boolean {
return 0x1F3FB <= codePoint && codePoint <= 0x1F3FF;
}

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

/**
* Returns whether the regional indicator ending at `offset` (already counted
* by the caller) is paired, i.e. whether the full contiguous run of regional
* indicators it belongs to has even length.
*/
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