diff --git a/src/actions/commands/actions.ts b/src/actions/commands/actions.ts
index b1c4e84eccd..7ecd047adaa 100644
--- a/src/actions/commands/actions.ts
+++ b/src/actions/commands/actions.ts
@@ -3817,7 +3817,7 @@ class ActionChangeChar extends BaseCommand {
public couldActionApply(vimState: VimState, keysPressed: string[]): boolean {
return (
- super.doesActionApply(vimState, keysPressed) &&
+ super.couldActionApply(vimState, keysPressed) &&
!configuration.sneak &&
!vimState.recordedState.operator
);
diff --git a/src/actions/motion.ts b/src/actions/motion.ts
index 2bce940933a..909810c2723 100644
--- a/src/actions/motion.ts
+++ b/src/actions/motion.ts
@@ -14,6 +14,7 @@ import { BaseAction } from './base';
import { RegisterAction } from './base';
import { ChangeOperator, DeleteOperator, YankOperator } from './operator';
import { shouldWrapKey } from './wrapping';
+import { RecordedState } from '../state/recordedState';
export function isIMovement(o: IMovement | Position): o is IMovement {
return (o as IMovement).start !== undefined && (o as IMovement).stop !== undefined;
@@ -39,6 +40,11 @@ export interface IMovement {
registerMode?: RegisterMode;
}
+enum SelectionType {
+ Concatenating, // selections that concatenate repeated movements
+ Expanding, // selections that expand the start and end of the previous selection
+}
+
/**
* A movement is something like 'h', 'k', 'w', 'b', 'gg', etc.
*/
@@ -64,6 +70,10 @@ export abstract class BaseMovement extends BaseAction {
*/
public setsDesiredColumnToEOL = false;
+ protected minCount = 1;
+ protected maxCount = 99999;
+ protected selectionType = SelectionType.Concatenating;
+
constructor(keysPressed?: string[], isRepeat?: boolean) {
super();
@@ -111,51 +121,74 @@ export abstract class BaseMovement extends BaseAction {
): Promise {
let recordedState = vimState.recordedState;
let result: Position | IMovement = new Position(0, 0); // bogus init to satisfy typechecker
+ let prevResult: IMovement | undefined = undefined;
+ let firstMovementStart: Position = new Position(position.line, position.character);
- if (count < 1) {
- count = 1;
- } else if (count > 99999) {
- count = 99999;
- }
+ count = this.clampCount(count);
for (let i = 0; i < count; i++) {
const firstIteration = i === 0;
const lastIteration = i === count - 1;
- const temporaryResult =
- recordedState.operator && lastIteration
- ? await this.execActionForOperator(position, vimState)
- : await this.execAction(position, vimState);
-
- if (temporaryResult instanceof Position) {
- result = temporaryResult;
- position = temporaryResult;
- } else if (isIMovement(temporaryResult)) {
- if (result instanceof Position) {
- result = {
- start: new Position(0, 0),
- stop: new Position(0, 0),
- failed: false,
- };
- }
+ result = await this.createMovementResult(position, vimState, recordedState, lastIteration);
- result.failed = result.failed || temporaryResult.failed;
-
- if (firstIteration) {
- (result as IMovement).start = temporaryResult.start;
+ if (result instanceof Position) {
+ position = result;
+ } else if (isIMovement(result)) {
+ if (prevResult && result.failed) {
+ return prevResult;
}
- if (lastIteration) {
- (result as IMovement).stop = temporaryResult.stop;
- } else {
- position = temporaryResult.stop.getRightThroughLineBreaks();
+ if (firstIteration) {
+ firstMovementStart = new Position(result.start.line, result.start.character);
}
- result.registerMode = temporaryResult.registerMode;
+ position = this.adjustPosition(position, result, lastIteration);
+ prevResult = result;
}
}
+ if (this.selectionType === SelectionType.Concatenating && isIMovement(result)) {
+ result.start = firstMovementStart;
+ }
+
+ return result;
+ }
+
+ protected clampCount(count: number) {
+ count = Math.max(count, this.minCount);
+ count = Math.min(count, this.maxCount);
+ return count;
+ }
+
+ protected async createMovementResult(
+ position: Position,
+ vimState: VimState,
+ recordedState: RecordedState,
+ lastIteration: boolean
+ ): Promise {
+ const result =
+ recordedState.operator && lastIteration
+ ? await this.execActionForOperator(position, vimState)
+ : await this.execAction(position, vimState);
return result;
}
+ protected adjustPosition(position: Position, result: IMovement, lastIteration: boolean) {
+ if (!lastIteration) {
+ position = result.stop.getRightThroughLineBreaks();
+ }
+ return position;
+ }
+}
+
+export abstract class ExpandingSelection extends BaseMovement {
+ protected selectionType = SelectionType.Expanding;
+
+ protected adjustPosition(position: Position, result: IMovement, lastIteration: boolean) {
+ if (!lastIteration) {
+ position = result.stop;
+ }
+ return position;
+ }
}
abstract class MoveByScreenLine extends BaseMovement {
@@ -1338,14 +1371,14 @@ class MoveToMatchingBracket extends BaseMovement {
if (PairMatcher.pairings[text[i]]) {
// We found an opening char, now move to the matching closing char
const openPosition = new Position(position.line, i);
- return PairMatcher.nextPairedChar(openPosition, text[i], true) || failure;
+ return PairMatcher.nextPairedChar(openPosition, text[i]) || failure;
}
}
return failure;
}
- return PairMatcher.nextPairedChar(position, charToMatch, true) || failure;
+ return PairMatcher.nextPairedChar(position, charToMatch) || failure;
}
public async execActionForOperator(
@@ -1401,19 +1434,39 @@ class MoveToMatchingBracket extends BaseMovement {
}
}
-export abstract class MoveInsideCharacter extends BaseMovement {
+export abstract class MoveInsideCharacter extends ExpandingSelection {
modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock];
protected charToMatch: string;
protected includeSurrounding = false;
public async execAction(position: Position, vimState: VimState): Promise {
- const failure = { start: position, stop: position, failed: true };
- const text = TextEditor.getLineAt(position).text;
const closingChar = PairMatcher.pairings[this.charToMatch].match;
- const closedMatch = text[position.character] === closingChar;
+ let cursorStartPos = new Position(
+ vimState.cursorStartPosition.line,
+ vimState.cursorStartPosition.character
+ );
+ // maintain current selection on failure
+ const failure = { start: cursorStartPos, stop: position, failed: true };
+ // when matching inside content of a pair, search for the next pair if
+ // the inner content is already selected in full
+ if (!this.includeSurrounding) {
+ const adjacentPosLeft = cursorStartPos.getLeftThroughLineBreaks();
+ let adjacentPosRight = position.getRightThroughLineBreaks();
+ if (vimState.recordedState.operator) {
+ adjacentPosRight = adjacentPosRight.getLeftThroughLineBreaks();
+ }
+ const adjacentCharLeft = TextEditor.getCharAt(adjacentPosLeft);
+ const adjacentCharRight = TextEditor.getCharAt(adjacentPosRight);
+ if (adjacentCharLeft === this.charToMatch && adjacentCharRight === closingChar) {
+ cursorStartPos = adjacentPosLeft;
+ vimState.cursorStartPosition = adjacentPosLeft;
+ position = adjacentPosRight;
+ vimState.cursorPosition = adjacentPosRight;
+ }
+ }
// First, search backwards for the opening character of the sequence
- let startPos = PairMatcher.nextPairedChar(position, closingChar, closedMatch);
+ let startPos = PairMatcher.nextPairedChar(cursorStartPos, closingChar, vimState);
if (startPos === undefined) {
return failure;
}
@@ -1426,7 +1479,8 @@ export abstract class MoveInsideCharacter extends BaseMovement {
startPlusOne = new Position(startPos.line, startPos.character + 1);
}
- let endPos = PairMatcher.nextPairedChar(startPlusOne, this.charToMatch, false);
+ let endPos = PairMatcher.nextPairedChar(position, this.charToMatch, vimState);
+
if (endPos === undefined) {
return failure;
}
@@ -1452,26 +1506,13 @@ export abstract class MoveInsideCharacter extends BaseMovement {
vimState.recordedState.operatorPositionDiff = startPos.subtract(position);
}
+ vimState.cursorStartPosition = startPos;
return {
start: startPos,
stop: endPos,
diff: new PositionDiff(0, startPos === position ? 1 : 0),
};
}
-
- public async execActionForOperator(
- position: Position,
- vimState: VimState
- ): Promise {
- const result = await this.execAction(position, vimState);
- if (isIMovement(result)) {
- if (result.failed) {
- vimState.recordedState.hasRunOperator = false;
- vimState.recordedState.actionsRun = [];
- }
- }
- return result;
- }
}
@RegisterAction
@@ -1707,11 +1748,7 @@ class MoveToUnclosedRoundBracketBackward extends MoveToMatchingBracket {
public async execAction(position: Position, vimState: VimState): Promise {
const failure = { start: position, stop: position, failed: true };
const charToMatch = ')';
- const result = PairMatcher.nextPairedChar(
- position.getLeftThroughLineBreaks(),
- charToMatch,
- false
- );
+ const result = PairMatcher.nextPairedChar(position, charToMatch);
if (!result) {
return failure;
@@ -1727,11 +1764,7 @@ class MoveToUnclosedRoundBracketForward extends MoveToMatchingBracket {
public async execAction(position: Position, vimState: VimState): Promise {
const failure = { start: position, stop: position, failed: true };
const charToMatch = '(';
- const result = PairMatcher.nextPairedChar(
- position.getRightThroughLineBreaks(),
- charToMatch,
- false
- );
+ const result = PairMatcher.nextPairedChar(position, charToMatch);
if (!result) {
return failure;
@@ -1756,11 +1789,7 @@ class MoveToUnclosedCurlyBracketBackward extends MoveToMatchingBracket {
public async execAction(position: Position, vimState: VimState): Promise {
const failure = { start: position, stop: position, failed: true };
const charToMatch = '}';
- const result = PairMatcher.nextPairedChar(
- position.getLeftThroughLineBreaks(),
- charToMatch,
- false
- );
+ const result = PairMatcher.nextPairedChar(position, charToMatch);
if (!result) {
return failure;
@@ -1776,11 +1805,7 @@ class MoveToUnclosedCurlyBracketForward extends MoveToMatchingBracket {
public async execAction(position: Position, vimState: VimState): Promise {
const failure = { start: position, stop: position, failed: true };
const charToMatch = '{';
- const result = PairMatcher.nextPairedChar(
- position.getRightThroughLineBreaks(),
- charToMatch,
- false
- );
+ const result = PairMatcher.nextPairedChar(position, charToMatch);
if (!result) {
return failure;
@@ -1798,27 +1823,37 @@ class MoveToUnclosedCurlyBracketForward extends MoveToMatchingBracket {
}
}
-abstract class MoveTagMatch extends BaseMovement {
+abstract class MoveTagMatch extends ExpandingSelection {
modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualBlock];
protected includeTag = false;
public async execAction(position: Position, vimState: VimState): Promise {
const editorText = TextEditor.getText();
const offset = TextEditor.getOffsetAt(position);
- const tagMatcher = new TagMatcher(editorText, offset);
+ const tagMatcher = new TagMatcher(editorText, offset, vimState);
+ const cursorStartPos = new Position(
+ vimState.cursorStartPosition.line,
+ vimState.cursorStartPosition.character
+ );
const start = tagMatcher.findOpening(this.includeTag);
const end = tagMatcher.findClosing(this.includeTag);
if (start === undefined || end === undefined) {
return {
- start: position,
+ start: cursorStartPos,
stop: position,
failed: true,
};
}
- let startPosition = start ? TextEditor.getPositionAt(start) : position;
- let endPosition = end ? TextEditor.getPositionAt(end) : position;
+ let startPosition = start >= 0 ? TextEditor.getPositionAt(start) : cursorStartPos;
+ let endPosition = end >= 0 ? TextEditor.getPositionAt(end) : position;
+ if (
+ vimState.currentMode === ModeName.Visual ||
+ vimState.currentMode === ModeName.SurroundInputMode
+ ) {
+ endPosition = endPosition.getLeftThroughLineBreaks();
+ }
if (position.isAfter(endPosition)) {
vimState.recordedState.transformations.push({
@@ -1831,37 +1866,22 @@ abstract class MoveTagMatch extends BaseMovement {
diff: startPosition.subtract(position),
});
}
- if (start === end) {
- if (vimState.recordedState.operator instanceof ChangeOperator) {
- vimState.currentMode = ModeName.Insert;
- }
- return {
- start: startPosition,
- stop: startPosition,
- failed: true,
- };
- }
+ // if (start === end) {
+ // if (vimState.recordedState.operator instanceof ChangeOperator) {
+ // vimState.currentMode = ModeName.Insert;
+ // }
+ // return {
+ // start: startPosition,
+ // stop: startPosition,
+ // failed: true,
+ // };
+ // }
+ vimState.cursorStartPosition = startPosition;
return {
start: startPosition,
- stop: endPosition.getLeftThroughLineBreaks(true),
+ stop: endPosition,
};
}
-
- public async execActionForOperator(
- position: Position,
- vimState: VimState
- ): Promise {
- const result = await this.execAction(position, vimState);
- if (isIMovement(result)) {
- if (result.failed) {
- vimState.recordedState.hasRunOperator = false;
- vimState.recordedState.actionsRun = [];
- } else {
- result.stop = result.stop.getRight();
- }
- }
- return result;
- }
}
@RegisterAction
diff --git a/src/actions/plugins/surround.ts b/src/actions/plugins/surround.ts
index 79475b18846..e448bdb0921 100644
--- a/src/actions/plugins/surround.ts
+++ b/src/actions/plugins/surround.ts
@@ -420,7 +420,7 @@ export class CommandSurroundAddToReplacement extends BaseCommand {
if (startReplace.length === 1 && startReplace in PairMatcher.pairings) {
endReplace = PairMatcher.pairings[startReplace].match;
- if (!PairMatcher.pairings[startReplace].nextMatchIsForward) {
+ if (!PairMatcher.pairings[startReplace].isNextMatchForward) {
[startReplace, endReplace] = [endReplace, startReplace];
} else {
startReplace = startReplace + ' ';
@@ -544,25 +544,27 @@ export class CommandSurroundAddToReplacement extends BaseCommand {
}
if (target === 't') {
+ // `MoveInsideTag` must be run first as otherwise the search will
+ // look for the next enclosing tag after having selected the first
+ let innerTagContent = await new MoveInsideTag().execAction(position, vimState);
let { start, stop, failed } = await new MoveAroundTag().execAction(position, vimState);
- let tagEnd = await new MoveInsideTag().execAction(position, vimState);
- if (failed || tagEnd.failed) {
+ if (failed || innerTagContent.failed) {
return CommandSurroundAddToReplacement.Finish(vimState);
}
stop = stop.getRight();
- tagEnd.stop = tagEnd.stop.getRight();
+ innerTagContent.stop = innerTagContent.stop.getRight();
if (failed) {
return CommandSurroundAddToReplacement.Finish(vimState);
}
startReplaceRange = new Range(start, start.getRight());
- endReplaceRange = new Range(tagEnd.stop, tagEnd.stop.getRight());
+ endReplaceRange = new Range(innerTagContent.stop, innerTagContent.stop.getRight());
- startDeleteRange = new Range(start.getRight(), tagEnd.start);
- endDeleteRange = new Range(tagEnd.stop.getRight(), stop);
+ startDeleteRange = new Range(start.getRight(), innerTagContent.start);
+ endDeleteRange = new Range(innerTagContent.stop.getRight(), stop);
}
if (operator === 'change') {
diff --git a/src/actions/textobject.ts b/src/actions/textobject.ts
index 48b21f992ac..891bcc5ead0 100644
--- a/src/actions/textobject.ts
+++ b/src/actions/textobject.ts
@@ -13,6 +13,9 @@ import {
MoveAParentheses,
MoveASingleQuotes,
MoveASquareBracket,
+ MoveABacktick,
+ MoveAroundTag,
+ ExpandingSelection,
} from './motion';
import { ChangeOperator } from './operator';
@@ -37,7 +40,7 @@ export class SelectWord extends TextObjectMovement {
public async execAction(position: Position, vimState: VimState): Promise {
let start: Position;
let stop: Position;
- const currentChar = TextEditor.getLineAt(position).text[position.character];
+ const currentChar = TextEditor.getCharAt(position);
if (/\s/.test(currentChar)) {
start = position.getLastWordEnd().getRight();
@@ -152,26 +155,45 @@ export class SelectABigWord extends TextObjectMovement {
/**
* This is a custom action that I (johnfn) added. It selects procedurally
* larger blocks. e.g. if you had "blah (foo [bar 'ba|z'])" then it would
- * select 'baz' first. If you pressed az again, it'd then select [bar 'baz'],
+ * select 'baz' first. If you pressed af again, it'd then select [bar 'baz'],
* and if you did it a third time it would select "(foo [bar 'baz'])".
*/
@RegisterAction
-export class SelectAnExpandingBlock extends TextObjectMovement {
+export class SelectAnExpandingBlock extends ExpandingSelection {
keys = ['a', 'f'];
modes = [ModeName.Visual, ModeName.VisualLine];
public async execAction(position: Position, vimState: VimState): Promise {
- const ranges = [
- await new MoveASingleQuotes().execAction(position, vimState),
- await new MoveADoubleQuotes().execAction(position, vimState),
- await new MoveAClosingCurlyBrace().execAction(position, vimState),
- await new MoveAParentheses().execAction(position, vimState),
- await new MoveASquareBracket().execAction(position, vimState),
+ const blocks = [
+ new MoveADoubleQuotes(),
+ new MoveASingleQuotes(),
+ new MoveABacktick(),
+ new MoveAClosingCurlyBrace(),
+ new MoveAParentheses(),
+ new MoveASquareBracket(),
+ new MoveAroundTag(),
];
+ // ideally no state would change as we test each of the possible expansions
+ // a deep copy of vimState could work here but may be expensive
+ let ranges: IMovement[] = [];
+ for (const block of blocks) {
+ const cursorPos = new Position(position.line, position.character);
+ const cursorStartPos = new Position(
+ vimState.cursorStartPosition.line,
+ vimState.cursorStartPosition.character
+ );
+ ranges.push(await block.execAction(cursorPos, vimState));
+ vimState.cursorStartPosition = cursorStartPos;
+ }
+
+ ranges = ranges.filter(range => {
+ return !range.failed;
+ });
let smallestRange: Range | undefined = undefined;
for (const iMotion of ranges) {
+ const currentSelectedRange = new Range(vimState.cursorStartPosition, vimState.cursorPosition);
if (iMotion.failed) {
continue;
}
@@ -179,11 +201,16 @@ export class SelectAnExpandingBlock extends TextObjectMovement {
const range = Range.FromIMovement(iMotion);
let contender: Range | undefined = undefined;
- if (!smallestRange) {
- contender = range;
- } else {
- if (range.start.isAfter(smallestRange.start) && range.stop.isBefore(smallestRange.stop)) {
+ if (
+ range.start.isBefore(currentSelectedRange.start) &&
+ range.stop.isAfter(currentSelectedRange.stop)
+ ) {
+ if (!smallestRange) {
contender = range;
+ } else {
+ if (range.start.isAfter(smallestRange.start) && range.stop.isBefore(smallestRange.stop)) {
+ contender = range;
+ }
}
}
@@ -199,13 +226,19 @@ export class SelectAnExpandingBlock extends TextObjectMovement {
}
}
}
-
if (!smallestRange) {
return {
start: vimState.cursorStartPosition,
stop: vimState.cursorPosition,
};
} else {
+ // revert relevant state changes
+ vimState.cursorStartPosition = new Position(
+ smallestRange.start.line,
+ smallestRange.start.character
+ );
+ vimState.cursorPosition = new Position(smallestRange.stop.line, smallestRange.stop.character);
+ vimState.recordedState.operatorPositionDiff = undefined;
return {
start: smallestRange.start,
stop: smallestRange.stop,
diff --git a/src/common/matching/matcher.ts b/src/common/matching/matcher.ts
index f7f67fe7ccb..ce2389253af 100644
--- a/src/common/matching/matcher.ts
+++ b/src/common/matching/matcher.ts
@@ -3,30 +3,7 @@ import * as vscode from 'vscode';
import { TextEditor } from './../../textEditor';
import { Position, PositionDiff } from './../motion/position';
import { configuration } from '../../configuration/configuration';
-
-function escapeRegExpCharacters(value: string): string {
- return value.replace(/[\-\\\{\}\*\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, '\\$&');
-}
-
-let toReversedString = (function() {
- function reverse(str: string): string {
- let reversedStr = '';
- for (let i = str.length - 1; i >= 0; i--) {
- reversedStr += str.charAt(i);
- }
- return reversedStr;
- }
-
- let lastInput: string = '';
- let lastOutput: string = '';
- return function(str: string): string {
- if (lastInput !== str) {
- lastInput = str;
- lastOutput = reverse(lastInput);
- }
- return lastOutput;
- };
-})();
+import { VimState } from '../../state/vimState';
/**
* PairMatcher finds the position matching the given character, respecting nested
@@ -36,32 +13,112 @@ export class PairMatcher {
static pairings: {
[key: string]: {
match: string;
- nextMatchIsForward: boolean;
- directionLess?: boolean;
+ isNextMatchForward: boolean;
+ directionless?: boolean;
matchesWithPercentageMotion?: boolean;
};
} = {
- '(': { match: ')', nextMatchIsForward: true, matchesWithPercentageMotion: true },
- '{': { match: '}', nextMatchIsForward: true, matchesWithPercentageMotion: true },
- '[': { match: ']', nextMatchIsForward: true, matchesWithPercentageMotion: true },
- ')': { match: '(', nextMatchIsForward: false, matchesWithPercentageMotion: true },
- '}': { match: '{', nextMatchIsForward: false, matchesWithPercentageMotion: true },
- ']': { match: '[', nextMatchIsForward: false, matchesWithPercentageMotion: true },
+ '(': { match: ')', isNextMatchForward: true, matchesWithPercentageMotion: true },
+ '{': { match: '}', isNextMatchForward: true, matchesWithPercentageMotion: true },
+ '[': { match: ']', isNextMatchForward: true, matchesWithPercentageMotion: true },
+ ')': { match: '(', isNextMatchForward: false, matchesWithPercentageMotion: true },
+ '}': { match: '{', isNextMatchForward: false, matchesWithPercentageMotion: true },
+ ']': { match: '[', isNextMatchForward: false, matchesWithPercentageMotion: true },
+
// These characters can't be used for "%"-based matching, but are still
// useful for text objects.
- '<': { match: '>', nextMatchIsForward: true },
- '>': { match: '<', nextMatchIsForward: false },
+ '<': { match: '>', isNextMatchForward: true },
+ '>': { match: '<', isNextMatchForward: false },
// These are useful for deleting closing and opening quotes, but don't seem to negatively
// affect how text objects such as `ci"` work, which was my worry.
- '"': { match: '"', nextMatchIsForward: false, directionLess: true },
- "'": { match: "'", nextMatchIsForward: false, directionLess: true },
- '`': { match: '`', nextMatchIsForward: false, directionLess: true },
+ '"': { match: '"', isNextMatchForward: false, directionless: true },
+ "'": { match: "'", isNextMatchForward: false, directionless: true },
+ '`': { match: '`', isNextMatchForward: false, directionless: true },
};
+ private static findPairedChar(
+ position: Position,
+ charToFind: string,
+ charToStack: string,
+ stackHeight,
+ isNextMatchForward: boolean,
+ vimState?: VimState
+ ): Position | undefined {
+ let lineNumber = position.line;
+ let linePosition = position.character;
+ let lineCount = TextEditor.getLineCount();
+ let cursorChar = TextEditor.getCharAt(position);
+ if (vimState) {
+ let startPos = vimState.cursorStartPosition;
+ let endPos = vimState.cursorPosition;
+ if (startPos.isEqual(endPos) && cursorChar === charToFind) {
+ return position;
+ }
+ }
+
+ while (PairMatcher.keepSearching(lineNumber, lineCount, isNextMatchForward)) {
+ let lineText = TextEditor.getLineAt(new Position(lineNumber, 0)).text.split('');
+ const originalLineLength = lineText.length;
+ if (lineNumber === position.line) {
+ if (isNextMatchForward) {
+ lineText = lineText.slice(linePosition + 1, originalLineLength);
+ } else {
+ lineText = lineText.slice(0, linePosition);
+ }
+ }
+
+ while (true) {
+ if (lineText.length <= 0 || stackHeight <= -1) {
+ break;
+ }
+
+ let nextChar: string | undefined;
+ if (isNextMatchForward) {
+ nextChar = lineText.shift();
+ } else {
+ nextChar = lineText.pop();
+ }
+
+ if (nextChar === charToStack) {
+ stackHeight++;
+ } else if (nextChar === charToFind) {
+ stackHeight--;
+ } else {
+ continue;
+ }
+ }
+
+ if (stackHeight <= -1) {
+ let pairMemberChar: number;
+ if (isNextMatchForward) {
+ pairMemberChar = Math.max(0, originalLineLength - lineText.length - 1);
+ } else {
+ pairMemberChar = lineText.length;
+ }
+ return new Position(lineNumber, pairMemberChar);
+ }
+
+ if (isNextMatchForward) {
+ lineNumber++;
+ } else {
+ lineNumber--;
+ }
+ }
+ return undefined;
+ }
+
+ private static keepSearching(lineNumber, lineCount, isNextMatchForward) {
+ if (isNextMatchForward) {
+ return lineNumber <= lineCount - 1;
+ } else {
+ return lineNumber >= 0;
+ }
+ }
+
static nextPairedChar(
position: Position,
charToMatch: string,
- closed: boolean = true
+ vimState?: VimState
): Position | undefined {
/**
* We do a fairly basic implementation that only tracks the state of the type of
@@ -74,103 +131,29 @@ export class PairMatcher {
* PRs welcomed! (TODO)
* Though ideally VSC implements https://github.com/Microsoft/vscode/issues/7177
*/
- const toFind = this.pairings[charToMatch];
+ const pairing = this.pairings[charToMatch];
- if (toFind === undefined || toFind.directionLess) {
+ if (pairing === undefined || pairing.directionless) {
return undefined;
}
- let regex = new RegExp(
- '(' + escapeRegExpCharacters(charToMatch) + '|' + escapeRegExpCharacters(toFind.match) + ')',
- 'i'
+ const stackHeight = 0;
+ let matchedPos: Position | undefined;
+ const charToFind = pairing.match;
+ const charToStack = charToMatch;
+
+ matchedPos = PairMatcher.findPairedChar(
+ position,
+ charToFind,
+ charToStack,
+ stackHeight,
+ pairing.isNextMatchForward,
+ vimState
);
- let stackHeight = closed ? 0 : 1;
- let matchedPosition: Position | undefined = undefined;
-
- // find matched bracket up
- if (!toFind.nextMatchIsForward) {
- for (let lineNumber = position.line; lineNumber >= 0; lineNumber--) {
- let lineText = TextEditor.getLineAt(new Position(lineNumber, 0)).text;
- let startOffset =
- lineNumber === position.line ? lineText.length - position.character - 1 : 0;
-
- while (true) {
- let queryText = toReversedString(lineText).substr(startOffset);
- if (queryText === '') {
- break;
- }
-
- let m = queryText.match(regex);
-
- if (!m) {
- break;
- }
-
- let matchedChar = m[0];
- if (matchedChar === charToMatch) {
- stackHeight++;
- }
-
- if (matchedChar === toFind.match) {
- stackHeight--;
- }
-
- if (stackHeight === 0) {
- matchedPosition = new Position(
- lineNumber,
- lineText.length - startOffset - m.index! - 1
- );
- return matchedPosition;
- }
-
- startOffset = startOffset + m.index! + 1;
- }
- }
- } else {
- for (
- let lineNumber = position.line, lineCount = TextEditor.getLineCount();
- lineNumber < lineCount;
- lineNumber++
- ) {
- let lineText = TextEditor.getLineAt(new Position(lineNumber, 0)).text;
- let startOffset = lineNumber === position.line ? position.character : 0;
-
- while (true) {
- let queryText = lineText.substr(startOffset);
- if (queryText === '') {
- break;
- }
-
- let m = queryText.match(regex);
-
- if (!m) {
- break;
- }
-
- let matchedChar = m[0];
- if (matchedChar === charToMatch) {
- stackHeight++;
- }
-
- if (matchedChar === toFind.match) {
- stackHeight--;
- }
-
- if (stackHeight === 0) {
- matchedPosition = new Position(lineNumber, startOffset + m.index!);
- return matchedPosition;
- }
-
- startOffset = startOffset + m.index! + 1;
- }
- }
+ if (matchedPos) {
+ return matchedPos;
}
-
- if (matchedPosition) {
- return matchedPosition;
- }
-
// TODO(bell)
return undefined;
}
diff --git a/src/common/matching/tagMatcher.ts b/src/common/matching/tagMatcher.ts
index 470ca584412..10ffd468435 100644
--- a/src/common/matching/tagMatcher.ts
+++ b/src/common/matching/tagMatcher.ts
@@ -1,3 +1,7 @@
+import { TextEditor } from '../../textEditor';
+import { VimState } from '../../state/vimState';
+import { Position } from 'vscode';
+
type Tag = { name: string; type: 'close' | 'open'; startPos: number; endPos: number };
type MatchedTag = {
tag: string;
@@ -8,7 +12,8 @@ type MatchedTag = {
};
export class TagMatcher {
- static TAG_REGEX = /\<(\/)?([^\>\<\s]+)[^\>\<]*?(\/?)\>/g;
+ // see regexr.com/3t585
+ static TAG_REGEX = /\<(\/)?([^\>\<\s\/]+)(?:[^\>\<]*?)(\/)?\>/g;
static OPEN_FORWARD_SLASH = 1;
static TAG_NAME = 2;
static CLOSE_FORWARD_SLASH = 3;
@@ -18,7 +23,7 @@ export class TagMatcher {
closeStart: number | undefined;
closeEnd: number | undefined;
- constructor(corpus: string, position: number) {
+ constructor(corpus: string, position: number, vimState: VimState) {
let match = TagMatcher.TAG_REGEX.exec(corpus);
const tags: Tag[] = [];
@@ -72,8 +77,10 @@ export class TagMatcher {
}
}
+ const startPos = TextEditor.getOffsetAt(vimState.cursorStartPosition);
+ const endPos = position;
const tagsSurrounding = matchedTags.filter(n => {
- return position >= n.openingTagStart && position <= n.closingTagEnd;
+ return startPos > n.openingTagStart && endPos < n.closingTagEnd;
});
if (!tagsSurrounding.length) {
@@ -84,9 +91,18 @@ export class TagMatcher {
const nodeSurrounding = tagsSurrounding[0];
this.openStart = nodeSurrounding.openingTagStart;
- this.openEnd = nodeSurrounding.openingTagEnd;
- this.closeStart = nodeSurrounding.closingTagStart;
this.closeEnd = nodeSurrounding.closingTagEnd;
+ // if the inner tag content is already selected, expand to enclose tags with 'it' as in vim
+ if (
+ startPos === nodeSurrounding.openingTagEnd &&
+ endPos + 1 === nodeSurrounding.closingTagStart
+ ) {
+ this.openEnd = this.openStart;
+ this.closeStart = this.closeEnd;
+ } else {
+ this.openEnd = nodeSurrounding.openingTagEnd;
+ this.closeStart = nodeSurrounding.closingTagStart;
+ }
}
findOpening(inclusive: boolean): number | undefined {
diff --git a/src/mode/modeHandler.ts b/src/mode/modeHandler.ts
index 92fed3f756e..303191bfc53 100644
--- a/src/mode/modeHandler.ts
+++ b/src/mode/modeHandler.ts
@@ -521,11 +521,12 @@ export class ModeHandler implements vscode.Disposable {
}
if (recordedState.operatorReadyToExecute(vimState.currentMode)) {
- vimState = await this.executeOperator(vimState);
-
- vimState.recordedState.hasRunOperator = true;
- ranRepeatableAction = vimState.recordedState.operator.canBeRepeatedWithDot;
- ranAction = true;
+ if (vimState.recordedState.operator) {
+ vimState = await this.executeOperator(vimState);
+ vimState.recordedState.hasRunOperator = true;
+ ranRepeatableAction = vimState.recordedState.operator.canBeRepeatedWithDot;
+ ranAction = true;
+ }
}
if (vimState.currentMode === ModeName.Visual) {
@@ -757,7 +758,8 @@ export class ModeHandler implements vscode.Disposable {
let recordedState = vimState.recordedState;
if (!recordedState.operator) {
- throw new Error("what in god's name");
+ console.error('recordedState.operator: ' + recordedState.operator);
+ throw new Error("what in god's name. recordedState.operator is falsy.");
}
let resultVimState = vimState;
@@ -1471,14 +1473,14 @@ export class ModeHandler implements vscode.Disposable {
if (vimState.currentMode === ModeName.Insert) {
// Check if the keypress is a closing bracket to a corresponding opening bracket right next to it
- let result = PairMatcher.nextPairedChar(vimState.cursorPosition, key, false);
+ let result = PairMatcher.nextPairedChar(vimState.cursorPosition, key);
if (result !== undefined) {
if (vimState.cursorPosition.compareTo(result) === 0) {
return true;
}
}
- result = PairMatcher.nextPairedChar(vimState.cursorPosition.getLeft(), key, true);
+ result = PairMatcher.nextPairedChar(vimState.cursorPosition.getLeft(), key);
if (result !== undefined) {
if (vimState.cursorPosition.getLeftByCount(2).compareTo(result) === 0) {
return true;
diff --git a/test/mode/modeNormal.test.ts b/test/mode/modeNormal.test.ts
index e9efac33c3e..fe6bb942582 100644
--- a/test/mode/modeNormal.test.ts
+++ b/test/mode/modeNormal.test.ts
@@ -364,6 +364,22 @@ suite('Mode Normal', () => {
endMode: ModeName.Insert,
});
+ newTest({
+ title: "Can handle count prefixed 'ci)'",
+ start: [' b(l(baz(f|oo)baz)a)h '],
+ keysPressed: 'c3i)',
+ end: [' b(|)h '],
+ endMode: ModeName.Insert,
+ });
+
+ newTest({
+ title: "Can handle count prefixed 'ca)'",
+ start: [' b(l(baz(f|oo)baz)a)h '],
+ keysPressed: 'c3a)',
+ end: [' b|h '],
+ endMode: ModeName.Insert,
+ });
+
newTest({
title: "Can handle 'ca(' spanning multiple lines",
start: ['call(', ' |arg1)'],
diff --git a/test/mode/modeVisual.test.ts b/test/mode/modeVisual.test.ts
index 3feeeb6a7fb..9ac86c4d5d4 100644
--- a/test/mode/modeVisual.test.ts
+++ b/test/mode/modeVisual.test.ts
@@ -523,6 +523,15 @@ suite('Mode Visual', () => {
endMode: ModeName.Normal,
});
+ newTest({
+ title:
+ 'Count-prefixed vit alternates expanding selection between inner and outer tag brackets',
+ start: [''],
+ keysPressed: 'v3itd',
+ end: ['|
'],
+ endMode: ModeName.Normal,
+ });
+
newTest({
title: 'Can do vat on a matching tag',
start: ['one two'],
@@ -532,6 +541,46 @@ suite('Mode Visual', () => {
});
});
+ newTest({
+ title: 'Can do vat on multiple matching tags',
+ start: ['one two three four'],
+ keysPressed: 'vatatd',
+ end: ['one | four'],
+ endMode: ModeName.Normal,
+ });
+
+ newTest({
+ title: 'Can maintain selection on failure with vat on multiple matching tags',
+ start: ['one two three four'],
+ keysPressed: 'vatatatatd',
+ end: ['one | four'],
+ endMode: ModeName.Normal,
+ });
+
+ newTest({
+ title: 'Can maintain selection on failure with repeat-prefixed vat on multiple matching tags',
+ start: ['one two three four'],
+ keysPressed: 'v4atd',
+ end: ['one | four'],
+ endMode: ModeName.Normal,
+ });
+
+ newTest({
+ title: 'Repeat-prefixed vat does not bleed below',
+ start: ['', '\t
', '\t|test', '\t
', '
', '', 'do not delete'],
+ keysPressed: 'v8atd',
+ end: ['|', '', 'do not delete'],
+ endMode: ModeName.Normal,
+ });
+
+ newTest({
+ title: 'Failed vat does not expand or move selection, remains in visual mode',
+ start: ['one | two'],
+ keysPressed: 'v4atd',
+ end: ['one |two'],
+ endMode: ModeName.Normal,
+ });
+
newTest({
title: 'Can do vi) on a matching parenthesis',
start: ['test(te|st)'],
@@ -540,6 +589,14 @@ suite('Mode Visual', () => {
endMode: ModeName.Normal,
});
+ newTest({
+ title: 'Can do vi) on multiple matching parens',
+ start: ['test(te(te|st)st)'],
+ keysPressed: 'vi)i)d',
+ end: ['test(|)'],
+ endMode: ModeName.Normal,
+ });
+
newTest({
title: 'Can do va) on a matching parenthesis',
start: ['test(te|st);'],
@@ -548,6 +605,30 @@ suite('Mode Visual', () => {
endMode: ModeName.Normal,
});
+ newTest({
+ title: 'Can do va) on multiple matching parens',
+ start: ['test(te(te|st)st);'],
+ keysPressed: 'va)a)d',
+ end: ['test|;'],
+ endMode: ModeName.Normal,
+ });
+
+ newTest({
+ title: 'Failed va) does not expand or move selection, remains in visual mode',
+ start: ['one | two'],
+ keysPressed: 'v4a)d',
+ end: ['one |two'],
+ endMode: ModeName.Normal,
+ });
+
+ newTest({
+ title: 'Repeat-prefixed va) does not bleed below',
+ start: ['(', '\t(', '\t|', '\t)', ')', '', 'do not delete'],
+ keysPressed: 'v8a)d',
+ end: ['|', '', 'do not delete'],
+ endMode: ModeName.Normal,
+ });
+
newTest({
title: 'Can do va} on a matching bracket as first character',
start: ['1|{', 'test', '}1'],
@@ -556,6 +637,14 @@ suite('Mode Visual', () => {
endMode: ModeName.Normal,
});
+ newTest({
+ title: 'Can do va} on multiple matching brackets',
+ start: ['test{te{te|st}st};'],
+ keysPressed: 'va}a}d',
+ end: ['test|;'],
+ endMode: ModeName.Normal,
+ });
+
newTest({
title: 'Can do vi( on a matching bracket near first character',
start: ['test(()=>{', '|', '});'],
@@ -580,6 +669,37 @@ suite('Mode Visual', () => {
endMode: ModeName.Normal,
});
+ newTest({
+ title: 'Can do va] on multiple matching brackets',
+ start: ['test[te[te|st]st];'],
+ keysPressed: 'va]a]d',
+ end: ['test|;'],
+ endMode: ModeName.Normal,
+ });
+
+ newTest({
+ title: 'Can do repeat-prefixed vaf on multiple matching pairs of different types',
+ start: ['test test;'],
+ keysPressed: 'v8afd',
+ end: ['test | test;'],
+ endMode: ModeName.Normal,
+ });
+
+ newTest({
+ title: 'Repeat-prefixed vaf does not bleed below',
+ start: ['', '\t(', '\t|', '\t)', '
', '', 'do not delete'],
+ keysPressed: 'v8afd',
+ end: ['|', '', 'do not delete'],
+ endMode: ModeName.Normal,
+ });
+
+ newTest({
+ title: 'vaf only expands to enclosing pairs',
+ start: ['test (f|oo) "hi" test;'],
+ keysPressed: 'vafd',
+ end: ['test | "hi" test;'],
+ endMode: ModeName.Normal,
+ });
suite('handles replace in visual mode', () => {
newTest({
title: 'Can do a single line replace',