Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Fixed handling surrogare characters in insert, replace, delete mode in Vim #4934

Merged
merged 2 commits into from Sep 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 34 additions & 8 deletions src/keyboard/vim.js
Expand Up @@ -923,6 +923,20 @@ domLib.importCssString(`.normal-mode .ace_cursor{
return range.head;
}

function updateSelectionForSurrogateCharacters(cm, curStart, curEnd) {
// start and character position when no selection
// is the same in visual mode, and differs in 1 character in normal mode
if (curStart.line === curEnd.line && curStart.ch >= curEnd.ch - 1) {
var text = cm.getLine(curStart.line);
var charCode = text.charCodeAt(curStart.ch);
if (0xD800 <= charCode && charCode <= 0xD8FF) {
curEnd.ch += 1;
}
}

return {start: curStart, end: curEnd};
}

var defaultKeymap = [
// Key to key mapping. This goes first to make it possible to override
// existing mappings.
Expand Down Expand Up @@ -2540,9 +2554,10 @@ domLib.importCssString(`.normal-mode .ace_cursor{
mode = vim.visualBlock ? 'block' :
linewise ? 'line' :
'char';
var newPositions = updateSelectionForSurrogateCharacters(cm, curStart, curEnd);
cmSel = makeCmSelection(cm, {
anchor: curStart,
head: curEnd
anchor: newPositions.start,
head: newPositions.end
}, mode);
if (linewise) {
var ranges = cmSel.ranges;
Expand Down Expand Up @@ -2574,9 +2589,10 @@ domLib.importCssString(`.normal-mode .ace_cursor{
}
mode = 'char';
var exclusive = !motionArgs.inclusive || linewise;
var newPositions = updateSelectionForSurrogateCharacters(cm, curStart, curEnd);
cmSel = makeCmSelection(cm, {
anchor: curStart,
head: curEnd
anchor: newPositions.start,
head: newPositions.end
}, mode, exclusive);
}
cm.setSelections(cmSel.ranges, cmSel.primary);
Expand Down Expand Up @@ -3374,9 +3390,11 @@ domLib.importCssString(`.normal-mode .ace_cursor{
} else if (insertAt == 'bol') {
head = new Pos(head.line, 0);
} else if (insertAt == 'charAfter') {
head = offsetCursor(head, 0, 1);
var newPosition = updateSelectionForSurrogateCharacters(cm, head, offsetCursor(head, 0, 1));
head = newPosition.end;
} else if (insertAt == 'firstNonBlank') {
head = motions.moveToFirstNonWhiteSpaceCharacter(cm, head);
var newPosition = updateSelectionForSurrogateCharacters(cm, head, motions.moveToFirstNonWhiteSpaceCharacter(cm, head));
head = newPosition.end;
} else if (insertAt == 'startOfSelectedArea') {
if (!vim.visualMode)
return;
Expand Down Expand Up @@ -3449,9 +3467,10 @@ domLib.importCssString(`.normal-mode .ace_cursor{
vim.visualBlock = !!actionArgs.blockwise;
head = clipCursorToContent(
cm, new Pos(anchor.line, anchor.ch + repeat - 1));
var newPosition = updateSelectionForSurrogateCharacters(cm, anchor, head)
vim.sel = {
anchor: anchor,
head: head
anchor: newPosition.start,
head: newPosition.end
};
CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""});
updateCmSelection(cm);
Expand Down Expand Up @@ -3749,18 +3768,25 @@ domLib.importCssString(`.normal-mode .ace_cursor{
}
curEnd = new Pos(curStart.line, replaceTo);
}

var newPositions = updateSelectionForSurrogateCharacters(cm, curStart, curEnd);
curStart = newPositions.start;
curEnd = newPositions.end;
if (replaceWith=='\n') {
if (!vim.visualMode) cm.replaceRange('', curStart, curEnd);
// special case, where vim help says to replace by just one line-break
(CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm);
} else {
var replaceWithStr = cm.getRange(curStart, curEnd);
// replace all surrogate characters with selected character
replaceWithStr = replaceWithStr.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, replaceWith);
//replace all characters in range by selected, but keep linebreaks
replaceWithStr = replaceWithStr.replace(/[^\n]/g, replaceWith);
if (vim.visualBlock) {
// Tabs are split in visua block before replacing
var spaces = new Array(cm.getOption("tabSize")+1).join(' ');
replaceWithStr = cm.getSelection();
replaceWithStr = replaceWithStr.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, replaceWith);
replaceWithStr = replaceWithStr.replace(/\t/g, spaces).replace(/[^\n]/g, replaceWith).split('\n');
cm.replaceSelections(replaceWithStr);
} else {
Expand Down
43 changes: 43 additions & 0 deletions src/keyboard/vim_test.js
Expand Up @@ -1850,6 +1850,13 @@ testVim('a_eol', function(cm, vim, helpers) {
helpers.assertCursorAt(0, lines[0].length);
eq('vim-insert', cm.getOption('keyMap'));
});
testVim('a with surrogate characters', function(cm, vim, helpers) {
cm.setCursor(0, 0);
helpers.doKeys('a');
helpers.doKeys('test');
helpers.doKeys('<Esc>');
eq('😀test', cm.getValue());
}, {value: '😀'});
testVim('A_endOfSelectedArea', function(cm, vim, helpers) {
cm.setCursor(0, 0);
helpers.doKeys('v', 'j', 'l');
Expand All @@ -1863,6 +1870,13 @@ testVim('i', function(cm, vim, helpers) {
helpers.assertCursorAt(0, 1);
eq('vim-insert', cm.getOption('keyMap'));
});
testVim('i with surrogate characters', function(cm, vim, helpers) {
cm.setCursor(0, 0);
helpers.doKeys('i');
helpers.doKeys('test');
helpers.doKeys('<Esc>');
eq('test😀', cm.getValue());
}, { value: '😀' });
testVim('i_repeat', function(cm, vim, helpers) {
helpers.doKeys('3', 'i');
helpers.doKeys('test')
Expand Down Expand Up @@ -2111,6 +2125,11 @@ testVim('r', function(cm, vim, helpers) {
helpers.doKeys('r', '<CR>');
eq('\nx', cm.getValue());
}, { value: 'wordet\nanother' });
testVim('r with surrogate characters', function(cm, vim, helpers) {
cm.setCursor(0, 0);
helpers.doKeys('r', 'u');
eq('u', cm.getValue());
}, { value: '😀' });
testVim('r_visual_block', function(cm, vim, helpers) {
cm.ace.setOptions({tabSize: 4, useSoftTabs: false}); // ace_patch TODO
cm.setCursor(2, 3);
Expand All @@ -2125,6 +2144,16 @@ testVim('r_visual_block', function(cm, vim, helpers) {
helpers.doKeys('<C-v>', 'h', 'h', 'r', 'r');
eq('1 l\n5 l\nalllefg\nrrrrrrrr', cm.getValue());
}, {value: '1234\n5678\nabcdefg', indentWithTabs: true});
testVim('r_visual with surrogate characters', function(cm, vim, helpers) {
cm.setCursor(0, 0);
helpers.doKeys('v', 'r', 'u');
eq('u', cm.getValue());
}, { value: '😀' });
testVim('r_visual_block with surrogate characters', function(cm, vim, helpers) {
cm.setCursor(0, 0);
helpers.doKeys('<C-v>', 'r', 'u');
eq('u', cm.getValue());
}, { value: '😀' });
testVim('R', function(cm, vim, helpers) {
cm.setCursor(0, 1);
helpers.doKeys('R');
Expand Down Expand Up @@ -2673,13 +2702,27 @@ testVim('s_normal', function(cm, vim, helpers) {
helpers.doKeys('<Esc>');
eq('ac', cm.getValue());
}, { value: 'abc'});
testVim('s_normal surrogate character', function(cm, vim, helpers) {
cm.setCursor(0, 0);
helpers.doKeys('s');
helpers.doKeys('test');
helpers.doKeys('<Esc>');
eq('test', cm.getValue());
}, { value: '😀' });
testVim('s_visual', function(cm, vim, helpers) {
cm.setCursor(0, 1);
helpers.doKeys('v', 's');
helpers.doKeys('<Esc>');
helpers.assertCursorAt(0, 0);
eq('ac', cm.getValue());
}, { value: 'abc'});
testVim('d with surrogate character', function(cm, vim, helpers) {
cm.setCursor(0, 0);
helpers.doKeys('v');
helpers.doKeys('d');
helpers.doKeys('<Esc>');
eq('', cm.getValue());
}, { value: '😀' });
testVim('o_visual', function(cm, vim, helpers) {
cm.setCursor(0,0);
helpers.doKeys('v','l','l','l','o');
Expand Down