Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: relative, plus/minus ranges. closes #2384 #3071

Merged
merged 11 commits into from
Oct 11, 2018
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/actions/commands/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1774,7 +1774,11 @@ class CommandShowCommandLine extends BaseCommand {

public async exec(position: Position, vimState: VimState): Promise<VimState> {
if (vimState.currentMode === ModeName.Normal) {
vimState.currentCommandlineText = '';
if (vimState.recordedState.count) {
vimState.currentCommandlineText = `.,.+${vimState.recordedState.count - 1}`;
} else {
vimState.currentCommandlineText = '';
}
} else {
vimState.currentCommandlineText = "'<,'>";
}
Expand Down
6 changes: 5 additions & 1 deletion src/cmd_line/commands/substitute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,11 @@ export class SubstituteCommand extends node.CommandBase {
endLine = new vscode.Position(TextEditor.getLineCount() - 1, 0);
} else {
startLine = range.lineRefToPosition(vimState.editor, range.left, vimState);
endLine = range.lineRefToPosition(vimState.editor, range.right, vimState);
if (range.right.length === 0) {
endLine = startLine;
} else {
endLine = range.lineRefToPosition(vimState.editor, range.right, vimState);
}
}

if (this._arguments.count && this._arguments.count >= 0) {
Expand Down
71 changes: 45 additions & 26 deletions src/cmd_line/lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,18 @@ namespace LexerFunctions {
case '7':
case '8':
case '9':
return lexLineRef;
if (tokens.length < 1) {
// special case - first digitey token is always a line number
return lexDigits(TokenType.LineNumber);
} else {
// otherwise, use previous token to determine which flavor of digit lexer should be used
let previousTokenType = tokens[tokens.length - 1].type;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

if (previousTokenType === TokenType.Plus || previousTokenType === TokenType.Minus) {
return lexDigits(TokenType.Offset);
} else {
return lexDigits(TokenType.LineNumber);
}
}
case '+':
tokens.push(emitToken(TokenType.Plus, state)!);
continue;
Expand Down Expand Up @@ -110,33 +121,41 @@ namespace LexerFunctions {
return lexRange;
}

function lexLineRef(state: Scanner, tokens: Token[]): ILexFunction | null {
// The first digit has already been lexed.
while (true) {
if (state.isAtEof) {
tokens.push(emitToken(TokenType.LineNumber, state)!);
return null;
}
/**
* when we're lexing digits, it could either be a line number or an offset, depending on whether
* our previous token was a + or a -
*
* so it's lexRange's job to specify which token to emit.
*/
function lexDigits(tokenType: TokenType) {
return function(state: Scanner, tokens: Token[]): ILexFunction | null {
// The first digit has already been lexed.
while (true) {
if (state.isAtEof) {
tokens.push(emitToken(tokenType, state)!);
return null;
}

var c = state.next();
switch (c) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
continue;
default:
state.backup();
tokens.push(emitToken(TokenType.LineNumber, state)!);
return lexRange;
var c = state.next();
switch (c) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
continue;
default:
state.backup();
tokens.push(emitToken(tokenType, state)!);
return lexRange;
}
}
}
};
}

function lexCommand(state: Scanner, tokens: Token[]): ILexFunction | null {
Expand Down
135 changes: 116 additions & 19 deletions src/cmd_line/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import * as vscode from 'vscode';
import { VimState } from '../state/vimState';
import * as token from './token';

type LineRefOperation = token.TokenType.Plus | token.TokenType.Minus | undefined;

export class LineRange {
left: token.Token[];
separator: token.Token;
Expand All @@ -20,15 +22,27 @@ export class LineRange {
}

if (!this.separator) {
if (this.left.length > 0 && tok.type !== token.TokenType.Offset) {
// XXX: is this always this error?
throw Error('not a Vim command');
if (this.left.length > 0) {
switch (tok.type) {
case token.TokenType.Offset:
case token.TokenType.Plus:
case token.TokenType.Minus:
break;
default:
throw Error('Trailing characters');
}
}
this.left.push(tok);
} else {
if (this.right.length > 0 && tok.type !== token.TokenType.Offset) {
// XXX: is this always this error?
throw Error('not a Vim command');
if (this.right.length > 0) {
switch (tok.type) {
case token.TokenType.Offset:
case token.TokenType.Plus:
case token.TokenType.Minus:
break;
default:
throw Error('Trailing characters');
}
}
this.right.push(tok);
}
Expand Down Expand Up @@ -57,20 +71,30 @@ export class LineRange {
toks: token.Token[],
vimState: VimState
): vscode.Position {
var first = toks[0];
switch (first.type) {
case token.TokenType.Dollar:
let currentLineNum: number;
let currentColumn = 0; // only mark does this differently
let currentOperation: LineRefOperation = undefined;

var firstToken = toks[0];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// handle first-token special cases (e.g. %, inital line number is "." by default)
switch (firstToken.type) {
case token.TokenType.Percent:
return new vscode.Position(doc.document.lineCount - 1, 0);
case token.TokenType.Dollar:
currentLineNum = doc.document.lineCount - 1;
break;
case token.TokenType.Plus:
case token.TokenType.Minus:
case token.TokenType.Dot:
return new vscode.Position(doc.selection.active.line, 0);
currentLineNum = doc.selection.active.line;
// undocumented: if the first token is plus or minus, vim seems to behave as though there was a "."
currentOperation = firstToken.type === token.TokenType.Dot ? undefined : firstToken.type;
break;
case token.TokenType.LineNumber:
var line = Number.parseInt(first.content, 10);
line = Math.max(0, line - 1);
line = Math.min(doc.document.lineCount, line);
return new vscode.Position(line, 0);
currentLineNum = Number.parseInt(firstToken.content, 10) - 1; // user sees 1-based - everything else is 0-based
break;
case token.TokenType.SelectionFirstLine:
let startLine = Math.min.apply(
currentLineNum = Math.min.apply(
null,
doc.selections.map(
selection =>
Expand All @@ -79,21 +103,94 @@ export class LineRange {
: selection.end.line
)
);
return new vscode.Position(startLine, 0);
break;
case token.TokenType.SelectionLastLine:
let endLine = Math.max.apply(
currentLineNum = Math.max.apply(
null,
doc.selections.map(
selection =>
selection.start.isAfter(selection.end) ? selection.start.line : selection.end.line
)
);
return new vscode.Position(endLine, 0);
break;
case token.TokenType.Mark:
return vimState.historyTracker.getMark(first.content).position;
currentLineNum = vimState.historyTracker.getMark(firstToken.content).position.line;
currentColumn = vimState.historyTracker.getMark(firstToken.content).position.character;
break;
default:
throw new Error('Not Implemented');
}

// now handle subsequent tokens, offsetting the current candidate line number
for (let tokenIndex = 1; tokenIndex < toks.length; ++tokenIndex) {
let currentToken = toks[tokenIndex];

switch (currentOperation) {
case token.TokenType.Plus:
switch (currentToken.type) {
case token.TokenType.Minus:
case token.TokenType.Plus:
// undocumented: when there's two operators in a row, vim behaves as though there's a "1" between them
currentLineNum += 1;
currentColumn = 0;
currentOperation = currentToken.type;
break;
case token.TokenType.Offset:
currentLineNum += Number.parseInt(currentToken.content, 10);
currentColumn = 0;
currentOperation = undefined;
break;
default:
throw Error('Trailing characters');
}
break;
case token.TokenType.Minus:
switch (currentToken.type) {
case token.TokenType.Minus:
case token.TokenType.Plus:
// undocumented: when there's two operators in a row, vim behaves as though there's a "1" between them
currentLineNum -= 1;
currentColumn = 0;
currentOperation = currentToken.type;
break;
case token.TokenType.Offset:
currentLineNum -= Number.parseInt(currentToken.content, 10);
currentColumn = 0;
currentOperation = undefined;
break;
default:
throw Error('Trailing characters');
}
break;
case undefined:
switch (currentToken.type) {
case token.TokenType.Minus:
case token.TokenType.Plus:
currentOperation = currentToken.type;
break;
default:
throw Error('Trailing characters');
}
break;
}
}

// undocumented: when there's a trailing operation in the tank without an RHS, vim uses "1"
switch (currentOperation) {
case token.TokenType.Plus:
currentLineNum += 1;
currentColumn = 0;
break;
case token.TokenType.Minus:
currentLineNum -= 1;
currentColumn = 0;
break;
}

// finally, make sure current position is in bounds :)
currentLineNum = Math.max(0, currentLineNum);
currentLineNum = Math.min(doc.document.lineCount - 1, currentLineNum);
return new vscode.Position(currentLineNum, currentColumn);
xconverge marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/cmd_line/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ function parseLineRange(state: ParserState, commandLine: node.CommandLine): IPar
case token.TokenType.SelectionFirstLine:
case token.TokenType.SelectionLastLine:
case token.TokenType.Mark:
case token.TokenType.Offset:
case token.TokenType.Plus:
case token.TokenType.Minus:
commandLine.range.addToken(tok);
continue;
case token.TokenType.CommandName:
Expand Down
67 changes: 65 additions & 2 deletions test/cmd_line/substitute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,77 @@ suite('Basic substitute', () => {
confirmStub.restore();
});

test('Replace multiple lines', async () => {
test('Replace across all lines', async () => {
await modeHandler.handleMultipleKeyEvents(['i', 'a', 'b', 'a', '<Esc>', 'o', 'a', 'b']);
await commandLine.Run('%s/a/d/g', modeHandler.vimState);

assertEqualLines(['dbd', 'db']);
});

test('Replace across specific lines', async () => {
newTest({
title: 'Replace on specific single line',
start: ['blah blah', 'bla|h', 'blah blah', 'blah blah'],
keysPressed: ':3s/blah/yay\n',
end: ['blah blah', 'bla|h', 'yay blah', 'blah blah'],
});

newTest({
title: 'Replace on current line using dot',
start: ['blah blah', '|blah', 'blah blah', 'blah blah'],
keysPressed: ':.s/blah/yay\n',
end: ['blah blah', '|yay', 'blah blah', 'blah blah'],
});

newTest({
title: 'Replace single relative line using dot and plus',
start: ['blah blah', 'bla|h', 'blah blah', 'blah blah'],
keysPressed: ':.+2s/blah/yay\n',
end: ['blah blah', 'bla|h', 'blah blah', 'yay blah'],
});

newTest({
title: 'Replace across specific line range',
start: ['blah blah', 'bla|h', 'blah blah', 'blah blah'],
keysPressed: ':3,4s/blah/yay\n',
end: ['blah blah', 'bla|h', 'yay blah', 'yay blah'],
});

newTest({
title: 'Replace across relative line range using dot, plus, and minus',
start: ['blah blah', '|blah', 'blah blah', 'blah blah'],
keysPressed: ':.-1,.+1s/blah/yay\n',
end: ['yay blah', '|yay', 'yay blah', 'blah blah'],
});

newTest({
title: 'Replace across relative line range using numLines+colon shorthand',
start: ['blah blah', '|blah', 'blah blah', 'blah blah'],
keysPressed: '3:s/blah/yay\n',
end: ['blah blah', '|yay', 'yay blah', 'yay blah'],
});

newTest({
title: 'Undocumented: operator without LHS assumes dot as LHS',
start: ['blah blah', 'bla|h', 'blah blah', 'blah blah'],
keysPressed: ':+2s/blah/yay\n',
end: ['blah blah', 'bla|h', 'blah blah', 'yay blah'],
});

newTest({
title: 'Undocumented: multiple consecutive operators use 1 as RHS',
start: ['blah blah', 'bla|h', 'blah blah', 'blah blah'],
keysPressed: ':.++1s/blah/yay\n',
end: ['blah blah', 'bla|h', 'blah blah', 'yay blah'],
});

newTest({
title: 'Undocumented: trailing operators use 1 as RHS',
start: ['blah blah', 'bla|h', 'blah blah', 'blah blah'],
keysPressed: ':.+1+s/blah/yay\n',
end: ['blah blah', 'bla|h', 'blah blah', 'yay blah'],
});

test('Replace specific single equal lines', async () => {
await modeHandler.handleMultipleKeyEvents(['i', 'a', 'b', 'a', '<Esc>', 'o', 'a', 'b']);
await commandLine.Run('1,1s/a/d/g', modeHandler.vimState);

Expand Down