Skip to content

Commit

Permalink
Merge pull request #36 from tshino/playback-till-end-of-file
Browse files Browse the repository at this point in the history
Add repeatPlaybackTillEndOfFile command
  • Loading branch information
tshino authored Jan 26, 2022
2 parents 481c251 + b6246fe commit d6c8905
Show file tree
Hide file tree
Showing 8 changed files with 381 additions and 2 deletions.
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('$type', 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

0 comments on commit d6c8905

Please sign in to comment.