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

Add split-cursor motion detection and playback #7

Merged
merged 4 commits into from
Nov 21, 2021
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]]);
});
});
});