Skip to content

Commit

Permalink
Merge pull request #7 from tshino/feature/split-cursor
Browse files Browse the repository at this point in the history
Add split-cursor motion detection and playback
  • Loading branch information
tshino authored Nov 21, 2021
2 parents 6f64c02 + 4e609d1 commit ff442a6
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 20 deletions.
63 changes: 50 additions & 13 deletions src/cursor_motion_detector.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
33 changes: 26 additions & 7 deletions src/internal_commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
82 changes: 82 additions & 0 deletions test/suite/cursor_motion_detector.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down
30 changes: 30 additions & 0 deletions test/suite/internal_commands.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]]);
});
});
});

0 comments on commit ff442a6

Please sign in to comment.