diff --git a/CHANGELOG.md b/CHANGELOG.md index f2e737ae..4dbafc2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to the Keyboard Macro Bata extension will be documented in t - Update - Updated keymap wrapper for Awesome Emacs Keymap (v0.37.1). - Fix + - Some types of code completion were not being reproduced correctly. [#30](https://github.com/tshino/vscode-kb-macro/pull/30) - Reenabled running tests with macOS runners on GitHub Actions. [#28](https://github.com/tshino/vscode-kb-macro/pull/28) ### [0.8.0] - 2022-01-01 diff --git a/src/command_sequence.js b/src/command_sequence.js index 7090e3dd..7d75ddd9 100644 --- a/src/command_sequence.js +++ b/src/command_sequence.js @@ -35,6 +35,30 @@ const CommandSequence = function() { continue; } } + // Combine cursor motion to the left and successive typing with deleting to the right + if (i + 1 < sequence.length && + sequence[i].command === 'internal:performCursorMotion' && + sequence[i + 1].command === 'internal:performType') { + const args1 = sequence[i].args || {}; + const args2 = sequence[i + 1].args || {}; + const characterDelta1 = args1.characterDelta || 0; + const lineDelta1 = args1.lineDelta || 0; + const selectionLength1 = args1.selectionLength || 0; + const groupSize1 = args1.groupSize || 1; + const deleteLeft2 = args2.deleteLeft || 0; + const deleteRight2 = args2.deleteRight || 0; + if (lineDelta1 === 0 && + selectionLength1 === 0 && + groupSize1 === 1 && + characterDelta1 < 0 && + characterDelta1 + deleteRight2 === 0) { + sequence[i + 1].args.deleteLeft = deleteLeft2 + deleteRight2; + delete sequence[i + 1].args.deleteRight; + sequence.splice(i, 1); + i--; + continue; + } + } // Concatenate consecutive direct typing if (0 < i && sequence[i - 1].command === 'internal:performType' && @@ -45,7 +69,9 @@ const CommandSequence = function() { const text1 = args1.text || ''; const text2 = args2.text || ''; const deleteLeft2 = args2.deleteLeft || 0; - if (text1.length >= deleteLeft2) { + const deleteRight2 = args2.deleteRight || 0; + if (text1.length >= deleteLeft2 && + deleteRight2 === 0) { const text = text1.substr(0, text1.length - deleteLeft2) + text2; sequence[i - 1].args.text = text; sequence.splice(i, 1); diff --git a/src/typing_detector.js b/src/typing_detector.js index 77775021..e2b8747a 100644 --- a/src/typing_detector.js +++ b/src/typing_detector.js @@ -66,11 +66,29 @@ const TypingDetector = function() { // every change replaces the text of the respective selection return changes.every((chg, i) => selections[i].isEqual(chg.range)); }; - const deletesLeftAndInserts = function(changes, selections) { + const isInsertingWithDeleting = function(changes, selections) { const emptySelection = selections.every(sel => sel.isEmpty); + if (!emptySelection) { + return false; + } const uniformRangeLength = changes.every(chg => chg.rangeLength === changes[0].rangeLength); - const cursorAtEndOfRange = selections.every((sel, i) => sel.active.isEqual(changes[i].range.end)); - return emptySelection && uniformRangeLength && cursorAtEndOfRange; + if (!uniformRangeLength) { + return false; + } + const sameLine = selections.every((sel, i) => sel.active.line === changes[i].range.start.line); + if (!sameLine) { + return false; + } + const deleteLeft = selections[0].active.character - changes[0].range.start.character; + const deleteRight = changes[0].range.end.character - selections[0].active.character; + if (deleteLeft < 0 || deleteRight < 0) { + return false; + } + const uniformDeletingLength = selections.every((sel, i) => ( + deleteLeft === sel.active.character - changes[i].range.start.character && + deleteRight === changes[i].range.end.character - sel.active.character + )); + return uniformDeletingLength; }; const isBracketCompletionWithSelection = function(selections, changes) { let uniformPairedText = changes.every( @@ -100,7 +118,7 @@ const TypingDetector = function() { notifyDetectedTyping(TypingType.Direct, { text: changes[0].text }); return true; } - if (deletesLeftAndInserts(changes, selections)) { + if (isInsertingWithDeleting(changes, selections)) { // Every change (in possible multi-cursor) is a combination of deleting // common number of characters to the left and inserting a common text. // This happens when a code completion occurs. @@ -109,12 +127,20 @@ const TypingDetector = function() { // 2. type 'r', 'Array' is suggested // 3. accept the suggestion // 4. then edit event happens, that replaces 'ar' with 'Array' - const deleteLeft = changes[0].rangeLength; + const deleteLeft = selections[0].active.character - changes[0].range.start.character; + const deleteRight = changes[0].range.end.character - selections[0].active.character; const prediction = util.makeSelectionsAfterTyping(changes); if (!util.isEqualSelections(selections, prediction)) { cursorMotionDetector.setPrediction(textEditor, prediction); } - notifyDetectedTyping(TypingType.Direct, { deleteLeft, text: changes[0].text }); + const args = { text: changes[0].text }; + if (0 < deleteLeft) { + args.deleteLeft = deleteLeft; + } + if (0 < deleteRight) { + args.deleteRight = deleteRight; + } + notifyDetectedTyping(TypingType.Direct, args); return true; } } diff --git a/test/suite/command_sequence.test.js b/test/suite/command_sequence.test.js index 4ed56ca2..ab7f3bca 100644 --- a/test/suite/command_sequence.test.js +++ b/test/suite/command_sequence.test.js @@ -69,7 +69,7 @@ describe('CommandSequence', () => { seq.optimize(); assert.deepStrictEqual(seq.get(), [ TYPE123 ]); }); - it('should not concatenate direct typing commands with deleting', () => { + it('should not concatenate direct typing commands with deleting (1)', () => { const TYPE1 = { command: 'internal:performType', args: { text: 'X' } @@ -84,7 +84,22 @@ describe('CommandSequence', () => { seq.optimize(); assert.deepStrictEqual(seq.get(), [ TYPE1, TYPE2 ]); }); - it('should concatenate direct typing followed by another typing with deleting', () => { + it('should not concatenate direct typing commands with deleting (2)', () => { + const TYPE1 = { + command: 'internal:performType', + args: { text: 'X' } + }; + const TYPE2 = { + command: 'internal:performType', + args: { deleteRight: 2, text: 'ABC' } + }; + const seq = CommandSequence(); + seq.push(TYPE1); + seq.push(TYPE2); + seq.optimize(); + assert.deepStrictEqual(seq.get(), [ TYPE1, TYPE2 ]); + }); + it('should concatenate direct typing followed by another typing with deleting to the left', () => { const TYPE1 = { command: 'internal:performType', args: { text: 'a' } @@ -108,7 +123,7 @@ describe('CommandSequence', () => { seq.optimize(); assert.deepStrictEqual(seq.get(), [ TYPE123 ]); }); - it('should concatenate direct typing with deleting followed by another typing without deleting', () => { + it('should concatenate direct typing with deleting followed by another typing without deleting (1)', () => { const TYPE1 = { command: 'internal:performType', args: { deleteLeft: 1, text: 'a' } @@ -127,6 +142,25 @@ describe('CommandSequence', () => { seq.optimize(); assert.deepStrictEqual(seq.get(), [ TYPE12 ]); }); + it('should concatenate direct typing with deleting followed by another typing without deleting (2)', () => { + const TYPE1 = { + command: 'internal:performType', + args: { deleteRight: 1, text: 'a' } + }; + const TYPE2 = { + command: 'internal:performType', + args: { text: 'b' } + }; + const TYPE12 = { + command: 'internal:performType', + args: { deleteRight: 1, text: 'ab' } + }; + const seq = CommandSequence(); + seq.push(TYPE1); + seq.push(TYPE2); + seq.optimize(); + assert.deepStrictEqual(seq.get(), [ TYPE12 ]); + }); it('should remove a pair of cursor motion that results no effect', () => { const MOVE1 = { command: 'internal:performCursorMotion', @@ -202,5 +236,24 @@ describe('CommandSequence', () => { seq.optimize(); assert.deepStrictEqual(seq.get(), [ MOVE1, MOVE2 ]); }); + it('should combine cursor motion to the left and successive typing with deleting to the right', () => { + const TYPE1 = { + command: 'internal:performCursorMotion', + args: { characterDelta: -1 } + }; + const TYPE2 = { + command: 'internal:performType', + args: { deleteRight: 1, text: 'a' } + }; + const TYPE12 = { + command: 'internal:performType', + args: { deleteLeft: 1, text: 'a' } + }; + const seq = CommandSequence(); + seq.push(TYPE1); + seq.push(TYPE2); + seq.optimize(); + assert.deepStrictEqual(seq.get(), [ TYPE12 ]); + }); }); }); diff --git a/test/suite/playback_typing.test.js b/test/suite/playback_typing.test.js index 6407e2c1..5dc1a61b 100644 --- a/test/suite/playback_typing.test.js +++ b/test/suite/playback_typing.test.js @@ -10,6 +10,7 @@ describe('Recording and Playback: Typing', () => { let textEditor; const Cmd = CommandsToTest; const Type = text => ({ command: 'internal:performType', args: { text } }); + const ReplaceRight = (deleteRight, text) => ({ command: 'internal:performType', args: { deleteRight, text } }); const DefaultType = text => ({ command: 'default:type', args: { text } }); const MoveLeft = delta => ({ command: 'internal:performCursorMotion', args: { characterDelta: -delta } }); const MoveRight = delta => ({ command: 'internal:performCursorMotion', args: { characterDelta: delta } }); @@ -279,7 +280,7 @@ describe('Recording and Playback: Typing', () => { await vscode.commands.executeCommand('type', { text: '10' }); await vscode.commands.executeCommand('type', { text: ')' }); // This overwrites the closing bracket. keyboardMacro.finishRecording(); - assert.deepStrictEqual(getSequence(), [ Type('()'), MoveLeft(1), Type('10'), MoveRight(1) ]); + assert.deepStrictEqual(getSequence(), [ Type('()'), MoveLeft(1), Type('10'), ReplaceRight(1, ')') ]); assert.strictEqual(textEditor.document.lineAt(5).text, '(10)'); assert.deepStrictEqual(getSelections(), [[5, 4]]); @@ -295,7 +296,7 @@ describe('Recording and Playback: Typing', () => { await vscode.commands.executeCommand('type', { text: '10' }); await vscode.commands.executeCommand('type', { text: ')' }); // This overwrites the closing bracket. keyboardMacro.finishRecording(); - assert.deepStrictEqual(getSequence(), [ Type('()'), MoveLeft(1), Type('10'), MoveRight(1) ]); + assert.deepStrictEqual(getSequence(), [ Type('()'), MoveLeft(1), Type('10'), ReplaceRight(1, ')') ]); assert.strictEqual(textEditor.document.lineAt(5).text, '(10)'); assert.strictEqual(textEditor.document.lineAt(6).text, '(10)'); assert.deepStrictEqual(getSelections(), [[5, 4], [6, 4]]); diff --git a/test/suite/typing_detector.test.js b/test/suite/typing_detector.test.js index a8f7ddcb..05edd05e 100644 --- a/test/suite/typing_detector.test.js +++ b/test/suite/typing_detector.test.js @@ -285,6 +285,13 @@ describe('TypingDetector', () => { expectedLogs: [[0, {text:'EFGHI', deleteLeft:4}]], expectedPrediction: [[20, 9]] }); }); + it('should process events, detect code completion and invoke the callback function (3)', async () => { + testDetection({ changes: [ + makeContentChange(new vscode.Range(20, 4, 20, 6), '"key": ""') + ], precond: [[20, 5]], + expectedLogs: [[0, {text:'"key": ""', deleteLeft:1, deleteRight:1}]], + expectedPrediction: [[20, 13]] }); + }); it('should not make prediction if no change is expected', async () => { testDetection({ changes: [ makeContentChange(new vscode.Range(10, 0, 10, 4), 'Abcd')