diff --git a/src/core/commandRunner/CommandRunner.ts b/src/core/commandRunner/CommandRunner.ts index 7f0bc0e9bf..ff4e901cdf 100644 --- a/src/core/commandRunner/CommandRunner.ts +++ b/src/core/commandRunner/CommandRunner.ts @@ -2,6 +2,7 @@ import * as vscode from "vscode"; import { ActionType } from "../../actions/actions.types"; import { OutdatedExtensionError } from "../../errors"; import processTargets from "../../processTargets"; +import isTesting from "../../testUtil/isTesting"; import { Graph, ProcessedTargetsContext } from "../../typings/Types"; import { isString } from "../../util/type"; import { canonicalizeAndValidateCommand } from "../commandVersionUpgrades/canonicalizeAndValidateCommand"; @@ -146,7 +147,7 @@ export default class CommandRunner { const err = e as Error; if (err instanceof OutdatedExtensionError) { this.showUpdateExtensionErrorMessage(err); - } else { + } else if (!isTesting()) { vscode.window.showErrorMessage(err.message); } console.error(err.message); diff --git a/src/core/inferFullTargets.ts b/src/core/inferFullTargets.ts index 270177095f..8def1fb35c 100644 --- a/src/core/inferFullTargets.ts +++ b/src/core/inferFullTargets.ts @@ -1,8 +1,11 @@ import { + Mark, + Modifier, PartialListTargetDescriptor, PartialPrimitiveTargetDescriptor, PartialRangeTargetDescriptor, PartialTargetDescriptor, + PositionModifier, PrimitiveTargetDescriptor, RangeTargetDescriptor, TargetDescriptor, @@ -94,36 +97,30 @@ function inferPrimitiveTarget( }; } - const hasPosition = !!target.modifiers?.find( - (modifier) => modifier.type === "position" - ); + const ownPositionalModifier = getPositionalModifier(target); + const ownNonPositionalModifiers = getNonPositionalModifiers(target); // Position without a mark can be something like "take air past end of line" + // We will remove this case when we implement #736 const mark = target.mark ?? - (hasPosition ? getPreviousMark(previousTargets) : null) ?? { + (ownPositionalModifier == null + ? null + : getPreviousMark(previousTargets)) ?? { type: "cursor", }; - const previousModifiers = getPreviousModifiers(previousTargets); + const nonPositionalModifiers = + ownNonPositionalModifiers ?? + getPreviousNonPositionalModifiers(previousTargets) ?? + []; - const modifiers = target.modifiers ?? previousModifiers ?? []; + const positionalModifier = + ownPositionalModifier ?? getPreviousPositionalModifier(previousTargets); - // "bring line to after this" needs to infer line on second target - const modifierTypes = [ - ...new Set(modifiers.map((modifier) => modifier.type)), + const modifiers = [ + ...(positionalModifier == null ? [] : [positionalModifier]), + ...nonPositionalModifiers, ]; - if ( - previousModifiers != null && - modifierTypes.length === 1 && - modifierTypes[0] === "position" - ) { - const containingScopeModifier = previousModifiers.find( - (modifier) => modifier.type === "containingScope" - ); - if (containingScopeModifier != null) { - modifiers.push(containingScopeModifier); - } - } return { type: target.type, @@ -132,45 +129,107 @@ function inferPrimitiveTarget( }; } -function getPreviousMark(previousTargets: PartialTargetDescriptor[]) { - return getPreviousTarget( - previousTargets, - (target: PartialPrimitiveTargetDescriptor) => target.mark != null - )?.mark; +function getPositionalModifier( + target: PartialPrimitiveTargetDescriptor +): PositionModifier | undefined { + if (target.modifiers == null) { + return undefined; + } + + const positionModifierIndex = target.modifiers.findIndex( + (modifier) => modifier.type === "position" + ); + + if (positionModifierIndex > 0) { + throw Error("Position modifiers must be at the start of a modifier chain"); + } + + return positionModifierIndex === -1 + ? undefined + : (target.modifiers[positionModifierIndex] as PositionModifier); } -function getPreviousModifiers(previousTargets: PartialTargetDescriptor[]) { - return getPreviousTarget( +/** + * Return a list of non-positional modifiers on the given target. We return + * undefined if there are none. Note that we will never return an empty list; we + * will always return `undefined` if there are no non-positional modifiers. + * @param target The target from which to get the non-positional modifiers + * @returns A list of non-positional modifiers or `undefined` if there are none + */ +function getNonPositionalModifiers( + target: PartialPrimitiveTargetDescriptor +): Modifier[] | undefined { + const nonPositionalModifiers = target.modifiers?.filter( + (modifier) => modifier.type !== "position" + ); + return nonPositionalModifiers == null || nonPositionalModifiers.length === 0 + ? undefined + : nonPositionalModifiers; +} + +function getPreviousMark( + previousTargets: PartialTargetDescriptor[] +): Mark | undefined { + return getPreviousTargetAttribute( previousTargets, - (target: PartialPrimitiveTargetDescriptor) => target.modifiers != null - )?.modifiers; + (target: PartialPrimitiveTargetDescriptor) => target.mark + ); } -function getPreviousTarget( +function getPreviousNonPositionalModifiers( + previousTargets: PartialTargetDescriptor[] +): Modifier[] | undefined { + return getPreviousTargetAttribute(previousTargets, getNonPositionalModifiers); +} + +function getPreviousPositionalModifier( + previousTargets: PartialTargetDescriptor[] +): PositionModifier | undefined { + return getPreviousTargetAttribute(previousTargets, getPositionalModifier); +} + +/** + * Walks backward through the given targets and their descendants trying to find + * the first target for which the given attribute extractor returns a + * non-nullish value. Returns `undefined` if none could be found + * @param previousTargets The targets that precede the target we are trying to + * infer. We look in these targets and their descendants for the given attribute + * @param getAttribute The function used to extract the attribute from a + * primitive target + * @returns The extracted attribute or undefined if one could not be found + */ +function getPreviousTargetAttribute( previousTargets: PartialTargetDescriptor[], - useTarget: (target: PartialPrimitiveTargetDescriptor) => boolean -): PartialPrimitiveTargetDescriptor | null { + getAttribute: (target: PartialPrimitiveTargetDescriptor) => T | undefined +): T | undefined { // Search from back(last) to front(first) for (let i = previousTargets.length - 1; i > -1; --i) { const target = previousTargets[i]; switch (target.type) { - case "primitive": - if (useTarget(target)) { - return target; + case "primitive": { + const attributeValue = getAttribute(target); + if (attributeValue != null) { + return attributeValue; } break; - case "range": - if (useTarget(target.anchor)) { - return target.anchor; + } + case "range": { + const attributeValue = getAttribute(target.anchor); + if (attributeValue != null) { + return attributeValue; } break; + } case "list": - const result = getPreviousTarget(target.elements, useTarget); - if (result != null) { - return result; + const attributeValue = getPreviousTargetAttribute( + target.elements, + getAttribute + ); + if (attributeValue != null) { + return attributeValue; } break; } } - return null; + return undefined; } diff --git a/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToEndOfSecondCarWhaleAndEndOfWhaleTakeWhale.yml b/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToEndOfSecondCarWhaleAndEndOfJustWhaleTakeWhale.yml similarity index 84% rename from src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToEndOfSecondCarWhaleAndEndOfWhaleTakeWhale.yml rename to src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToEndOfSecondCarWhaleAndEndOfJustWhaleTakeWhale.yml index 2f1eb9460b..a71f9edab7 100644 --- a/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToEndOfSecondCarWhaleAndEndOfWhaleTakeWhale.yml +++ b/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToEndOfSecondCarWhaleAndEndOfJustWhaleTakeWhale.yml @@ -1,8 +1,7 @@ languageId: plaintext command: - version: 1 - spokenForm: bring point and harp to end of second car whale and end of whale take whale - action: replaceWithTarget + spokenForm: bring point and harp to end of second car whale and end of just whale take whale + version: 2 targets: - type: list elements: @@ -13,15 +12,22 @@ command: - type: list elements: - type: primitive - position: after - insideOutsideType: inside - selectionType: token - modifier: {type: subpiece, pieceType: character, anchor: 1, active: 1, excludeAnchor: false, excludeActive: false} mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: position, position: end} + - type: ordinalRange + scopeType: {type: character} + anchor: 1 + active: 1 + excludeAnchor: false + excludeActive: false - type: primitive - position: after - insideOutsideType: inside mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: position, position: end} + - {type: toRawSelection} + usePrePhraseSnapshot: false + action: {name: replaceWithTarget} marksToCheck: [default.w] initialState: documentContents: hello. world diff --git a/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToStartOfSecondCarWhaleAndStartOfWhaleTakeWhale.yml b/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToStartOfSecondCarWhaleAndStartOfJustWhaleTakeWhale.yml similarity index 85% rename from src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToStartOfSecondCarWhaleAndStartOfWhaleTakeWhale.yml rename to src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToStartOfSecondCarWhaleAndStartOfJustWhaleTakeWhale.yml index c800f11648..98685ec465 100644 --- a/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToStartOfSecondCarWhaleAndStartOfWhaleTakeWhale.yml +++ b/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndHarpToStartOfSecondCarWhaleAndStartOfJustWhaleTakeWhale.yml @@ -1,10 +1,9 @@ languageId: plaintext command: - version: 1 spokenForm: >- - bring point and harp to start of second car whale and start of whale take + bring point and harp to start of second car whale and start of just whale take whale - action: replaceWithTarget + version: 2 targets: - type: list elements: @@ -15,15 +14,22 @@ command: - type: list elements: - type: primitive - position: before - insideOutsideType: inside - selectionType: token - modifier: {type: subpiece, pieceType: character, anchor: 1, active: 1, excludeAnchor: false, excludeActive: false} mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: position, position: start} + - type: ordinalRange + scopeType: {type: character} + anchor: 1 + active: 1 + excludeAnchor: false + excludeActive: false - type: primitive - position: before - insideOutsideType: inside mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: position, position: start} + - {type: toRawSelection} + usePrePhraseSnapshot: false + action: {name: replaceWithTarget} marksToCheck: [default.w] initialState: documentContents: hello. world diff --git a/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndPointToEndOfSecondCarWhaleAndEndOfWhaleTakeWhale.yml b/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndPointToEndOfSecondCarWhaleAndEndOfJustWhaleTakeWhale.yml similarity index 83% rename from src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndPointToEndOfSecondCarWhaleAndEndOfWhaleTakeWhale.yml rename to src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndPointToEndOfSecondCarWhaleAndEndOfJustWhaleTakeWhale.yml index 6202f6bb19..5533531e76 100644 --- a/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndPointToEndOfSecondCarWhaleAndEndOfWhaleTakeWhale.yml +++ b/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndPointToEndOfSecondCarWhaleAndEndOfJustWhaleTakeWhale.yml @@ -1,8 +1,7 @@ languageId: plaintext command: - version: 1 - spokenForm: bring point and point to end of second car whale and end of whale take whale - action: replaceWithTarget + spokenForm: bring point and point to end of second car whale and end of just whale take whale + version: 2 targets: - type: list elements: @@ -13,15 +12,22 @@ command: - type: list elements: - type: primitive - position: after - insideOutsideType: inside - selectionType: token - modifier: {type: subpiece, pieceType: character, anchor: 1, active: 1, excludeAnchor: false, excludeActive: false} mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: position, position: end} + - type: ordinalRange + scopeType: {type: character} + anchor: 1 + active: 1 + excludeAnchor: false + excludeActive: false - type: primitive - position: after - insideOutsideType: inside mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: position, position: end} + - {type: toRawSelection} + usePrePhraseSnapshot: false + action: {name: replaceWithTarget} marksToCheck: [default.w] initialState: documentContents: hello. world diff --git a/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndPointToStartOfSecondCarWhaleAndStartOfWhaleTakeWhale.yml b/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndPointToStartOfSecondCarWhaleAndStartOfJustWhaleTakeWhale.yml similarity index 83% rename from src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndPointToStartOfSecondCarWhaleAndStartOfWhaleTakeWhale.yml rename to src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndPointToStartOfSecondCarWhaleAndStartOfJustWhaleTakeWhale.yml index 9e6aa159a5..bc88db91a8 100644 --- a/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndPointToStartOfSecondCarWhaleAndStartOfWhaleTakeWhale.yml +++ b/src/test/suite/fixtures/recorded/hatTokenMap/bringPointAndPointToStartOfSecondCarWhaleAndStartOfJustWhaleTakeWhale.yml @@ -1,10 +1,9 @@ languageId: plaintext command: - version: 1 spokenForm: >- - bring point and point to start of second car whale and start of whale take + bring point and point to start of second car whale and start of just whale take whale - action: replaceWithTarget + version: 2 targets: - type: list elements: @@ -15,15 +14,22 @@ command: - type: list elements: - type: primitive - position: before - insideOutsideType: inside - selectionType: token - modifier: {type: subpiece, pieceType: character, anchor: 1, active: 1, excludeAnchor: false, excludeActive: false} mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: position, position: start} + - type: ordinalRange + scopeType: {type: character} + anchor: 1 + active: 1 + excludeAnchor: false + excludeActive: false - type: primitive - position: before - insideOutsideType: inside mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: position, position: start} + - {type: toRawSelection} + usePrePhraseSnapshot: false + action: {name: replaceWithTarget} marksToCheck: [default.w] initialState: documentContents: hello. world diff --git a/src/test/suite/fixtures/recorded/hatTokenMap/moveFourthCarHarpPastSecondCarWhaleToEndOfWhaleTakeWhale.yml b/src/test/suite/fixtures/recorded/hatTokenMap/moveFourthCarHarpPastSecondCarWhaleToEndOfJustWhaleTakeWhale.yml similarity index 75% rename from src/test/suite/fixtures/recorded/hatTokenMap/moveFourthCarHarpPastSecondCarWhaleToEndOfWhaleTakeWhale.yml rename to src/test/suite/fixtures/recorded/hatTokenMap/moveFourthCarHarpPastSecondCarWhaleToEndOfJustWhaleTakeWhale.yml index 213464506d..c31575a50a 100644 --- a/src/test/suite/fixtures/recorded/hatTokenMap/moveFourthCarHarpPastSecondCarWhaleToEndOfWhaleTakeWhale.yml +++ b/src/test/suite/fixtures/recorded/hatTokenMap/moveFourthCarHarpPastSecondCarWhaleToEndOfJustWhaleTakeWhale.yml @@ -1,26 +1,38 @@ languageId: plaintext command: - version: 1 - spokenForm: move fourth car harp past second car whale to end of whale take whale - action: moveToTarget + spokenForm: move fourth car harp past second car whale to end of just whale take whale + version: 2 targets: - type: range - start: + anchor: type: primitive - selectionType: token - modifier: {type: subpiece, pieceType: character, anchor: 3, active: 3, excludeAnchor: false, excludeActive: false} mark: {type: decoratedSymbol, symbolColor: default, character: h} - end: + modifiers: + - type: ordinalRange + scopeType: {type: character} + anchor: 3 + active: 3 + excludeAnchor: false + excludeActive: false + active: type: primitive - selectionType: token - modifier: {type: subpiece, pieceType: character, anchor: 1, active: 1, excludeAnchor: false, excludeActive: false} mark: {type: decoratedSymbol, symbolColor: default, character: w} - excludeStart: false - excludeEnd: false + modifiers: + - type: ordinalRange + scopeType: {type: character} + anchor: 1 + active: 1 + excludeAnchor: false + excludeActive: false + excludeAnchor: false + excludeActive: false - type: primitive - position: after - insideOutsideType: inside mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: position, position: end} + - {type: toRawSelection} + usePrePhraseSnapshot: false + action: {name: moveToTarget} marksToCheck: [default.w] initialState: documentContents: hello world whatever diff --git a/src/test/suite/fixtures/recorded/inference/bringHarpAfterLineTrapAndBlockSpun.yml b/src/test/suite/fixtures/recorded/inference/bringHarpAfterLineTrapAndBlockSpun.yml new file mode 100644 index 0000000000..2cae3a03aa --- /dev/null +++ b/src/test/suite/fixtures/recorded/inference/bringHarpAfterLineTrapAndBlockSpun.yml @@ -0,0 +1,62 @@ +languageId: plaintext +command: + spokenForm: bring harp after line trap and block spun + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: h} + - type: list + elements: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: t} + modifiers: + - {type: position, position: after} + - type: containingScope + scopeType: {type: line} + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: s} + modifiers: + - type: containingScope + scopeType: {type: paragraph} + usePrePhraseSnapshot: true + action: {name: replaceWithTarget} +initialState: + documentContents: |- + hello + there + + testing + selections: + - anchor: {line: 3, character: 7} + active: {line: 3, character: 7} + marks: + default.h: + start: {line: 0, character: 0} + end: {line: 0, character: 5} + default.t: + start: {line: 1, character: 0} + end: {line: 1, character: 5} + default.s: + start: {line: 3, character: 0} + end: {line: 3, character: 7} +finalState: + documentContents: |- + hello + there + hello + + testing + + hello + selections: + - anchor: {line: 4, character: 7} + active: {line: 4, character: 7} + thatMark: + - anchor: {line: 2, character: 0} + active: {line: 2, character: 5} + - anchor: {line: 6, character: 0} + active: {line: 6, character: 5} + sourceMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 5} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: h}, modifiers: []}, {type: list, elements: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: t}, modifiers: [{type: position, position: after}, {type: containingScope, scopeType: {type: line}}]}, {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: s}, modifiers: [{type: position, position: after}, {type: containingScope, scopeType: {type: paragraph}}]}]}]