From 72fd4b7b616f070be198dc8fc437d48a74637a53 Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Fri, 23 Sep 2022 21:54:42 +0200 Subject: [PATCH] fix: Fixed handling surrogare characters in insert, replace, delete mode in Vim --- src/keyboard/vim.js | 36 ++++++++++++++++++++++++++++++------ src/keyboard/vim_test.js | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/keyboard/vim.js b/src/keyboard/vim.js index 328c661ad4c..ec24195ddca 100644 --- a/src/keyboard/vim.js +++ b/src/keyboard/vim.js @@ -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. @@ -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; @@ -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); @@ -3449,9 +3465,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); @@ -3749,18 +3766,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 { diff --git a/src/keyboard/vim_test.js b/src/keyboard/vim_test.js index 1ea85e537ad..05469cee0a7 100644 --- a/src/keyboard/vim_test.js +++ b/src/keyboard/vim_test.js @@ -1863,6 +1863,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(''); + eq('testπŸ˜€', cm.getValue()); +}, { value: 'πŸ˜€' }); testVim('i_repeat', function(cm, vim, helpers) { helpers.doKeys('3', 'i'); helpers.doKeys('test') @@ -2111,6 +2118,11 @@ testVim('r', function(cm, vim, helpers) { helpers.doKeys('r', ''); 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); @@ -2125,6 +2137,16 @@ testVim('r_visual_block', function(cm, vim, helpers) { helpers.doKeys('', '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('', 'r', 'u'); + eq('u', cm.getValue()); +}, { value: 'πŸ˜€' }); testVim('R', function(cm, vim, helpers) { cm.setCursor(0, 1); helpers.doKeys('R'); @@ -2673,6 +2695,13 @@ testVim('s_normal', function(cm, vim, helpers) { helpers.doKeys(''); 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(''); + eq('test', cm.getValue()); +}, { value: 'πŸ˜€' }); testVim('s_visual', function(cm, vim, helpers) { cm.setCursor(0, 1); helpers.doKeys('v', 's'); @@ -2680,6 +2709,13 @@ testVim('s_visual', function(cm, vim, helpers) { 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(''); + eq('', cm.getValue()); +}, { value: 'πŸ˜€' }); testVim('o_visual', function(cm, vim, helpers) { cm.setCursor(0,0); helpers.doKeys('v','l','l','l','o');