diff --git a/src/Actions/Delete.ts b/src/Actions/Delete.ts index 2c5b0088..b93eabac 100644 --- a/src/Actions/Delete.ts +++ b/src/Actions/Delete.ts @@ -10,33 +10,36 @@ import { UtilRange } from '../Utils/Range'; export class ActionDelete { @StaticReflect.metadata(SymbolMetadata.Action.isChange, true) - static byMotions(args: { + static async byMotions(args: { motions: Motion[]; isChangeAction?: boolean; shouldYank?: boolean; - }): Thenable { + }): Promise { args.isChangeAction = args.isChangeAction === undefined ? false : args.isChangeAction; args.shouldYank = args.shouldYank === undefined ? false : args.shouldYank; const activeTextEditor = window.activeTextEditor; if (!activeTextEditor) { - return Promise.resolve(false); + return false; } const document = activeTextEditor.document; - let ranges = activeTextEditor.selections.map((selection) => { + let ranges: Range[] = []; + + for (const selection of activeTextEditor.selections) { const start = selection.active; - const end = args.motions.reduce((position, motion) => { - return motion.apply(position, { + let position = start; + for (const motion of args.motions) { + position = await motion.apply(position, { isInclusive: true, shouldCrossLines: false, isChangeAction: args.isChangeAction, }); - }, start); - return new Range(start, end); - }); + } + ranges.push(new Range(start, position)); + } if (args.motions.some((motion) => motion.isLinewise)) { ranges = ranges.map((range) => UtilRange.toLinewise(range, document)); @@ -46,20 +49,19 @@ export class ActionDelete { // TODO: Move cursor to first non-space if needed - return (args.shouldYank - ? ActionRegister.yankByMotions({ - motions: args.motions, - isChangeAction: args.isChangeAction, - }) - : Promise.resolve(true) - ) - .then(() => { - return activeTextEditor.edit((editBuilder) => { - ranges.forEach((range) => editBuilder.delete(range)); - }); - }) - .then(() => ActionSelection.shrinkToStarts()) - .then(() => ActionReveal.primaryCursor()); + if (args.shouldYank) { + await ActionRegister.yankByMotions({ + motions: args.motions, + isChangeAction: args.isChangeAction, + }); + } + await activeTextEditor.edit((editBuilder) => { + ranges.forEach((range) => editBuilder.delete(range)); + }); + await ActionSelection.shrinkToStarts(); + await ActionReveal.primaryCursor(); + + return true; } @StaticReflect.metadata(SymbolMetadata.Action.isChange, true) diff --git a/src/Actions/MoveCursor.ts b/src/Actions/MoveCursor.ts index 1c2b30a3..52383c43 100644 --- a/src/Actions/MoveCursor.ts +++ b/src/Actions/MoveCursor.ts @@ -42,12 +42,12 @@ export class ActionMoveCursor { return Promise.resolve(true); } - static byMotions(args: { + static async byMotions(args: { motions: Motion[]; isVisualMode?: boolean; isVisualLineMode?: boolean; noEmptyAtLineEnd?: boolean; - }): Thenable { + }): Promise { args.isVisualMode = args.isVisualMode === undefined ? false : args.isVisualMode; args.isVisualLineMode = args.isVisualLineMode === undefined ? false : args.isVisualLineMode; args.noEmptyAtLineEnd = args.noEmptyAtLineEnd === undefined ? false : args.noEmptyAtLineEnd; @@ -55,7 +55,7 @@ export class ActionMoveCursor { const activeTextEditor = window.activeTextEditor; if (!activeTextEditor) { - return Promise.resolve(false); + return false; } // Prevent preferred character update if no motion updates character. @@ -65,19 +65,21 @@ export class ActionMoveCursor { const document = activeTextEditor.document; - activeTextEditor.selections = activeTextEditor.selections.map((selection, i) => { + const selections: Selection[] = []; + + for (let i = 0; i < activeTextEditor.selections.length; i++) { + const selection = activeTextEditor.selections[i]; let anchor: Position; - let active = args.motions.reduce( - (position, motion) => { - return motion.apply(position, { - preferredColumn: ActionMoveCursor.preferredColumnBySelectionIndex[i], - }); - }, - args.isVisualMode - ? UtilSelection.getActiveInVisualMode(selection) - : selection.active, - ); + let active = args.isVisualMode + ? UtilSelection.getActiveInVisualMode(selection) + : selection.active; + + for (const motion of args.motions) { + active = await motion.apply(active, { + preferredColumn: ActionMoveCursor.preferredColumnBySelectionIndex[i], + }); + } if (args.isVisualMode) { anchor = selection.anchor; @@ -127,9 +129,12 @@ export class ActionMoveCursor { anchor = active; } - return new Selection(anchor, active); - }); + selections.push(new Selection(anchor, active)); + } + + activeTextEditor.selections = selections; + await ActionReveal.primaryCursor(); - return ActionReveal.primaryCursor(); + return true; } } diff --git a/src/Actions/Register.ts b/src/Actions/Register.ts index d0725b1b..ed52895f 100644 --- a/src/Actions/Register.ts +++ b/src/Actions/Register.ts @@ -72,33 +72,40 @@ export class ActionRegister { return Promise.resolve(true); } - static yankByMotions(args: { motions: Motion[]; isChangeAction?: boolean }): Thenable { + static async yankByMotions(args: { + motions: Motion[]; + isChangeAction?: boolean; + }): Promise { args.isChangeAction = args.isChangeAction === undefined ? false : args.isChangeAction; const activeTextEditor = window.activeTextEditor; if (!activeTextEditor) { - return Promise.resolve(false); + return false; } const isLinewise = args.motions.some((motion) => motion.isLinewise); - const ranges = activeTextEditor.selections.map((selection) => { + let ranges: Range[] = []; + + for (const selection of activeTextEditor.selections) { const start = selection.active; - const end = args.motions.reduce((position, motion) => { - return motion.apply(position, { + let position = start; + for (const motion of args.motions) { + position = await motion.apply(position, { isInclusive: true, shouldCrossLines: false, isChangeAction: args.isChangeAction, }); - }, start); - return new Range(start, end); - }); - - return ActionRegister.yankRanges({ + } + ranges.push(new Range(start, position)); + } + await ActionRegister.yankRanges({ ranges: ranges, isLinewise: isLinewise, }); + + return true; } static async yankByTextObject(args: { textObject: TextObject }): Promise { diff --git a/src/Mappers/SpecialKeys/Motion.ts b/src/Mappers/SpecialKeys/Motion.ts index 86e4b3cb..e55413b2 100644 --- a/src/Mappers/SpecialKeys/Motion.ts +++ b/src/Mappers/SpecialKeys/Motion.ts @@ -11,6 +11,7 @@ import { MotionMatchPair } from '../../Motions/MatchPair'; import { MotionLine } from '../../Motions/Line'; import { MotionParagraph } from '../../Motions/Paragraph'; import { MotionDocument } from '../../Motions/Document'; +import { MotionNavigation } from '../../Motions/Navigation'; interface MotionGenerator { (args?: {}): Motion; @@ -120,6 +121,8 @@ export class SpecialKeyMotion extends GenericMapper implements SpecialKeyCommon { keys: 'space', motionGenerators: [MotionDirection.next] }, { keys: 'backspace', motionGenerators: [MotionDirection.prev] }, + { keys: 'g d', motionGenerators: [MotionNavigation.toDeclaration] }, + { keys: 'g D', motionGenerators: [MotionNavigation.toTypeDefinition] }, ]; constructor() { diff --git a/src/Motions/Direction.ts b/src/Motions/Direction.ts index 02e74f0e..2d7de9e0 100644 --- a/src/Motions/Direction.ts +++ b/src/Motions/Direction.ts @@ -33,8 +33,8 @@ export class MotionDirection extends Motion { }); } - apply(from: Position, option?: any): Position { - from = super.apply(from); + async apply(from: Position, option?: any): Promise { + from = await super.apply(from); const activeTextEditor = window.activeTextEditor; diff --git a/src/Motions/Document.ts b/src/Motions/Document.ts index 23dc4890..63039b33 100644 --- a/src/Motions/Document.ts +++ b/src/Motions/Document.ts @@ -28,8 +28,8 @@ export class MotionDocument extends Motion { return obj; } - apply(from: Position): Position { - from = super.apply(from); + async apply(from: Position): Promise { + from = await super.apply(from); const activeTextEditor = window.activeTextEditor; diff --git a/src/Motions/Line.ts b/src/Motions/Line.ts index 8466e7c4..42ed3c30 100644 --- a/src/Motions/Line.ts +++ b/src/Motions/Line.ts @@ -22,8 +22,8 @@ export class MotionLine extends Motion { return obj; } - apply(from: Position): Position { - from = super.apply(from); + async apply(from: Position): Promise { + from = await super.apply(from); const activeTextEditor = window.activeTextEditor; diff --git a/src/Motions/Match.ts b/src/Motions/Match.ts index e727b4b6..b8d9bda8 100644 --- a/src/Motions/Match.ts +++ b/src/Motions/Match.ts @@ -66,10 +66,10 @@ export class MotionMatch extends Motion { return obj; } - apply(from: Position, option: { isInclusive?: boolean } = {}): Position { + async apply(from: Position, option: { isInclusive?: boolean } = {}): Promise { option.isInclusive = option.isInclusive === undefined ? false : option.isInclusive; - from = super.apply(from); + from = await super.apply(from); const activeTextEditor = window.activeTextEditor; diff --git a/src/Motions/MatchPair.ts b/src/Motions/MatchPair.ts index 614359ce..ce3e80d3 100644 --- a/src/Motions/MatchPair.ts +++ b/src/Motions/MatchPair.ts @@ -36,15 +36,15 @@ export class MotionMatchPair extends Motion { return new MotionMatchPair(); } - apply( + async apply( from: Position, option: { isInclusive?: boolean; } = {}, - ): Position { + ): Promise { option.isInclusive = option.isInclusive === undefined ? false : option.isInclusive; - from = super.apply(from); + from = await super.apply(from); const activeTextEditor = window.activeTextEditor; diff --git a/src/Motions/Motion.ts b/src/Motions/Motion.ts index becc83a7..0c1c43d2 100644 --- a/src/Motions/Motion.ts +++ b/src/Motions/Motion.ts @@ -23,7 +23,7 @@ export abstract class Motion { this.characterDelta += characterDelta; } - apply(from: Position, option?: any): Position { + async apply(from: Position, option?: any): Promise { const activeTextEditor = window.activeTextEditor; if (!activeTextEditor) { diff --git a/src/Motions/Navigation.ts b/src/Motions/Navigation.ts new file mode 100644 index 00000000..23f6f206 --- /dev/null +++ b/src/Motions/Navigation.ts @@ -0,0 +1,32 @@ +import { commands, window, Position } from 'vscode'; +import { Motion } from './Motion'; + +export class MotionNavigation extends Motion { + private command: string; + + static toDeclaration(): Motion { + const obj = new MotionNavigation({ isLinewise: true }); + obj.command = 'editor.action.goToDeclaration'; + return obj; + } + + static toTypeDefinition(): Motion { + const obj = new MotionNavigation({ isLinewise: true }); + obj.command = 'editor.action.goToTypeDefinition'; + return obj; + } + + async apply(from: Position): Promise { + from = await super.apply(from); + + const activeTextEditor = window.activeTextEditor; + + if (!activeTextEditor) { + return from; + } + + await commands.executeCommand(this.command); + + return activeTextEditor.selection.active; + } +} diff --git a/src/Motions/Paragraph.ts b/src/Motions/Paragraph.ts index 55db5ddf..978f5f8a 100644 --- a/src/Motions/Paragraph.ts +++ b/src/Motions/Paragraph.ts @@ -33,8 +33,8 @@ export class MotionParagraph extends Motion { }); } - apply(from: Position): Position { - from = super.apply(from); + async apply(from: Position): Promise { + from = await super.apply(from); const activeTextEditor = window.activeTextEditor; diff --git a/src/Motions/Word.ts b/src/Motions/Word.ts index b94c4666..cae2684b 100644 --- a/src/Motions/Word.ts +++ b/src/Motions/Word.ts @@ -79,20 +79,20 @@ export class MotionWord extends Motion { return obj; } - apply( + async apply( from: Position, option: { isInclusive?: boolean; isChangeAction?: boolean; shouldCrossLines?: boolean; } = {}, - ): Position { + ): Promise { option.isInclusive = option.isInclusive === undefined ? false : option.isInclusive; option.isChangeAction = option.isChangeAction === undefined ? false : option.isChangeAction; option.shouldCrossLines = option.shouldCrossLines === undefined ? true : option.shouldCrossLines; - from = super.apply(from); + from = await super.apply(from); const activeTextEditor = window.activeTextEditor; diff --git a/test/Framework/BlackBox.ts b/test/Framework/BlackBox.ts index d5528ad4..5a0bdd97 100644 --- a/test/Framework/BlackBox.ts +++ b/test/Framework/BlackBox.ts @@ -3,6 +3,7 @@ import * as TestUtil from './Util'; import { TextEditor, TextDocument, Selection, extensions } from 'vscode'; export interface TestCase { + language?: string; from: string; inputs: string; to: string; @@ -93,7 +94,7 @@ const extractInfo = (originalText: string) => { }; }; -let reusableDocument: TextDocument; +const reusableDocuments: Map = new Map(); export const run = (testCase: TestCase, before?: (textEditor: TextEditor) => void) => { const plainFrom = testCase.from.replace(/\n/g, '\\n'); @@ -106,37 +107,44 @@ export const run = (testCase: TestCase, before?: (textEditor: TextEditor) => voi test(expectation, (done) => { tries++; + const language = testCase.language || 'plaintext'; const fromInfo = extractInfo(testCase.from); const toInfo = extractInfo(testCase.to); const inputs = testCase.inputs.split(' '); - TestUtil.createTempDocument(fromInfo.cleanText, reusableDocument).then( - async (textEditor) => { - reusableDocument = textEditor.document; + TestUtil.createTempDocument( + fromInfo.cleanText, + reusableDocuments.get(language), + language, + ).then(async (textEditor) => { + reusableDocuments.set(textEditor.document.languageId, textEditor.document); - if (before) { - before(textEditor); - } - - TestUtil.setSelections(fromInfo.selections); + if (before) { + before(textEditor); + } - await waitForMillisecond(50 * tries); + TestUtil.setSelections(fromInfo.selections); - for (let i = 0; i < inputs.length; i++) { - getCurrentMode()!.input(inputs[i]); - await waitForMillisecond(20 * tries); - } + await waitForMillisecond(50 * tries); - try { - assert.equal(TestUtil.getDocument()!.getText(), toInfo.cleanText); - assert.deepEqual(TestUtil.getSelections(), toInfo.selections); - } catch (error) { - done(error); - return; - } + for (let i = 0; i < inputs.length; i++) { + getCurrentMode()!.input(inputs[i]); + await waitForMillisecond(20 * tries); + } - done(); - }, - ); + if (language !== 'plaintext') { + await waitForMillisecond(50 * tries); + } + + try { + assert.equal(TestUtil.getDocument()!.getText(), toInfo.cleanText); + assert.deepEqual(TestUtil.getSelections(), toInfo.selections); + } catch (error) { + done(error); + return; + } + + done(); + }); }); }; diff --git a/test/Framework/Util.ts b/test/Framework/Util.ts index 3f6f4a5b..d5c3770e 100644 --- a/test/Framework/Util.ts +++ b/test/Framework/Util.ts @@ -1,7 +1,6 @@ import { workspace, window, - Uri, TextDocument, TextEditor, Position, @@ -10,39 +9,41 @@ import { EndOfLine, } from 'vscode'; -export function createTempDocument( +export async function createTempDocument( content?: string, reusableDocument?: TextDocument, -): Thenable { - let getTextEditor: Thenable; + language: string = 'plaintext', +): Promise { + let textEditor: TextEditor; if ( - reusableDocument && + reusableDocument?.languageId === language && window.activeTextEditor && window.activeTextEditor.document === reusableDocument ) { - getTextEditor = Promise.resolve(window.activeTextEditor); + textEditor = window.activeTextEditor; } else { - const uri = reusableDocument - ? reusableDocument.uri - : Uri.parse(`untitled:${__dirname}.${Math.random()}.tmp`); - getTextEditor = workspace - .openTextDocument(uri) - .then((document) => window.showTextDocument(document)); + let document: TextDocument; + if (reusableDocument?.languageId === language) { + document = await workspace.openTextDocument(reusableDocument.uri); + } else { + document = await workspace.openTextDocument({ language }); + // for non-plaintext files, sleep for a while to let the language server load + if (language !== 'plaintext') { + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + } + textEditor = await window.showTextDocument(document); } - return getTextEditor.then((textEditor) => { - if (content) { - return textEditor - .edit((editBuilder) => { - editBuilder.setEndOfLine(EndOfLine.LF); - editBuilder.replace(new Range(0, 0, textEditor.document.lineCount, 0), content); - }) - .then(() => textEditor); - } + if (content) { + await textEditor.edit((editBuilder) => { + editBuilder.setEndOfLine(EndOfLine.LF); + editBuilder.replace(new Range(0, 0, textEditor.document.lineCount, 0), content); + }); + } - return textEditor; - }); + return textEditor; } export function getDocument(): TextDocument | undefined { diff --git a/test/ModeNormal/g d.test.ts b/test/ModeNormal/g d.test.ts new file mode 100644 index 00000000..23f5937f --- /dev/null +++ b/test/ModeNormal/g d.test.ts @@ -0,0 +1,43 @@ +import * as BlackBox from '../Framework/BlackBox'; + +suite('Normal: g d', () => { + const testCases: BlackBox.TestCase[] = [ + { + language: 'javascript', + from: 'class C {\n x = 0;\n}\nconst c = new C();\n[]c.x = 1;', + inputs: 'g d', + to: 'class C {\n x = 0;\n}\nconst []c = new C();\nc.x = 1;', + }, + { + language: 'javascript', + from: 'class C {\n x = 0;\n}\nconst c = new C();\nc.[]x = 1;', + inputs: 'g d', + to: 'class C {\n []x = 0;\n}\nconst c = new C();\nc.x = 1;', + }, + ]; + + for (let i = 0; i < testCases.length; i++) { + BlackBox.run(testCases[i]); + } +}); + +suite('Normal: g D', () => { + const testCases: BlackBox.TestCase[] = [ + { + language: 'javascript', + from: 'class C {\n x = 0;\n}\nconst c = new C();\n[]c.x = 1;', + inputs: 'g D', + to: 'class []C {\n x = 0;\n}\nconst c = new C();\nc.x = 1;', + }, + { + language: 'javascript', + from: 'class C {\n x = 0;\n}\nconst c = new C();\nc.[]x = 1;', + inputs: 'g D', + to: 'class C {\n x = 0;\n}\nconst c = new C();\nc.[]x = 1;', + }, + ]; + + for (let i = 0; i < testCases.length; i++) { + BlackBox.run(testCases[i]); + } +}); diff --git a/test/index.ts b/test/index.ts index 6b6dbef9..a33bfb30 100644 --- a/test/index.ts +++ b/test/index.ts @@ -7,7 +7,7 @@ export function run(): Promise { const mocha = new Mocha({ ui: 'tdd', timeout: 2500, - retries: 2, + retries: 3, }); mocha.useColors(true);