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 repeatPlaybackTillEndOfFile command #36

Merged
merged 5 commits into from
Jan 26, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to the Keyboard Macro Bata extension will be documented in t

### [Unreleased]
- Feature
- Added a new `Keyboard Macro: Repeat Playback Till End of File` command. [#34](https://github.com/tshino/vscode-kb-macro/issues/34)
- Made the `kb-macro.wrap` command queueable to reduce input misses during recording. [#32](https://github.com/tshino/vscode-kb-macro/pull/32)
- Documentation
- Added 'Tips' section to the README.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ This is the default keybinding set for recording/playback of this extension. Cop
| `Playback` | `kb-macro.playback` | Perform playback of the last recorded sequence |
| `Abort Playback` | `kb-macro.abortPlayback` | Abort currently-running playback |
| `Repeat Playback` | `kb-macro.repeatPlayback` | Perform playback specified number of times |
| `Repeat Playback Till End of File` | `kb-macro.repeatPlaybackTillEndOfFile` | Repeat playback until the cursor reaches the end of the file |

The `kb-macro.repeatPlayback` command shows an input box to specify the number of times.

Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"onCommand:kb-macro.playback",
"onCommand:kb-macro.abortPlayback",
"onCommand:kb-macro.repeatPlayback",
"onCommand:kb-macro.repeatPlaybackTillEndOfFile",
"onCommand:kb-macro.wrap"
],
"main": "./src/extension.js",
Expand Down Expand Up @@ -106,6 +107,11 @@
"command": "kb-macro.repeatPlayback",
"title": "Repeat Playback",
"category": "Keyboard Macro"
},
{
"command": "kb-macro.repeatPlaybackTillEndOfFile",
"title": "Repeat Playback Till End of File",
"category": "Keyboard Macro"
}
],
"keybindings": [
Expand Down
83 changes: 83 additions & 0 deletions src/end_of_file_detector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use strict';

const endOfFileDetectorUtil = (function() {
const getCursorPosition = function(textEditor) {
return textEditor.selections[textEditor.selections.length - 1].active;
};
const calculateDistanceBelow = function(textEditor) {
if (!textEditor) {
return [0, 0];
}
const lineCount = textEditor.document.lineCount;
const currentLine = getCursorPosition(textEditor).line;
const lineLength = textEditor.document.lineAt(currentLine).text.length;
const currentChar = getCursorPosition(textEditor).character;
return [
Math.max(0, lineCount - 1 - currentLine),
Math.max(0, lineLength - currentChar)
];
};
const compareDistance = function(a, b) {
if (a[0] < b[0]) {
return -1;
} else if (a[0] > b[0]) {
return 1;
}
if (a[1] < b[1]) {
return -1;
} else if (a[1] > b[1]) {
return 1;
}
return 0;
};
return {
getCursorPosition,
calculateDistanceBelow,
compareDistance
};
})();

const EndOfFileDetector = function(textEditor) {
let lastDistanceBelow = endOfFileDetectorUtil.calculateDistanceBelow(textEditor);

// whether distanceBelow[0] is predicted to decline or not
let belowLinesDeclines = null;

const reachedEndOfFile = function() {
const distanceBelow = endOfFileDetectorUtil.calculateDistanceBelow(textEditor);
const compBelow = endOfFileDetectorUtil.compareDistance(distanceBelow, lastDistanceBelow);
if (distanceBelow[0] === 0 && distanceBelow[1] === 0) {
// it reached the end of the document
return true;
}
if (compBelow >= 0) {
// distance to the bottom of the document should always decline, otherwise we stop
return true;
}
if (belowLinesDeclines === null) {
belowLinesDeclines = distanceBelow[0] < lastDistanceBelow[0];
} else if (belowLinesDeclines) {
if (distanceBelow[0] >= lastDistanceBelow[0]) {
// rest lines below the cursor should decline consistently, otherwise, we stop
return true;
}
if (distanceBelow[0] === 0) {
// it reached the last line of the document
return true;
}
}
lastDistanceBelow = distanceBelow;
return false;
};

return {
reachedEndOfFile
};
};

module.exports = {
EndOfFileDetector,

// testing purpose only
endOfFileDetectorUtil
};
1 change: 1 addition & 0 deletions src/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function activate(context) {
registerCommand('playback', keyboardMacro.playback);
registerCommand('abortPlayback', keyboardMacro.abortPlayback);
registerCommand('repeatPlayback', keyboardMacro.repeatPlayback);
registerCommand('repeatPlaybackTillEndOfFile', keyboardMacro.repeatPlaybackTillEndOfFile);
registerCommand('wrap', keyboardMacro.wrap);

keyboardMacro.registerInternalCommand('internal:performType', internalCommands.performType);
Expand Down
24 changes: 22 additions & 2 deletions src/keyboard_macro.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';
const vscode = require('vscode');
const { CommandSequence } = require('./command_sequence.js');
const { EndOfFileDetector } = require('./end_of_file_detector.js');
const reentrantGuard = require('./reentrant_guard.js');

const KeyboardMacro = function({ awaitController }) {
Expand Down Expand Up @@ -113,7 +114,7 @@ const KeyboardMacro = function({ awaitController }) {
return ok;
};

const playbackImpl = async function(args) {
const playbackImpl = async function(args, { tillEndOfFile = false } = {}) {
if (recording) {
return;
}
Expand All @@ -123,14 +124,26 @@ const KeyboardMacro = function({ awaitController }) {
args = (args && typeof(args) === 'object') ? args : {};
const repeat = typeof(args.repeat) === 'number' ? args.repeat : 1;
const commands = sequence.get();
let endOfFileDetector;
if (tillEndOfFile) {
endOfFileDetector = EndOfFileDetector(vscode.window.activeTextEditor);
}
let ok = true;
for (let k = 0; k < repeat && ok && !shouldAbortPlayback; k++) {
for (let k = 0; k < repeat || tillEndOfFile; k++) {
for (const spec of commands) {
ok = await invokeCommandSync(spec, 'playback');
if (!ok || shouldAbortPlayback) {
break;
}
}
if (!ok || shouldAbortPlayback) {
break;
}
if (tillEndOfFile) {
if (endOfFileDetector.reachedEndOfFile()) {
break;
}
}
}
} finally {
const reason = shouldAbortPlayback ?
Expand Down Expand Up @@ -169,6 +182,12 @@ const KeyboardMacro = function({ awaitController }) {
}
});

const repeatPlaybackTillEndOfFile = reentrantGuard.makeGuardedCommand(async function() {
const args = {};
const option = { tillEndOfFile: true };
await playbackImpl(args, option);
});

const makeCommandSpec = function(args) {
if (!args || !args.command) {
return null;
Expand Down Expand Up @@ -230,6 +249,7 @@ const KeyboardMacro = function({ awaitController }) {
abortPlayback,
validatePositiveIntegerInput,
repeatPlayback,
repeatPlaybackTillEndOfFile,
wrap,

// testing purpose only
Expand Down
162 changes: 162 additions & 0 deletions test/suite/end_of_file_detector.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
'use strict';
const assert = require('assert');
const vscode = require('vscode');
const { TestUtil } = require('./test_util.js');
const { endOfFileDetectorUtil, EndOfFileDetector } = require('../../src/end_of_file_detector.js');

describe('endOfFileDetectorUtil', () => {
describe('getCursorPosition', () => {
const getCursorPosition = endOfFileDetectorUtil.getCursorPosition;
it('should return the cursor position', () => {
const Input = { selections: [ new vscode.Selection(3, 5, 3, 5) ] };
const Expected = new vscode.Position(3, 5);
assert.strictEqual(getCursorPosition(Input).isEqual(Expected), true);
});
it('should return the active position of current selection range (1)', () => {
const Input = { selections: [ new vscode.Selection(3, 5, 6, 8) ] };
const Expected = new vscode.Position(6, 8);
assert.strictEqual(getCursorPosition(Input).isEqual(Expected), true);
});
it('should return the active position of current selection range (2)', () => {
const Input = { selections: [ new vscode.Selection(6, 8, 3, 5) ] };
const Expected = new vscode.Position(3, 5);
assert.strictEqual(getCursorPosition(Input).isEqual(Expected), true);
});
it('should return the position of the last cursor in multi-cursor (1)', () => {
const Input = { selections: [
new vscode.Selection(1, 4, 1, 4),
new vscode.Selection(2, 4, 2, 4),
new vscode.Selection(3, 4, 3, 4)
] };
const Expected = new vscode.Position(3, 4);
assert.strictEqual(getCursorPosition(Input).isEqual(Expected), true);
});
it('should return the position of the last cursor in multi-cursor (2)', () => {
const Input = { selections: [
new vscode.Selection(3, 4, 3, 4),
new vscode.Selection(2, 4, 2, 4),
new vscode.Selection(1, 4, 1, 4)
] };
const Expected = new vscode.Position(1, 4);
assert.strictEqual(getCursorPosition(Input).isEqual(Expected), true);
});
});
describe('calculateDistanceBelow', () => {
const calculateDistanceBelow = endOfFileDetectorUtil.calculateDistanceBelow;
it('should return [0, 0] if the editor is null', () => {
assert.deepStrictEqual(calculateDistanceBelow(null), [0, 0]);
});
it('should return the number of lines below the cursor and the number of characters right of the cursor', () => {
const editorMock = {
document: {
lineCount: 10,
lineAt: () => ({ text: ' '.repeat(15) })
},
selections: [
new vscode.Selection(5, 3, 5, 3)
]
};
assert.deepStrictEqual(calculateDistanceBelow(editorMock), [4, 12]);
});
it('should return [0, 0] if the cursor is at the end of the document', () => {
const editorMock = {
document: {
lineCount: 10,
lineAt: () => ({ text: ' '.repeat(15) })
},
selections: [
new vscode.Selection(9, 15, 9, 15)
]
};
assert.deepStrictEqual(calculateDistanceBelow(editorMock), [0, 0]);
});
});
describe('compareDistance', () => {
const compareDistance = endOfFileDetectorUtil.compareDistance;
it('should return 0 if two arguments are the same distance', () => {
assert.strictEqual(compareDistance([3, 4], [3, 4]), 0);
assert.strictEqual(compareDistance([0, 1], [0, 1]), 0);
assert.strictEqual(compareDistance([10, 0], [10, 0]), 0);
assert.strictEqual(compareDistance([8, 5], [8, 5]), 0);
});
it('should return a positive integer if first one is greater', () => {
assert.strictEqual(compareDistance([4, 1], [3, 1]) > 0, true);
assert.strictEqual(compareDistance([4, 5], [3, 7]) > 0, true);
assert.strictEqual(compareDistance([4, 7], [3, 5]) > 0, true);
assert.strictEqual(compareDistance([5, 8], [5, 4]) > 0, true);
});
it('should return a negative integer if first one is smaller', () => {
assert.strictEqual(compareDistance([3, 1], [4, 1]) < 0, true);
assert.strictEqual(compareDistance([3, 7], [4, 5]) < 0, true);
assert.strictEqual(compareDistance([3, 5], [4, 7]) < 0, true);
assert.strictEqual(compareDistance([5, 4], [5, 8]) < 0, true);
});
});
});

describe('EndOfFileDetector', () => {
let textEditor;
const setSelections = function(array) {
textEditor.selections = TestUtil.arrayToSelections(array);
};
before(async () => {
vscode.window.showInformationMessage('Started test for EndOfFileDetector.');
textEditor = await TestUtil.setupTextEditor({ content: '' });
});

describe('reachedEndOfFile', () => {
beforeEach(async () => {
await TestUtil.resetDocument(textEditor, (
'0. zero\n' +
'1. one\n' +
'2. two\n' +
'3. three\n' +
'4. four\n' +
'5. five'
));
});
it('should return true immediately if the cursor does not move at all', async () => {
setSelections([[2, 3]]);
const detector = EndOfFileDetector(textEditor);
assert.strictEqual(detector.reachedEndOfFile(), true);
});
it('should return true immediately if the cursor moves up', async () => {
setSelections([[2, 3]]);
const detector = EndOfFileDetector(textEditor);
setSelections([[1, 3]]);
assert.strictEqual(detector.reachedEndOfFile(), true);
});
it('should return true immediately if the cursor moves to the left', async () => {
setSelections([[2, 3]]);
const detector = EndOfFileDetector(textEditor);
setSelections([[2, 2]]);
assert.strictEqual(detector.reachedEndOfFile(), true);
});
it('should return true if it reaches the end of the document', async () => {
setSelections([[5, 5]]);
const detector = EndOfFileDetector(textEditor);
setSelections([[5, 6]]);
assert.strictEqual(detector.reachedEndOfFile(), false);
setSelections([[5, 7]]);
assert.strictEqual(detector.reachedEndOfFile(), true);
});
it('should return true if it reaches the last line of the document', async () => {
setSelections([[3, 0]]);
const detector = EndOfFileDetector(textEditor);
setSelections([[4, 0]]);
assert.strictEqual(detector.reachedEndOfFile(), false);
setSelections([[5, 0]]);
assert.strictEqual(detector.reachedEndOfFile(), true);
});
it('should return true if the rest lines below the cursor stops to decline', async () => {
setSelections([[1, 0]]);
const detector = EndOfFileDetector(textEditor);
setSelections([[2, 0]]);
assert.strictEqual(detector.reachedEndOfFile(), false);
setSelections([[3, 0]]);
assert.strictEqual(detector.reachedEndOfFile(), false);
setSelections([[3, 8]]);
assert.strictEqual(detector.reachedEndOfFile(), true);
});
});
});
Loading