Skip to content

Commit

Permalink
[WIP] MVP support for expressions, :let, and :echo
Browse files Browse the repository at this point in the history
There remain a mountain of bugs and TODOs, but this a big step toward much more substantial vimscript support.
Refs #463
Fixes #7136, fixes #7155
  • Loading branch information
J-Fields committed Aug 22, 2022
1 parent 662737b commit ea9197f
Show file tree
Hide file tree
Showing 12 changed files with 1,541 additions and 5 deletions.
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1219,5 +1219,11 @@
"webpack-cli": "4.10.0",
"webpack-merge": "5.8.0",
"webpack-stream": "7.0.0"
},
"__metadata": {
"id": "d96e79c6-8b25-4be3-8545-0e0ecefcae03",
"publisherDisplayName": "vscodevim",
"publisherId": "5d63889b-1b67-4b1f-8350-4f1dce041a26",
"isPreReleaseVersion": false
}
}
53 changes: 53 additions & 0 deletions src/cmd_line/commands/echo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { optWhitespace, Parser, whitespace } from 'parsimmon';
import { VimState } from '../../state/vimState';
import { StatusBar } from '../../statusBar';
import { ExCommand } from '../../vimscript/exCommand';
import { EvaluationContext } from '../../vimscript/expression/evaluate';
import { expressionParser } from '../../vimscript/expression/parser';
import { Expression, Value } from '../../vimscript/expression/types';

export function displayValue(value: Value, topLevel = true): string {
switch (value.type) {
case 'number':
case 'float':
// TODO: this is incorrect for float with exponent
return value.value.toString();
case 'string':
return topLevel ? value.value : `'${value.value.replace("'", "''")}'`;
case 'list':
return `[${value.items.map((v) => displayValue(v, false)).join(', ')}]`;
case 'dict_val':
return `{${[...value.items]
.map(([k, v]) => `'${k}': ${displayValue(v, false)}`)
.join(', ')}}`;
}
}

export class EchoCommand extends ExCommand {
public static argParser(echoArgs: { sep: string; error: boolean }): Parser<EchoCommand> {
return optWhitespace
.then(expressionParser.sepBy(whitespace))
.map((expressions) => new EchoCommand(echoArgs, expressions));
}

private sep: string;
private error: boolean;
private expressions: Expression[];
private constructor(args: { sep: string; error: boolean }, expressions: Expression[]) {
super();
this.sep = args.sep;
this.error = args.error;
this.expressions = expressions;
}

public override neovimCapable(): boolean {
return true;
}

public async execute(vimState: VimState): Promise<void> {
const ctx = new EvaluationContext();
const values = this.expressions.map((x) => ctx.evaluate(x));
const message = values.map((v) => displayValue(v)).join(this.sep);
StatusBar.setText(vimState, message, this.error);
}
}
139 changes: 139 additions & 0 deletions src/cmd_line/commands/let.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { alt, optWhitespace, Parser, seq, string, whitespace } from 'parsimmon';
import { env } from 'process';
import { VimState } from '../../state/vimState';
import { StatusBar } from '../../statusBar';
import { ExCommand } from '../../vimscript/exCommand';
import {
add,
concat,
divide,
modulo,
multiply,
str,
subtract,
} from '../../vimscript/expression/build';
import { EvaluationContext } from '../../vimscript/expression/evaluate';
import {
envVariableParser,
expressionParser,
optionParser,
registerParser,
variableParser,
} from '../../vimscript/expression/parser';
import {
EnvVariableExpression,
Expression,
OptionExpression,
RegisterExpression,
VariableExpression,
} from '../../vimscript/expression/types';
import { displayValue } from './echo';

export type LetCommandOperation = '=' | '+=' | '-=' | '*=' | '/=' | '%=' | '.=' | '..=';
export type LetCommandVariable =
| VariableExpression
| OptionExpression
| RegisterExpression
| EnvVariableExpression;
export type LetCommandArgs =
| {
operation: LetCommandOperation;
variable: LetCommandVariable;
expression: Expression;
}
| {
operation: 'print';
variables: LetCommandVariable[];
};

const operationParser: Parser<LetCommandOperation> = alt(
string('='),
string('+='),
string('-='),
string('*='),
string('/='),
string('%='),
string('.='),
string('..=')
);

const letVarParser: Parser<LetCommandVariable> = alt(
variableParser,
optionParser,
envVariableParser,
registerParser
);

export class LetCommand extends ExCommand {
// TODO: Support unpacking
// TODO: Support indexing
// TODO: Support slicing
public static readonly argParser: Parser<LetCommand> = alt(
// `:let {var} = {expr}`
// `:let {var} += {expr}`
// `:let {var} -= {expr}`
// `:let {var} .= {expr}`
whitespace.then(
seq(letVarParser, operationParser.wrap(optWhitespace, optWhitespace), expressionParser).map(
([variable, operation, expression]) =>
new LetCommand({
operation,
variable,
expression,
})
)
),
// `:let`
// `:let {var-name} ...`
optWhitespace
.then(letVarParser.sepBy(whitespace))
.map((variables) => new LetCommand({ operation: 'print', variables }))
);

private args: LetCommandArgs;
constructor(args: LetCommandArgs) {
super();
this.args = args;
}

async execute(vimState: VimState): Promise<void> {
const context = new EvaluationContext();
if (this.args.operation === 'print') {
if (this.args.variables.length === 0) {
// TODO
} else {
const variable = this.args.variables[this.args.variables.length - 1];
const value = context.evaluate(variable);
// TODO: If number, should include # sign
StatusBar.setText(vimState, `${variable.name} ${displayValue(value)}`);
}
} else {
const variable = this.args.variable;
let value = context.evaluate(this.args.expression);
if (variable.type === 'variable') {
if (this.args.operation === '+=') {
value = context.evaluate(add(variable, value));
} else if (this.args.operation === '-=') {
value = context.evaluate(subtract(variable, value));
} else if (this.args.operation === '*=') {
value = context.evaluate(multiply(variable, value));
} else if (this.args.operation === '/=') {
value = context.evaluate(divide(variable, value));
} else if (this.args.operation === '%=') {
value = context.evaluate(modulo(variable, value));
} else if (this.args.operation === '.=') {
value = context.evaluate(concat(variable, value));
} else if (this.args.operation === '..=') {
value = context.evaluate(concat(variable, value));
}
context.setVariable(variable, value);
} else if (variable.type === 'register') {
// TODO
} else if (variable.type === 'option') {
// TODO
} else if (variable.type === 'env_variable') {
value = str(env[variable.name] ?? '');
}
}
}
}
34 changes: 34 additions & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ interface IErrorMessage {

export enum ErrorCode {
InvalidAddress = 14,
InvalidExpression = 15,
InvalidRange = 16,
MarkNotSet = 20,
NoAlternateFile = 23,
Expand All @@ -13,6 +14,8 @@ export enum ErrorCode {
NoPreviousCommand = 34,
NoPreviousRegularExpression = 35,
NoWriteSinceLastChange = 37,
UnknownFunction = 117,
UndefinedVariable = 121,
ErrorWritingToFile = 208,
FileNoLongerAvailable = 211,
RecursiveMapping = 223,
Expand All @@ -34,11 +37,26 @@ export enum ErrorCode {
AtStartOfChangeList = 662,
AtEndOfChangeList = 663,
ChangeListIsEmpty = 664,
ListIndexOutOfRange = 684,
CanOnlyCompareListWithList = 691,
InvalidOperationForList = 692,
KeyNotPresentInDictionary = 716,
CannotUseSliceWithADictionary = 719,
DuplicateKeyInDictionary = 721,
UsingADictionaryAsANumber = 728,
UsingListAsAString = 730,
UsingDictionaryAsAString = 731,
CanOnlyCompareDictionaryWithDictionary = 735,
InvalidOperationForDictionary = 736,
UsingAListAsANumber = 745,
NoPreviouslyUsedRegister = 748,
UsingAFloatAsANumber = 805,
UsingFloatAsAString = 806,
}

export const ErrorMessage: IErrorMessage = {
14: 'Invalid address',
15: 'Invalid expression',
16: 'Invalid range',
20: 'Mark not set',
23: 'No alternate file',
Expand All @@ -48,6 +66,8 @@ export const ErrorMessage: IErrorMessage = {
34: 'No previous command',
35: 'No previous regular expression',
37: 'No write since last change (add ! to override)',
117: 'Unknown function',
121: 'Undefined variable',
208: 'Error writing to file',
211: 'File no longer available', // TODO: Should be `File "[file_name]" no longer available`
223: 'Recursive mapping',
Expand All @@ -69,7 +89,21 @@ export const ErrorMessage: IErrorMessage = {
662: 'At start of changelist',
663: 'At end of changelist',
664: 'changelist is empty',
684: 'list index out of range',
691: 'Can only compare List with List',
692: 'Invalid operation for List',
716: 'Key not present in Dictionary',
719: 'Cannot use [:] with a Dictionary',
721: 'Duplicate key in Dictionary',
728: 'Using a Dictionary as a Number',
730: 'Using List as a String',
731: 'Using Dictionary as a String',
735: 'Can only compare Dictionary with Dictionary',
736: 'Invalid operation for Dictionary',
745: 'Using a List as a Number',
748: 'No previously used register',
805: 'Using a Float as a Number',
806: 'Using Float as a String',
};

export class VimError extends Error {
Expand Down
10 changes: 6 additions & 4 deletions src/vimscript/exCommandParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DeleteCommand } from '../cmd_line/commands/delete';
import { DigraphsCommand } from '../cmd_line/commands/digraph';
import { FileCommand } from '../cmd_line/commands/file';
import { FileInfoCommand } from '../cmd_line/commands/fileInfo';
import { EchoCommand } from '../cmd_line/commands/echo';
import { GotoCommand } from '../cmd_line/commands/goto';
import { GotoLineCommand } from '../cmd_line/commands/gotoLine';
import { HistoryCommand } from '../cmd_line/commands/history';
Expand Down Expand Up @@ -44,6 +45,7 @@ import { StatusBar } from '../statusBar';
import { ExCommand } from './exCommand';
import { LineRange } from './lineRange';
import { nameAbbrevParser } from './parserUtils';
import { LetCommand } from '../cmd_line/commands/let';

type ArgParser = Parser<ExCommand>;

Expand Down Expand Up @@ -202,11 +204,11 @@ export const builtinExCommands: ReadonlyArray<[[string, string], ArgParser | und
[['dsp', 'lit'], undefined],
[['e', 'dit'], FileCommand.argParsers.edit],
[['ea', 'rlier'], undefined],
[['ec', 'ho'], undefined],
[['echoe', 'rr'], undefined],
[['ec', 'ho'], EchoCommand.argParser({ sep: ' ', error: false })],
[['echoe', 'rr'], EchoCommand.argParser({ sep: ' ', error: true })],
[['echoh', 'l'], undefined],
[['echom', 'sg'], undefined],
[['echon', ''], undefined],
[['echon', ''], EchoCommand.argParser({ sep: '', error: false })],
[['el', 'se'], undefined],
[['elsei', 'f'], undefined],
[['em', 'enu'], undefined],
Expand Down Expand Up @@ -297,7 +299,7 @@ export const builtinExCommands: ReadonlyArray<[[string, string], ArgParser | und
[['ld', 'o'], undefined],
[['le', 'ft'], LeftCommand.argParser],
[['lefta', 'bove'], undefined],
[['let', ''], undefined],
[['let', ''], LetCommand.argParser],
[['lex', 'pr'], undefined],
[['lf', 'ile'], undefined],
[['lfd', 'o'], undefined],
Expand Down
Loading

0 comments on commit ea9197f

Please sign in to comment.