From 5eb402656e2d6f88fb2f8d81489a59fe920ed0f4 Mon Sep 17 00:00:00 2001 From: David Little Date: Thu, 19 Sep 2024 09:52:14 -0500 Subject: [PATCH] Tests / fixes for `moveBy` (#52) Adds tests to verify working unit motions, and simplifies and corrects the unit-motion logic. --- .eslintrc.json | 3 +- .gitignore | 2 +- .vscode/launch.json | 1 + .vscode/tasks.json | 4 +- debug-profile.code-profile | 3 + notes.md | 4 + npm-run.sh | 5 +- package-lock.json | 27 +- package.json | 7 +- src/web/activeMotions.ts | 7 +- src/web/unitMotions.ts | 316 +++++++++--------- ...impleMotion.ux.mts => moveByNumber.ux.mts} | 23 +- test/specs/moveByParagraph.ux.mts | 266 +++++++++++++++ test/specs/moveBySection.ux.mts | 314 +++++++++++++++++ test/specs/moveBySubword.ux.mts | 128 +++++++ test/specs/string.prototype.replaceall.d.ts | 7 + test/specs/utils.mts | 14 +- wdio.conf.mts | 5 +- 18 files changed, 950 insertions(+), 186 deletions(-) create mode 100644 debug-profile.code-profile create mode 100644 notes.md rename test/specs/{simpleMotion.ux.mts => moveByNumber.ux.mts} (50%) create mode 100644 test/specs/moveByParagraph.ux.mts create mode 100644 test/specs/moveBySection.ux.mts create mode 100644 test/specs/moveBySubword.ux.mts create mode 100644 test/specs/string.prototype.replaceall.d.ts diff --git a/.eslintrc.json b/.eslintrc.json index ea1b166..7f2d469 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -37,7 +37,8 @@ { "allowModules": [ "lodash", - "wdio-vscode-service" + "wdio-vscode-service", + "string.prototype.replaceall" ] } ], diff --git a/.gitignore b/.gitignore index 4397be6..20ff65a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ dist/* .DS_Store .cache/ .parcel-cache -wdio.conf.mts .wdio-vscode-service wdio.log coverage/** +.package.json diff --git a/.vscode/launch.json b/.vscode/launch.json index 0202d83..9ed1552 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -24,6 +24,7 @@ "debugWebWorkerHost": true, "request": "launch", "args": [ + "--profile=debug-profile", "--extensionDevelopmentPath=${workspaceFolder}", "--extensionDevelopmentKind=web" ], diff --git a/.vscode/tasks.json b/.vscode/tasks.json index b090f08..f10b04d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -6,7 +6,7 @@ { "label": "npm: compile-web", "type": "shell", - "command": "bash ./npm-run.sh compile", + "command": "bash ./npm-run.sh compile-web", "group": { "kind": "build", "isDefault": true @@ -19,7 +19,7 @@ { "label": "npm: watch-web", "type": "shell", - "command": "bash ./npm-run.sh watch", + "command": "bash ./npm-run.sh watch-web", "group": "build", "isBackground": true, "problemMatcher": [ diff --git a/debug-profile.code-profile b/debug-profile.code-profile new file mode 100644 index 0000000..b72a655 --- /dev/null +++ b/debug-profile.code-profile @@ -0,0 +1,3 @@ +{ + "name": "debug-profile" +} diff --git a/notes.md b/notes.md new file mode 100644 index 0000000..62e30e8 --- /dev/null +++ b/notes.md @@ -0,0 +1,4 @@ +Currently debugging issue with `resolveUnit` in `unitMotions` + +problem: when using start or end boundaries (instead of both), the selectWhole +skips every even unit diff --git a/npm-run.sh b/npm-run.sh index 1b64e98..36b21ff 100644 --- a/npm-run.sh +++ b/npm-run.sh @@ -1,5 +1,6 @@ #!/bin/bash -export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")" +NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # load nvm +nvm install nvm use -npm run $@ +npm run "$@" diff --git a/package-lock.json b/package-lock.json index 4bd2ce2..1e3ddf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "selection-utilities", - "version": "0.6.5", + "version": "0.6.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "selection-utilities", - "version": "0.6.5", + "version": "0.6.6", "dependencies": { "@wdio/spec-reporter": "^8.40.3", "ts-node": "^10.9.2" @@ -35,6 +35,7 @@ "nyc": "^17.0.0", "penv": "^0.2.0", "process": "^0.11.10", + "string.prototype.replaceall": "^1.0.10", "ts-loader": "^9.5.1", "typescript": "^5.4.3", "wdio-vscode-service": "^6.0.3", @@ -13631,6 +13632,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.replaceall": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.replaceall/-/string.prototype.replaceall-1.0.10.tgz", + "integrity": "sha512-PKLapcZUZmXUdfIM6rTTTMYOxaj4JiQrgl0SKEeCFug1CdMAuJq8hVZd4eek9yMXAW4ldGUq+TiZRtjLJRU96g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", diff --git a/package.json b/package.json index a21ed6b..df5f79f 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "publisher": "haberdashPI", "repository": "https://github.com/haberdashPI/vscode-selection-utilities", "description": "Kakaune-inspired collection of useful commands for manipulating selections.", - "version": "0.6.6", + "version": "0.6.7", "icon": "logo.png", "engines": { "vscode": "^1.92.0" @@ -46,6 +46,10 @@ "name": "word", "regex": "(_*[\\p{L}][_\\p{L}0-9]*)|(_+)|([0-9][0-9.]*)|((?<=[\\s\\r\\n])[^\\p{L}^\\s]+(?=[\\s\\r\\n]))" }, + { + "name": "number", + "regex": "[0-9]+" + }, { "name": "subword", "regex": "(_*[\\p{L}][0-9\\p{Ll}]+_*)|(_+)|(\\p{Lu}[\\p{Lu}0-9]+_*(?!\\p{Ll}))|(\\p{L})|([^\\p{L}^\\s^0-9])|([0-9][0-9.]*)" @@ -450,6 +454,7 @@ "nyc": "^17.0.0", "penv": "^0.2.0", "process": "^0.11.10", + "string.prototype.replaceall": "^1.0.10", "ts-loader": "^9.5.1", "typescript": "^5.4.3", "wdio-vscode-service": "^6.0.3", diff --git a/src/web/activeMotions.ts b/src/web/activeMotions.ts index 8b42aa6..2f8ac44 100644 --- a/src/web/activeMotions.ts +++ b/src/web/activeMotions.ts @@ -54,7 +54,9 @@ function revealActive(args: {at: 'top' | 'center' | 'bottom'} = {at: 'center'}) } } -function activePageMove(args: {dir?: 'up' | 'down'; count?: number, select?: boolean} = {}) { +function activePageMove( + args: {dir?: 'up' | 'down'; count?: number; select?: boolean} = {} +) { const editor = vscode.window.activeTextEditor; if (editor) { const heights = editor.visibleRanges.map(range => { @@ -69,7 +71,8 @@ function activePageMove(args: {dir?: 'up' | 'down'; count?: number, select?: boo editor.selections = editor.selections.map(sel => { const active = clampedLineTranslate(sel.active, ed.document, steps); let anchor = sel.anchor; - if (args.select === false) { // args.select === undefined defaults to true + if (args.select === false) { + // args.select === undefined defaults to true anchor = active; } return new vscode.Selection(anchor, active); diff --git a/src/web/unitMotions.ts b/src/web/unitMotions.ts index c6959fe..1edf5e5 100644 --- a/src/web/unitMotions.ts +++ b/src/web/unitMotions.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import {updateView} from './selectionMemory'; import {clampedLineTranslate, IHash} from './util'; +import {cloneDeep} from 'lodash'; interface MoveByArgs { unit?: string; @@ -132,9 +133,16 @@ export function registerUnitMotions(context: vscode.ExtensionContext) { } } +enum RangePosition { + First, + Middle, + Last, +} + interface Range { start?: vscode.Position; end?: vscode.Position; + position?: RangePosition; } function* singleLineUnitsForDoc( @@ -181,11 +189,12 @@ function* linesOf( } function* regexMatches( - matcher: RegExp, + matcher_: RegExp, line: string, forward: boolean, offset: number | undefined ): Generator<[number, number]> { + const matcher = cloneDeep(matcher_); matcher.lastIndex = 0; let match = matcher.exec(line); while (match) { @@ -210,20 +219,37 @@ function* mapIter(iter: Iterable, fn: (x: T) => R) { } type Unit = RegExp | MultiLineUnit | string[]; -function unitsForDoc( - document: vscode.TextDocument, +function* unitsForDoc( + doc: vscode.TextDocument, from: vscode.Position, unit: Unit, forward: boolean -) { +): Generator { + // 'units' to denote the start/end of a document (avoids edge cases downstream) + const first = new vscode.Position(0, 0); + const startUnit = { + start: first, + end: first, + position: RangePosition.First, + }; + + const last = lastPosition(doc); + const endUnit = { + start: last, + end: last, + position: RangePosition.Last, + }; + if (unit instanceof RegExp) { - return singleLineUnitsForDoc(document, from, unit, false, forward); + yield* singleLineUnitsForDoc(doc, from, unit, false, forward); } else { - return multiLineUnitsForDoc(document, from, unit as MultiLineUnit, forward); + yield* multiLineUnitsForDoc(doc, from, unit as MultiLineUnit, forward); } + + yield forward ? endUnit : startUnit; } -export function first(x: Iterable): T | undefined { +export function popFirst(x: Iterable): T | undefined { const itr: Iterator = x[Symbol.iterator](); const result: IteratorResult = itr.next(); if (!result.done) { @@ -264,11 +290,11 @@ function* singleRegexUnitsForDoc( unit: MultiLineUnit, forward: boolean ) { - let first_match: undefined | null | number = undefined; + let firstMatch: undefined | null | number = undefined; function closeMatch(line: number) { - if (first_match !== undefined) { - const startLine: null | number = forward ? first_match : line + 1; - const endLine: null | number = forward ? line - 1 : first_match; + if (firstMatch !== undefined) { + const startLine: null | number = forward ? firstMatch : line + 1; + const endLine: null | number = forward ? line - 1 : firstMatch; let endCol: null | number = null; if (endLine !== null) { endCol = document.lineAt(new vscode.Position(endLine, 0)).range.end @@ -292,14 +318,14 @@ function* singleRegexUnitsForDoc( // fall in the middle of a sequence of matching lines we // represent the fact that we dont' know where this sequence // starts using `null` - first_match = null; - } else if (first_match === undefined) { - first_match = line; + firstMatch = null; + } else if (firstMatch === undefined) { + firstMatch = line; } } else { const result = closeMatch(line); if (result !== undefined) { - first_match = undefined; + firstMatch = undefined; yield result; } } @@ -348,15 +374,15 @@ function* multiRegexUnitsForDoc( } function* multiLineUnitsForDoc( - document: vscode.TextDocument, + doc: vscode.TextDocument, from: vscode.Position, unit: MultiLineUnit, forward: boolean ): Generator { if (unit.regexs.length > 1) { - yield* multiRegexUnitsForDoc(document, from, unit, forward); + yield* multiRegexUnitsForDoc(doc, from, unit, forward); } else { - yield* singleRegexUnitsForDoc(document, from, unit, forward); + yield* singleRegexUnitsForDoc(doc, from, unit, forward); } } @@ -389,15 +415,20 @@ function toBoundary(args: {boundary?: string}) { } } -// this handles unit cleanup when looking for two boundaries, it handles two -// problesm: -// 1. when you look for units in one direction sometime you miss the start (or -// end) of a unit you're in the middle of (for multi-line units in particular). -// If we want to resolve the boundaries of a unit, we need to look -// backwards from the starting position. -// 2. when you get to the end of the document you need to treat the edge of the -// document as unit boundaries to make things work out cleanly for -// 'start-only' and `end-only` boundary resolution +function fuseRanges(a: Range, b: Range): Range { + if (!a?.start) { + return {start: b?.start, end: a?.end}; + } else if (!a?.end) { + return {start: a?.start, end: b?.end}; + } else { + return a; + } +} + +// this handles unit cleanup when looking for two boundaries: when you look for units in one +// direction sometime you miss the start (or end) of a unit you're in the middle of (for +// multi-line units in particular). If we want to resolve the boundaries of a unit, we need +// to look backwards from the starting position. function* resolveUnitBoundaries( resolve: Boundary | undefined, units: Generator, @@ -406,103 +437,81 @@ function* resolveUnitBoundaries( unit: Unit, forward: boolean ): Generator { - function* resolveUnit(first_unit: Range | undefined) { - const backwards = unitsForDoc(document, from, unit, !forward); - let foundUnit = false; - function* resolveHelper(back: Range) { - if (resolve === Boundary.Start) { - if (back.start) { - yield back; - if (first_unit?.start) { - yield first_unit; - } else { - const next = first(units); - if (next?.start) { - yield next; - } else { - yield { - start: forward ? lastPosition(document) : firstPosition(), - }; - } + let backwards: Generator; + function* resolveHelper(firstUnit: Range, back: Range): Generator { + if (resolve === Boundary.Start && (!firstUnit?.start || !forward)) { + if (forward) { + if (!firstUnit?.start) { + firstUnit = fuseRanges(firstUnit, back); + const moreBack = popFirst(backwards); + if (moreBack) { + back = moreBack; } - foundUnit = true; } - } else if (resolve === Boundary.End) { - if (back.end) { + if (back.position !== RangePosition.First) { yield back; - if (first_unit?.end) yield first_unit; - else { - const next = first(units); - if (next?.end) yield next; - else { - yield { - end: forward ? lastPosition(document) : firstPosition(), - }; - } + } + yield firstUnit; + } else { + if (!back?.start) { + firstUnit = fuseRanges(firstUnit, back); + const moreBack = popFirst(backwards); + if (moreBack) { + back = moreBack; } - foundUnit = true; } - } else if (back.start) { - if (back.end) { - foundUnit = true; + if (back.position !== RangePosition.First) { yield back; - } else if (first_unit?.end) { - foundUnit = true; - yield {start: back.start, end: first_unit.end}; - } - if (first_unit?.end && first_unit?.start) { - yield first_unit; - foundUnit = true; } + yield firstUnit; } - } - for (const back of backwards) { - yield* resolveHelper(back); - if (foundUnit) return; - } - - if (resolve !== Boundary.Both) { - if (forward) { - const first = firstPosition(); - yield* resolveHelper({start: first, end: first}); + } else if (resolve === Boundary.End && (!firstUnit?.end || forward)) { + if (!forward) { + if (!firstUnit?.end) { + firstUnit = fuseRanges(firstUnit, back); + const moreBack = popFirst(backwards); + if (moreBack) { + back = moreBack; + } + } + if (back.position !== RangePosition.Last) { + yield back; + } + yield firstUnit; } else { - const last = lastPosition(document); - yield* resolveHelper({start: last, end: last}); + if (!back?.end) { + firstUnit = fuseRanges(firstUnit, back); + const moreBack = popFirst(backwards); + if (moreBack) { + back = moreBack; + } + } + if (back.position !== RangePosition.Last) { + yield back; + } + yield firstUnit; } + } else if (resolve === Boundary.Both && (!firstUnit?.start || !firstUnit?.end)) { + yield fuseRanges(firstUnit, back); + } else { + yield firstUnit; } } - const first_unit = first(units); - if (first_unit) { - if (forward) { - if ( - resolve === Boundary.End - ? !first_unit.end || first_unit.end.isAfter(from) - : !first_unit.start || first_unit.start.isAfter(from) - ) { - yield* resolveUnit(first_unit); - } else yield first_unit; - } else if ( - resolve === Boundary.Start - ? !first_unit.start || first_unit.start.isBefore(from) - : !first_unit.end || first_unit.end.isBefore(from) - ) { - yield* resolveUnit(first_unit); - } else { - yield first_unit; - } + // TODO: this is where I'm currently stumped + const firstUnit = popFirst(units); + if (!firstUnit) { + return; } else { - yield* resolveUnit(first_unit); - } - yield* units; - if (resolve !== Boundary.Both) { - if (forward) { - const last = lastPosition(document); - yield {start: last, end: last}; - } else { - const first = firstPosition(); - yield {start: first, end: first}; + backwards = unitsForDoc(document, from, unit, !forward); + let back = popFirst(backwards); + while (boundsMatch(back, firstUnit)) { + back = popFirst(backwards); } + if (back) { + yield* resolveHelper(firstUnit, back); + } + yield* units; } } @@ -511,9 +520,6 @@ function lastPosition(document: vscode.TextDocument) { const endCol = document.lineAt(last).range.end.character; return new vscode.Position(last, endCol); } -function firstPosition() { - return new vscode.Position(0, 0); -} function moveBy(editor: vscode.TextEditor, args: MoveByArgs) { const unit = unitNameToRegex(editor, args.unit); @@ -549,44 +555,35 @@ function moveBy(editor: vscode.TextEditor, args: MoveByArgs) { if (x.end) yield withStart(x.end); } } - // treat the edge of the document as a boundary as well - if (forward) { - yield withStart(lastPosition(editor.document)); - } else { - yield withStart(firstPosition()); - } } // translate a sequence of units (regex start and stop boundaries) // to a sequence of selections: the selections surround a single // unit around from start-to-start, end-to-end or start-to-end - function* selectUnits(xs: Generator, forward: boolean) { - let last: vscode.Position | undefined | null = null; - let current: vscode.Position | undefined | null; - for (const x of xs) { - if (boundary !== Boundary.Both) { + function* selectUnits(xs: Generator) { + if (boundary === Boundary.Both) { + for (const x of xs) { + if (x.start && x.end) { + if (forward) { + yield new vscode.Selection(x.start, x.end); + } else { + yield new vscode.Selection(x.end, x.start); + } + } + } + } else { + let last: vscode.Position | undefined = undefined; + let current: vscode.Position | undefined = undefined; + for (const x of xs) { last = current; - current = boundary === Boundary.Start ? x.start : x.end; - if (current && last) { + if (boundary === Boundary.Start) { + current = x.start; + } else { + current = x.end; + } + if (current !== undefined && last !== undefined) { yield new vscode.Selection(last, current); } - } else { - if (!x.start || !x.end) throw new Error('Unexpected missing range bound'); - if (forward) yield new vscode.Selection(x.start, x.end); - else yield new vscode.Selection(x.start, x.end); - } - } - if (boundary !== Boundary.Both) { - if (forward) { - if (current === undefined) - throw new Error('Unexpected missing range bound'); - else if (current !== null) - yield new vscode.Selection(current, lastPosition(editor.document)); - } else { - if (current === undefined) - throw new Error('Unexpected missing range bound'); - else if (current !== null) - yield new vscode.Selection(current, firstPosition()); } } } @@ -608,26 +605,26 @@ function moveBy(editor: vscode.TextEditor, args: MoveByArgs) { unit, forward ); - selections = selectUnits(resolved, forward); + selections = selectUnits(resolved); } else { selections = selectBoundaries(units, holdSelect ? select.anchor : undefined); } - let count = 0; + let count = 0; // how many selections have we advanced? let lastsel; let startSel: vscode.Position | undefined = undefined; for (const sel of selections) { if (count > 0 || !boundsMatch(sel, select)) { - if ( - forward - ? sel.end.isAfter(select.active) - : sel.start.isBefore(select.active) - ) + if (forward && sel.end.isAfter(select.active)) { + count += 1; + } else if (!forward && sel.start.isBefore(select.active)) { count += 1; + } if (count === 1) { startSel = sel.anchor; } } if (count >= steps) { + // do we need the selection to start from the first selection region? if (!selectOneUnit && startSel && selectWholeUnit) { return new vscode.Selection(startSel, sel.active); } else { @@ -647,11 +644,22 @@ function moveBy(editor: vscode.TextEditor, args: MoveByArgs) { }; } -function boundsMatch(x: vscode.Selection, y: vscode.Selection) { - return ( - (x.start.isEqual(y.start) && x.end.isEqual(y.end)) || - (x.end.isEqual(y.start) && x.start.isEqual(y.end)) - ); +function boundsMatch(x: Range | undefined, y: Range | undefined) { + let startEqual = false; + if (x?.start === undefined && y?.start === undefined) { + startEqual = true; + } else if (x?.start && y?.start) { + startEqual = x?.start.isEqual(y.start); + } + + let endEqual = false; + if (x?.end === undefined && y?.end === undefined) { + endEqual = true; + } else if (x?.end && y?.end) { + endEqual = x?.end.isEqual(y.end); + } + + return startEqual && endEqual; } function narrowTo( diff --git a/test/specs/simpleMotion.ux.mts b/test/specs/moveByNumber.ux.mts similarity index 50% rename from test/specs/simpleMotion.ux.mts rename to test/specs/moveByNumber.ux.mts index 0a828de..25c94f8 100644 --- a/test/specs/simpleMotion.ux.mts +++ b/test/specs/moveByNumber.ux.mts @@ -3,34 +3,29 @@ import '@wdio/globals'; import 'wdio-vscode-service'; import {setupEditor, storeCoverageStats, waitUntilCursorUnmoving} from './utils.mts'; -import {TextEditor, Workbench} from 'wdio-vscode-service'; +import {TextEditor} from 'wdio-vscode-service'; -describe('Simple Motions', () => { +describe('Number Motion', () => { let editor: TextEditor; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - let workbench: Workbench; before(async () => { - editor = await setupEditor('foo bar biz baz'); - workbench = await browser.getWorkbench(); + editor = await setupEditor('foo bar biz 123 biz bar foo'); }); - it('Can move by word', async () => { + it('Can move by start+end away from number', async () => { await editor.moveCursor(1, 1); - await browser.executeWorkbench(vscode => { vscode.commands.executeCommand('selection-utilities.moveBy', { - unit: 'word', - selectWhole: true, + unit: 'number', value: 1, + selectWhole: true, boundary: 'both', }); }); - const [y, x] = await editor.getCoordinates(); - await waitUntilCursorUnmoving(editor, {y, x}); - expect(await editor.getSelectedText()).toEqual('foo'); + await waitUntilCursorUnmoving(editor); + expect(await editor.getSelectedText()).toEqual('123'); }); after(async () => { - await storeCoverageStats('simpleMotion'); + await storeCoverageStats('numberMotion'); }); }); diff --git a/test/specs/moveByParagraph.ux.mts b/test/specs/moveByParagraph.ux.mts new file mode 100644 index 0000000..d4fd554 --- /dev/null +++ b/test/specs/moveByParagraph.ux.mts @@ -0,0 +1,266 @@ +// start with just some basic tests to verify all is well + +import '@wdio/globals'; +import 'wdio-vscode-service'; +import { + cleanWhitespace, + setupEditor, + storeCoverageStats, + waitUntilCursorUnmoving, +} from './utils.mts'; +import {TextEditor} from 'wdio-vscode-service'; + +describe('Paragraph Motion', () => { + let editor: TextEditor; + before(async () => { + editor = await setupEditor(`aaaa + aaaa + + bbbb + bbbb + bbbb + + + cccc + cccc + `); + }); + + async function parMoveSelects(cmd: object, str: string) { + str = cleanWhitespace(str); + await browser.executeWorkbench((vscode, cmd) => { + const defaults = { + unit: 'paragraph', + value: 1, + }; + vscode.commands.executeCommand('selection-utilities.moveBy', { + ...defaults, + ...cmd, + }); + }, cmd); + await waitUntilCursorUnmoving(editor); + const result = await editor.getSelectedText(); + expect(cleanWhitespace(result)).toEqual(str); + } + + // TODO: add tests for position of active cursor location + + it('Can move by start+end', async () => { + await editor.moveCursor(1, 1); + + await parMoveSelects( + {selectWhole: true, boundary: 'both'}, + `aaaa + aaaa` + ); + }); + + it('Can move by start+end from middle', async () => { + await editor.moveCursor(2, 1); + await parMoveSelects( + {selectWhole: true, boundary: 'both'}, + `aaaa + aaaa` + ); + }); + + it('Can move by start', async () => { + await editor.moveCursor(1, 1); + + await parMoveSelects( + {selectWhole: true, boundary: 'start'}, + `aaaa + aaaa + + ` + ); + await parMoveSelects( + {selectWhole: true, boundary: 'start'}, + `bbbb + bbbb + bbbb + + + ` + ); + }); + + it('Can move by end', async () => { + await editor.moveCursor(1, 1); + + await parMoveSelects( + {selectWhole: true, boundary: 'end'}, + `aaaa + aaaa` + ); + await parMoveSelects( + {selectWhole: true, boundary: 'end'}, + ` + + bbbb + bbbb + bbbb` + ); + }); + + it('Can move backwards by start', async () => { + await editor.moveCursor(6, 1); + + await parMoveSelects( + {selectWhole: true, boundary: 'start', value: -1}, + `bbbb + bbbb + bbbb + + + ` + ); + }); + + it('Can move backwards by end', async () => { + await editor.moveCursor(6, 1); + + await parMoveSelects( + {selectWhole: true, boundary: 'end', value: -1}, + ` + + bbbb + bbbb + bbbb` + ); + }); + + it('Can move backwards by start+end', async () => { + await editor.moveCursor(6, 1); + + await parMoveSelects( + {selectWhole: true, boundary: 'both', value: -1}, + `bbbb + bbbb + bbbb` + ); + }); + + it('Can extend forward by start', async () => { + await editor.moveCursor(2, 1); + + await parMoveSelects( + {select: true, boundary: 'start'}, + `aaaa + + ` + ); + await parMoveSelects( + {select: true, boundary: 'start'}, + `aaaa + + bbbb + bbbb + bbbb + + + ` + ); + }); + + it('Can extend forward by end', async () => { + await editor.moveCursor(2, 1); + + await parMoveSelects({select: true, boundary: 'end'}, 'aaaa'); + await parMoveSelects( + {select: true, boundary: 'end'}, + `aaaa + + bbbb + bbbb + bbbb` + ); + }); + + it('Can extend bakcwards by start', async () => { + await editor.moveCursor(6, 1); + + await parMoveSelects( + {select: true, boundary: 'start', value: -1}, + `bbbb + bbbb + ` + ); + await parMoveSelects( + {select: true, boundary: 'start', value: -1}, + `aaaa + aaaa + + bbbb + bbbb + ` + ); + }); + + it('Can extend bakcwards by end', async () => { + await editor.moveCursor(6, 1); + + await parMoveSelects( + {select: true, boundary: 'end', value: -1}, + ` + + bbbb + bbbb + ` + ); + await parMoveSelects( + {select: true, boundary: 'end', value: -1}, + `aaaa + aaaa + + bbbb + bbbb + ` + ); + }); + + it('Can extent to "start" at file end', async () => { + await editor.moveCursor(9, 1); + + await parMoveSelects( + {select: true, boundary: 'start', value: 1}, + `cccc + cccc + ` + ); + }); + + it('Can extent to "end" at file start', async () => { + await editor.moveCursor(2, 1); + + await parMoveSelects( + {select: true, boundary: 'end', value: -1}, + `aaaa + ` + ); + }); + + it('Can extent to "end" at file end', async () => { + await editor.moveCursor(10, 5); + + await parMoveSelects( + {select: true, boundary: 'end', value: 1}, + ` + ` + ); + }); + + it('Can extent to "start" at file start', async () => { + await editor.moveCursor(2, 1); + + await parMoveSelects( + {select: true, boundary: 'start', value: -1}, + `aaaa + ` + ); + }); + + after(async () => { + await storeCoverageStats('paragraphMotion'); + }); +}); diff --git a/test/specs/moveBySection.ux.mts b/test/specs/moveBySection.ux.mts new file mode 100644 index 0000000..d068e02 --- /dev/null +++ b/test/specs/moveBySection.ux.mts @@ -0,0 +1,314 @@ +// start with just some basic tests to verify all is well + +import '@wdio/globals'; +import 'wdio-vscode-service'; +import { + cleanWhitespace, + setupEditor, + storeCoverageStats, + waitUntilCursorUnmoving, +} from './utils.mts'; +import {TextEditor} from 'wdio-vscode-service'; + +describe('Section Motion', () => { + let editor: TextEditor; + before(async () => { + editor = await setupEditor(`# A + # -------------------- + + joebob + bizzle + + # B + # -------------------- + + billybob + bim + + # A.2 + # -------------------- + + wizard + bizard + milo + philo + dough + + + `); + }); + + async function parMoveSelects(cmd: object, str: string) { + str = cleanWhitespace(str); + await browser.executeWorkbench((vscode, cmd) => { + const defaults = { + unit: 'subsection', + value: 1, + }; + vscode.commands.executeCommand('selection-utilities.moveBy', { + ...defaults, + ...cmd, + }); + }, cmd); + await waitUntilCursorUnmoving(editor); + const result = await editor.getSelectedText(); + expect(cleanWhitespace(result)).toEqual(str); + } + + it('Can move by start+end', async () => { + await editor.moveCursor(1, 1); + + await parMoveSelects( + {selectWhole: true, boundary: 'both'}, + `# A + # --------------------` + ); + }); + + it('Can move by start+end from middle', async () => { + await editor.moveCursor(2, 3); + await parMoveSelects( + {selectWhole: true, boundary: 'both'}, + `# A + # --------------------` + ); + }); + + it('Can move by start', async () => { + await editor.moveCursor(1, 1); + + await parMoveSelects( + {selectWhole: true, boundary: 'start'}, + `# A + # -------------------- + + joebob + bizzle + + ` + ); + await parMoveSelects( + {selectWhole: true, boundary: 'start'}, + `# B + # -------------------- + + billybob + bim + + ` + ); + }); + + it('Can move by end', async () => { + await editor.moveCursor(1, 1); + + await parMoveSelects( + {selectWhole: true, boundary: 'end'}, + `# A + # --------------------` + ); + await parMoveSelects( + {selectWhole: true, boundary: 'end'}, + ` + + joebob + bizzle + + # B + # --------------------` + ); + }); + + it('Can move backwards by start', async () => { + await editor.moveCursor(9, 1); + + await parMoveSelects( + {selectWhole: true, boundary: 'start', value: -1}, + `# B + # -------------------- + + billybob + bim + + ` + ); + }); + + it('Can move backwards by end', async () => { + await editor.moveCursor(9, 1); + + await parMoveSelects( + {selectWhole: true, boundary: 'end', value: -1}, + ` + + joebob + bizzle + + # B + # --------------------` + ); + }); + + it('Can move backwards by start+end', async () => { + await editor.moveCursor(9, 1); + + await parMoveSelects( + {selectWhole: true, boundary: 'both', value: -1}, + `# B + # --------------------` + ); + }); + + it('Can extend forward by start', async () => { + await editor.moveCursor(2, 1); + + await parMoveSelects( + {select: true, boundary: 'start'}, + `# -------------------- + + joebob + bizzle + + ` + ); + await parMoveSelects( + {select: true, boundary: 'start'}, + `# -------------------- + + joebob + bizzle + + # B + # -------------------- + + billybob + bim + + ` + ); + }); + + it('Can extend forward by end', async () => { + await editor.moveCursor(2, 1); + + await parMoveSelects({select: true, boundary: 'end'}, '# --------------------'); + await parMoveSelects( + {select: true, boundary: 'end'}, + `# -------------------- + + joebob + bizzle + + # B + # --------------------` + ); + }); + + it('Can extend bakcwards by start', async () => { + await editor.moveCursor(11, 1); + + await parMoveSelects( + {select: true, boundary: 'start', value: -1}, + `# B + # -------------------- + + billybob + ` + ); + await parMoveSelects( + {select: true, boundary: 'start', value: -1}, + `# A + # -------------------- + + joebob + bizzle + + # B + # -------------------- + + billybob + ` + ); + }); + + it('Can extend backwards by end', async () => { + await editor.moveCursor(11, 1); + + await parMoveSelects( + {select: true, boundary: 'end', value: -1}, + ` + + billybob + ` + ); + await parMoveSelects( + {select: true, boundary: 'end', value: -1}, + ` + + joebob + bizzle + + # B + # -------------------- + + billybob + ` + ); + }); + + it('Can extend to "start" at file end', async () => { + await editor.moveCursor(16, 1); + + await parMoveSelects( + {select: true, boundary: 'start', value: 1}, + `wizard + bizard + milo + philo + dough + + + ` + ); + }); + + it('Can extend to "end" at file start', async () => { + await editor.moveCursor(2, 1); + + await parMoveSelects( + {select: true, boundary: 'end', value: -1}, + `# A + ` + ); + }); + + it('Can extend to "end" at file end', async () => { + await editor.moveCursor(16, 1); + + await parMoveSelects( + {select: true, boundary: 'end', value: 1}, + `wizard + bizard + milo + philo + dough + + + ` + ); + }); + + it('Can extend to "start" at file start', async () => { + await editor.moveCursor(2, 1); + + await parMoveSelects( + {select: true, boundary: 'start', value: -1}, + `# A + ` + ); + }); + + after(async () => { + await storeCoverageStats('sectionMotion'); + }); +}); diff --git a/test/specs/moveBySubword.ux.mts b/test/specs/moveBySubword.ux.mts new file mode 100644 index 0000000..3c9b8a1 --- /dev/null +++ b/test/specs/moveBySubword.ux.mts @@ -0,0 +1,128 @@ +// start with just some basic tests to verify all is well + +import '@wdio/globals'; +import 'wdio-vscode-service'; +import {setupEditor, storeCoverageStats, waitUntilCursorUnmoving} from './utils.mts'; +import {TextEditor} from 'wdio-vscode-service'; + +describe('Subword Motion', () => { + let editor: TextEditor; + before(async () => { + editor = await setupEditor('foo bar biz baz snake_case_ident'); + }); + + async function wordMoveSelects(cmd: object, str: string) { + await browser.executeWorkbench((vscode, cmd) => { + const defaults = { + unit: 'subword', + value: 1, + }; + vscode.commands.executeCommand('selection-utilities.moveBy', { + ...defaults, + ...cmd, + }); + }, cmd); + await waitUntilCursorUnmoving(editor); + expect(await editor.getSelectedText()).toEqual(str); + } + + it('Can move by start+end', async () => { + await editor.moveCursor(1, 1); + + await wordMoveSelects({selectWhole: true, boundary: 'both'}, 'foo'); + await wordMoveSelects({selectWhole: true, boundary: 'both'}, 'bar'); + }); + + it('Can move by start+end from middle', async () => { + await editor.moveCursor(1, 2); + await wordMoveSelects({selectWhole: true, boundary: 'both'}, 'foo'); + }); + + it('Can move by start', async () => { + await editor.moveCursor(1, 1); + + await wordMoveSelects({selectWhole: true, boundary: 'start'}, 'foo '); + await wordMoveSelects({selectWhole: true, boundary: 'start'}, 'bar '); + }); + + it('Can move by end', async () => { + await editor.moveCursor(1, 1); + + await wordMoveSelects({selectWhole: true, boundary: 'end'}, 'foo'); + await wordMoveSelects({selectWhole: true, boundary: 'end'}, ' bar'); + }); + + it('Can move backwards by start', async () => { + await editor.moveCursor(1, 20); + + await wordMoveSelects({selectWhole: true, boundary: 'start', value: -1}, 'snake_'); + }); + + it('Can move backwards by end', async () => { + await editor.moveCursor(1, 20); + + await wordMoveSelects({selectWhole: true, boundary: 'end', value: -1}, ' snake_'); + }); + + it('Can move backwards by start+end', async () => { + await editor.moveCursor(1, 20); + + await wordMoveSelects({selectWhole: true, boundary: 'both', value: -1}, 'snake_'); + }); + + it('Can extend forward by start', async () => { + await editor.moveCursor(1, 2); + + await wordMoveSelects({select: true, boundary: 'start'}, 'oo '); + await wordMoveSelects({select: true, boundary: 'start'}, 'oo bar '); + }); + + it('Can extend forward by end', async () => { + await editor.moveCursor(1, 2); + + await wordMoveSelects({select: true, boundary: 'end'}, 'oo'); + await wordMoveSelects({select: true, boundary: 'end'}, 'oo bar'); + }); + + it('Can extend bakcwards by start', async () => { + await editor.moveCursor(1, 7); + + await wordMoveSelects({select: true, boundary: 'start', value: -1}, 'ba'); + await wordMoveSelects({select: true, boundary: 'start', value: -1}, 'foo ba'); + }); + + it('Can extend bakcwards by end', async () => { + await editor.moveCursor(1, 7); + + await wordMoveSelects({select: true, boundary: 'end', value: -1}, ' ba'); + await wordMoveSelects({select: true, boundary: 'end', value: -1}, 'foo ba'); + }); + + it('Can extend to "start" at file end', async () => { + await editor.moveCursor(1, 29); + + await wordMoveSelects({select: true, boundary: 'start', value: 1}, 'dent'); + }); + + it('Can extend to "end" at file start', async () => { + await editor.moveCursor(1, 3); + + await wordMoveSelects({select: true, boundary: 'end', value: -1}, 'fo'); + }); + + it('Can extend to "end" at file end', async () => { + await editor.moveCursor(1, 29); + + await wordMoveSelects({select: true, boundary: 'end', value: 1}, 'dent'); + }); + + it('Can extend to "start" at file start', async () => { + await editor.moveCursor(1, 3); + + await wordMoveSelects({select: true, boundary: 'start', value: -1}, 'fo'); + }); + + after(async () => { + await storeCoverageStats('subwordMotion'); + }); +}); diff --git a/test/specs/string.prototype.replaceall.d.ts b/test/specs/string.prototype.replaceall.d.ts new file mode 100644 index 0000000..249ac54 --- /dev/null +++ b/test/specs/string.prototype.replaceall.d.ts @@ -0,0 +1,7 @@ +declare module 'string.prototype.replaceall' { + export default function replaceAll( + str: string, + regex: RegExp, + replacement: string + ): string; +} diff --git a/test/specs/utils.mts b/test/specs/utils.mts index c69664c..c1a0116 100644 --- a/test/specs/utils.mts +++ b/test/specs/utils.mts @@ -2,6 +2,7 @@ import {browser, expect} from '@wdio/globals'; import 'wdio-vscode-service'; import {Key} from 'webdriverio'; import {InputBox, TextEditor, Workbench, sleep} from 'wdio-vscode-service'; +import replaceAll from 'string.prototype.replaceall'; import loadash from 'lodash'; const {isEqual} = loadash; import * as fs from 'fs'; @@ -67,7 +68,14 @@ export async function clearNotifications(workbench: Workbench) { } } +export function cleanWhitespace(str: string) { + let result = replaceAll(str, /^[^\n\r\S]+(?=[\S\n\r])/gm, ''); + result = replaceAll(result, /^[^\n\r\S]+$/gm, ''); + return result; +} + export async function setupEditor(str: string) { + str = cleanWhitespace(str); const workbench = await browser.getWorkbench(); // clear any older notificatoins @@ -82,7 +90,7 @@ export async function setupEditor(str: string) { // NOTE: setting editor text is somewhat flakey, so we verify that it worked console.log('[DEBUG]: setting text to: ' + str.slice(0, 200) + '...'); await editor.setText(str); - await sleep(300); + await sleep(1000); await waitUntilCursorUnmoving(editor); const text = await editor.getText(); @@ -92,10 +100,6 @@ export async function setupEditor(str: string) { console.log('[DEBUG]: Focusing editor'); await editor.moveCursor(1, 1); - // NOTE: I often see flaky tests at the very start of a spec. My first guess is we need - // to give some time for the editor to finish loading stuff asynchronously from - // `setupEditor` before it is responsive again. - await sleep(1000); return editor; } diff --git a/wdio.conf.mts b/wdio.conf.mts index 1805f90..5b48d38 100644 --- a/wdio.conf.mts +++ b/wdio.conf.mts @@ -75,7 +75,7 @@ export const config: Options.Testrunner = { // and 30 processes will get spawned. The property handles how many capabilities // from the same test should run tests. // - maxInstances: 10, + maxInstances: 1, // // If you have trouble getting all important capabilities together, check out the // Sauce Labs platform configurator - a great tool to configure your capabilities: @@ -106,7 +106,8 @@ export const config: Options.Testrunner = { // Define all options that are relevant for the WebdriverIO instance here // // Level of logging verbosity: trace | debug | info | warn | error | silent - logLevel: 'info', + logLevel: process.env.COVERAGE ? 'warn' : 'info', + // // Set specific log levels per logger // loggers: