diff --git a/src/cursor_motion_detector.js b/src/cursor_motion_detector.js index 2bbbbdbc..32fa4c50 100644 --- a/src/cursor_motion_detector.js +++ b/src/cursor_motion_detector.js @@ -102,21 +102,58 @@ const CursorMotionDetector = function() { a.selectionLength === b.selectionLength ); }; + + const detectUniformMotion = function(document, target, base) { + const motion = calculateMotion(document, target[0], base[0]); + if (!motion) { + return; + } + for (let i = 1; i < target.length; i++) { + if (!equalsMotion(motion, calculateMotion(document, target[i], base[i]))) { + return; + } + } + // found uniform motion + return motion; + }; + const detectSplittingMotion = function(document, target, base, n) { + const motions = []; + for (let j = 0; j < n; j++) { + motions[j] = calculateMotion(document, target[j], base[0]); + if (!motions[j]) { + return; + } + if (motions[j].selectionLength !== motions[0].selectionLength) { + return; + } + } + for (let dest = 0; dest < target.length; dest++) { + const src = Math.floor(dest / n); + const m = calculateMotion(document, target[dest], base[src]); + if (!equalsMotion(m, motions[dest % n])) { + return; + } + } + // found uniform splitting motion + const motion = { + characterDelta: motions.map(m => m.characterDelta) + }; + if ('lineDelta' in motions[0]) { + motion.lineDelta = motions.map(m => m.lineDelta); + } + if ('selectionLength' in motions[0]) { + motion.selectionLength = motions[0].selectionLength; + } + return motion; + }; + const detectImplicitMotion = function(document, actual, expected) { if (actual.length === expected.length) { - const motion = calculateMotion(document, actual[0], expected[0]); - if (motion) { - if (actual.every((sel, i) => ( - i === 0 || - equalsMotion( - calculateMotion(document, sel, expected[i]), - motion - ) - ))) { - // found uniform cursor motion - return motion; - } - } + return detectUniformMotion(document, actual, expected); + } + if (actual.length % expected.length === 0) { + const n = actual.length / expected.length; + return detectSplittingMotion(document, actual, expected, n); } }; const detectAndRecordImplicitMotion = function(event) { diff --git a/src/internal_commands.js b/src/internal_commands.js index 89b56e1f..b992e1dd 100644 --- a/src/internal_commands.js +++ b/src/internal_commands.js @@ -88,15 +88,34 @@ const internalCommands = (function() { const document = textEditor.document; const characterDelta = args.characterDelta || 0; - const lineDelta = args.lineDelta || 0; + let lineDelta = args.lineDelta || 0; const selectionLength = args.selectionLength || 0; - const newSelections = Array.from(textEditor.selections).map(sel => { - const start = translate(document, sel.start, lineDelta, characterDelta); - const end = translate(document, start, 0, selectionLength); - return new vscode.Selection(start, end); - }); - textEditor.selections = newSelections; + if (Array.isArray(characterDelta)) { + // Splitting motion + // Each cursor splits into n cursors and goes to locations specified by the args. + const n = characterDelta.length; + if (!Array.isArray(lineDelta)) { + lineDelta = Array(n).fill(lineDelta); + } + const newSelections = Array.from(textEditor.selections).flatMap(sel => { + return Array.from(Array(n).keys()).map(i => { + const start = translate(document, sel.start, lineDelta[i], characterDelta[i]); + const end = translate(document, start, 0, selectionLength); + return new vscode.Selection(start, end); + }); + }); + textEditor.selections = newSelections; + } else { + // Unifor motion + // Each cursor moves with the same delta specified by the args. + const newSelections = Array.from(textEditor.selections).map(sel => { + const start = translate(document, sel.start, lineDelta, characterDelta); + const end = translate(document, start, 0, selectionLength); + return new vscode.Selection(start, end); + }); + textEditor.selections = newSelections; + } }; return { diff --git a/test/suite/cursor_motion_detector.test.js b/test/suite/cursor_motion_detector.test.js index e769ae23..6065286a 100644 --- a/test/suite/cursor_motion_detector.test.js +++ b/test/suite/cursor_motion_detector.test.js @@ -12,6 +12,9 @@ describe('CursorMotionDetector', () => { const MoveDown = (down, delta) => [ 0, { lineDelta: down, characterDelta: delta } ]; const MoveUpSelect = (up, delta, select) => [ 0, { lineDelta: -up, characterDelta: delta, selectionLength: select } ]; const MoveDownSelect = (down, delta, select) => [ 0, { lineDelta: down, characterDelta: delta, selectionLength: select } ]; + const Split = (delta) => [ 0, { characterDelta: delta } ]; + const Split2 = (delta, deltaV) => [ 0, { characterDelta: delta, lineDelta: deltaV } ]; + const SplitSelect = (delta, select) => [ 0, { characterDelta: delta, selectionLength: select } ]; describe('initial state', () => { it('should not be enabled to do detection', async () => { @@ -225,6 +228,85 @@ describe('CursorMotionDetector', () => { expectedLogs: [] }); }); + + it('should detect implicit motion (split into multi-cursor)', async () => { + testDetection({ + init: [ new vscode.Selection(3, 4, 3, 4) ], + inputs: [ + { predicted: [ new vscode.Selection(3, 7, 3, 7) ] }, + { changed: [ new vscode.Selection(3, 10, 3, 10), new vscode.Selection(3, 12, 3, 12) ] } + ], + expectedLogs: [ Split([ 3, 5 ]) ] + }); + }); + it('should detect implicit motion (split multi to multi)', async () => { + testDetection({ + init: [ new vscode.Selection(3, 4, 3, 4), new vscode.Selection(6, 4, 6, 4) ], + inputs: [ + { predicted: [ + new vscode.Selection(3, 7, 3, 7), + new vscode.Selection(6, 7, 6, 7) + ] }, + { changed: [ + new vscode.Selection(3, 10, 3, 10), new vscode.Selection(3, 12, 3, 12), + new vscode.Selection(6, 10, 6, 10), new vscode.Selection(6, 12, 6, 12) + ] } + ], + expectedLogs: [ Split([ 3, 5 ]) ] + }); + }); + it('should ignore non-uniform implicit motion (split multi to multi)', async () => { + testDetection({ + init: [ new vscode.Selection(3, 4, 3, 4), new vscode.Selection(6, 4, 6, 4) ], + inputs: [ + { predicted: [ + new vscode.Selection(3, 7, 3, 7), + new vscode.Selection(6, 7, 6, 7) + ] }, + { changed: [ + new vscode.Selection(3, 10, 3, 10), new vscode.Selection(3, 12, 3, 12), + new vscode.Selection(6, 10, 6, 10), new vscode.Selection(6, 11, 6, 11) // <= not match + ] } + ], + expectedLogs: [] + }); + }); + it('should detect implicit motion (split into multi-cursor on different lines)', async () => { + testDetection({ + init: [ new vscode.Selection(3, 4, 3, 4) ], + inputs: [ + { predicted: [ + new vscode.Selection(3, 7, 3, 7), + new vscode.Selection(6, 7, 6, 7) + ] }, + { changed: [ + new vscode.Selection(4, 2, 4, 2), new vscode.Selection(5, 7, 5, 7), + new vscode.Selection(7, 2, 7, 2), new vscode.Selection(8, 7, 8, 7), + ] } + ], + expectedLogs: [ Split2([ 2, 7 ], [ 1, 2 ]) ] + }); + }); + it('should detect implicit motion (split into multi-cursor with selection)', async () => { + testDetection({ + init: [ new vscode.Selection(3, 4, 3, 4) ], + inputs: [ + { predicted: [ new vscode.Selection(3, 7, 3, 7) ] }, + { changed: [ new vscode.Selection(3, 10, 3, 13), new vscode.Selection(3, 12, 3, 15) ] } + ], + expectedLogs: [ SplitSelect([ 3, 5 ], 3) ] + }); + }); + it('should ignore implicit motion with splitting to non-uniform selection length', async () => { + testDetection({ + init: [ new vscode.Selection(3, 4, 3, 4) ], + inputs: [ + { predicted: [ new vscode.Selection(3, 7, 3, 7) ] }, + { changed: [ new vscode.Selection(3, 10, 3, 13), new vscode.Selection(3, 12, 3, 16) ] } + ], + expectedLogs: [] + }); + }); }); describe('implicit motion without prediction', () => { it('should detect the unexpected motion of cursor (move to left)', async () => { diff --git a/test/suite/internal_commands.test.js b/test/suite/internal_commands.test.js index fe2e7733..9be3503a 100644 --- a/test/suite/internal_commands.test.js +++ b/test/suite/internal_commands.test.js @@ -276,4 +276,34 @@ describe('internalCommands', () => { assert.deepStrictEqual(getSelections(), [[3, 1, 3, 4], [4, 1, 4, 4]]); }); }); + + describe('performCursorMotion (split into multiple cursors)', () => { + before(async () => { + await TestUtil.resetDocument(textEditor, ( + 'abcde\n'.repeat(10) + + 'fghij klmno\n'.repeat(10) + )); + }); + it('should split cursor into multiple cursors', async () => { + setSelections([[3, 4]]); + await internalCommands.performCursorMotion({ characterDelta: [ -3, -1 ] }); + assert.deepStrictEqual(getSelections(), [[3, 1], [3, 3]]); + }); + it('should split cursor into multiple cursors (with lineDelta)', async () => { + setSelections([[3, 4]]); + await internalCommands.performCursorMotion({ + characterDelta: [ -3, -1 ], + lineDelta: [ -1, -2 ] + }); + assert.deepStrictEqual(getSelections(), [[2, 2], [1, 4]]); + }); + it('should split cursor into multiple cursors (with selectionLength)', async () => { + setSelections([[3, 4]]); + await internalCommands.performCursorMotion({ + characterDelta: [ -3, -1 ], + selectionLength: 1 + }); + assert.deepStrictEqual(getSelections(), [[3, 1, 3, 2], [3, 3, 3, 4]]); + }); + }); });