Skip to content

Commit

Permalink
Partial support for expressions, :let, and :echo (#7920)
Browse files Browse the repository at this point in the history
There remains 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 Sep 7, 2024
1 parent 9f979b2 commit 997a2c7
Show file tree
Hide file tree
Showing 21 changed files with 3,445 additions and 117 deletions.
37 changes: 37 additions & 0 deletions src/cmd_line/commands/echo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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 } from '../../vimscript/expression/types';
import { displayValue } from '../../vimscript/expression/displayValue';

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);
}
}
40 changes: 40 additions & 0 deletions src/cmd_line/commands/eval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { optWhitespace, Parser } from 'parsimmon';
import { VimState } from '../../state/vimState';
import { ExCommand } from '../../vimscript/exCommand';
import { expressionParser, functionCallParser } from '../../vimscript/expression/parser';
import { Expression } from '../../vimscript/expression/types';
import { EvaluationContext } from '../../vimscript/expression/evaluate';

export class EvalCommand extends ExCommand {
public static argParser: Parser<EvalCommand> = optWhitespace
.then(expressionParser)
.map((expression) => new EvalCommand(expression));

private expression: Expression;
private constructor(expression: Expression) {
super();
this.expression = expression;
}

public async execute(vimState: VimState): Promise<void> {
const ctx = new EvaluationContext();
ctx.evaluate(this.expression);
}
}

export class CallCommand extends ExCommand {
public static argParser: Parser<CallCommand> = optWhitespace
.then(functionCallParser)
.map((call) => new CallCommand(call));

private expression: Expression;
private constructor(funcCall: Expression) {
super();
this.expression = funcCall;
}

public async execute(vimState: VimState): Promise<void> {
const ctx = new EvaluationContext();
ctx.evaluate(this.expression);
}
}
154 changes: 154 additions & 0 deletions src/cmd_line/commands/let.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// eslint-disable-next-line id-denylist
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 '../../vimscript/expression/displayValue';
import { ErrorCode, VimError } from '../../error';

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

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

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

export class LetCommand extends ExCommand {
// TODO: Support unpacking
// TODO: Support indexing
// TODO: Support slicing
public static readonly argParser = (lock: boolean) =>
alt<LetCommand>(
// `: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,
lock,
}),
),
),
// `: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);
const prefix = value.type === 'number' ? '#' : value.type === 'funcref' ? '*' : '';
StatusBar.setText(vimState, `${variable.name} ${prefix}${displayValue(value)}`);
}
} else {
const variable = this.args.variable;

if (this.args.lock) {
if (this.args.operation !== '=') {
throw VimError.fromCode(ErrorCode.CannotModifyExistingVariable);
} else if (this.args.variable.type !== 'variable') {
// TODO: this error message should vary by type
throw VimError.fromCode(ErrorCode.CannotLockARegister);
}
}

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, this.args.lock);
} else if (variable.type === 'register') {
// TODO
} else if (variable.type === 'option') {
// TODO
} else if (variable.type === 'env_variable') {
value = str(env[variable.name] ?? '');
}
}
}
}
4 changes: 2 additions & 2 deletions src/cmd_line/commands/marks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export class DeleteMarksCommand extends ExCommand {
private static resolveMarkList(vimState: VimState, args: DeleteMarksArgs) {
const asciiRange = (start: string, end: string) => {
if (start > end) {
throw VimError.fromCode(ErrorCode.InvalidArgument);
throw VimError.fromCode(ErrorCode.InvalidArgument474);
}

const [asciiStart, asciiEnd] = [start.charCodeAt(0), end.charCodeAt(0)];
Expand All @@ -120,7 +120,7 @@ export class DeleteMarksCommand extends ExCommand {
} else {
const range = asciiRange(x.start, x.end);
if (range === undefined) {
throw VimError.fromCode(ErrorCode.InvalidArgument);
throw VimError.fromCode(ErrorCode.InvalidArgument474);
}
marks.push(...range.concat());
}
Expand Down
49 changes: 32 additions & 17 deletions src/cmd_line/commands/put.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { configuration } from '../../configuration/configuration';
import { VimState } from '../../state/vimState';

// eslint-disable-next-line id-denylist
import { Parser, alt, any, optWhitespace, seq } from 'parsimmon';
import { Parser, alt, any, optWhitespace, seq, string } from 'parsimmon';
import { Position } from 'vscode';
import { PutBeforeFromCmdLine, PutFromCmdLine } from '../../actions/commands/put';
import { ErrorCode, VimError } from '../../error';
Expand All @@ -11,12 +11,14 @@ import { StatusBar } from '../../statusBar';
import { ExCommand } from '../../vimscript/exCommand';
import { LineRange } from '../../vimscript/lineRange';
import { bangParser } from '../../vimscript/parserUtils';
import { expressionParser } from '../expression';
import { Expression } from '../../vimscript/expression/types';
import { expressionParser } from '../../vimscript/expression/parser';
import { EvaluationContext, toString } from '../../vimscript/expression/evaluate';

export interface IPutCommandArguments {
bang: boolean;
register?: string;
fromExpression?: string;
fromExpression?: Expression;
}

//
Expand All @@ -27,15 +29,20 @@ export interface IPutCommandArguments {
export class PutExCommand extends ExCommand {
public static readonly argParser: Parser<PutExCommand> = seq(
bangParser,
alt(
expressionParser,
optWhitespace
.then(any)
.map((x) => ({ register: x }))
.fallback({ register: undefined }),
optWhitespace.then(
alt<Partial<IPutCommandArguments>>(
string('=')
.then(optWhitespace)
.then(expressionParser)
.map((expression) => ({ fromExpression: expression })),
// eslint-disable-next-line id-denylist
any.map((register) => ({ register })).fallback({ register: undefined }),
),
),
).map(([bang, register]) => new PutExCommand({ bang, ...register }));

private static lastExpression: Expression | undefined;

public readonly arguments: IPutCommandArguments;

constructor(args: IPutCommandArguments) {
Expand All @@ -48,14 +55,22 @@ export class PutExCommand extends ExCommand {
}

async doPut(vimState: VimState, position: Position): Promise<void> {
if (this.arguments.fromExpression && this.arguments.register) {
// set the register to the value of the expression
Register.overwriteRegister(
vimState,
this.arguments.register,
this.arguments.fromExpression,
0,
);
if (this.arguments.register === '=' && this.arguments.fromExpression === undefined) {
if (PutExCommand.lastExpression === undefined) {
return;
}
this.arguments.fromExpression = PutExCommand.lastExpression;
}

if (this.arguments.fromExpression) {
PutExCommand.lastExpression = this.arguments.fromExpression;

this.arguments.register = '=';

const value = new EvaluationContext().evaluate(this.arguments.fromExpression);
const stringified =
value.type === 'list' ? value.items.map(toString).join('\n') : toString(value);
Register.overwriteRegister(vimState, this.arguments.register, stringified, 0);
}

const registerName = this.arguments.register || (configuration.useSystemClipboard ? '*' : '"');
Expand Down
Loading

0 comments on commit 997a2c7

Please sign in to comment.