Skip to content

Commit 997a2c7

Browse files
authored
Partial support for expressions, :let, and :echo (#7920)
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
1 parent 9f979b2 commit 997a2c7

File tree

21 files changed

+3445
-117
lines changed

21 files changed

+3445
-117
lines changed

src/cmd_line/commands/echo.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { optWhitespace, Parser, whitespace } from 'parsimmon';
2+
import { VimState } from '../../state/vimState';
3+
import { StatusBar } from '../../statusBar';
4+
import { ExCommand } from '../../vimscript/exCommand';
5+
import { EvaluationContext } from '../../vimscript/expression/evaluate';
6+
import { expressionParser } from '../../vimscript/expression/parser';
7+
import { Expression } from '../../vimscript/expression/types';
8+
import { displayValue } from '../../vimscript/expression/displayValue';
9+
10+
export class EchoCommand extends ExCommand {
11+
public static argParser(echoArgs: { sep: string; error: boolean }): Parser<EchoCommand> {
12+
return optWhitespace
13+
.then(expressionParser.sepBy(whitespace))
14+
.map((expressions) => new EchoCommand(echoArgs, expressions));
15+
}
16+
17+
private sep: string;
18+
private error: boolean;
19+
private expressions: Expression[];
20+
private constructor(args: { sep: string; error: boolean }, expressions: Expression[]) {
21+
super();
22+
this.sep = args.sep;
23+
this.error = args.error;
24+
this.expressions = expressions;
25+
}
26+
27+
public override neovimCapable(): boolean {
28+
return true;
29+
}
30+
31+
public async execute(vimState: VimState): Promise<void> {
32+
const ctx = new EvaluationContext();
33+
const values = this.expressions.map((x) => ctx.evaluate(x));
34+
const message = values.map((v) => displayValue(v)).join(this.sep);
35+
StatusBar.setText(vimState, message, this.error);
36+
}
37+
}

src/cmd_line/commands/eval.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { optWhitespace, Parser } from 'parsimmon';
2+
import { VimState } from '../../state/vimState';
3+
import { ExCommand } from '../../vimscript/exCommand';
4+
import { expressionParser, functionCallParser } from '../../vimscript/expression/parser';
5+
import { Expression } from '../../vimscript/expression/types';
6+
import { EvaluationContext } from '../../vimscript/expression/evaluate';
7+
8+
export class EvalCommand extends ExCommand {
9+
public static argParser: Parser<EvalCommand> = optWhitespace
10+
.then(expressionParser)
11+
.map((expression) => new EvalCommand(expression));
12+
13+
private expression: Expression;
14+
private constructor(expression: Expression) {
15+
super();
16+
this.expression = expression;
17+
}
18+
19+
public async execute(vimState: VimState): Promise<void> {
20+
const ctx = new EvaluationContext();
21+
ctx.evaluate(this.expression);
22+
}
23+
}
24+
25+
export class CallCommand extends ExCommand {
26+
public static argParser: Parser<CallCommand> = optWhitespace
27+
.then(functionCallParser)
28+
.map((call) => new CallCommand(call));
29+
30+
private expression: Expression;
31+
private constructor(funcCall: Expression) {
32+
super();
33+
this.expression = funcCall;
34+
}
35+
36+
public async execute(vimState: VimState): Promise<void> {
37+
const ctx = new EvaluationContext();
38+
ctx.evaluate(this.expression);
39+
}
40+
}

src/cmd_line/commands/let.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// eslint-disable-next-line id-denylist
2+
import { alt, optWhitespace, Parser, seq, string, whitespace } from 'parsimmon';
3+
import { env } from 'process';
4+
import { VimState } from '../../state/vimState';
5+
import { StatusBar } from '../../statusBar';
6+
import { ExCommand } from '../../vimscript/exCommand';
7+
import {
8+
add,
9+
concat,
10+
divide,
11+
modulo,
12+
multiply,
13+
str,
14+
subtract,
15+
} from '../../vimscript/expression/build';
16+
import { EvaluationContext } from '../../vimscript/expression/evaluate';
17+
import {
18+
envVariableParser,
19+
expressionParser,
20+
optionParser,
21+
registerParser,
22+
variableParser,
23+
} from '../../vimscript/expression/parser';
24+
import {
25+
EnvVariableExpression,
26+
Expression,
27+
OptionExpression,
28+
RegisterExpression,
29+
VariableExpression,
30+
} from '../../vimscript/expression/types';
31+
import { displayValue } from '../../vimscript/expression/displayValue';
32+
import { ErrorCode, VimError } from '../../error';
33+
34+
export type LetCommandOperation = '=' | '+=' | '-=' | '*=' | '/=' | '%=' | '.=' | '..=';
35+
export type LetCommandVariable =
36+
| VariableExpression
37+
| OptionExpression
38+
| RegisterExpression
39+
| EnvVariableExpression;
40+
export type LetCommandArgs =
41+
| {
42+
operation: LetCommandOperation;
43+
variable: LetCommandVariable;
44+
expression: Expression;
45+
lock: boolean;
46+
}
47+
| {
48+
operation: 'print';
49+
variables: LetCommandVariable[];
50+
};
51+
52+
const operationParser: Parser<LetCommandOperation> = alt(
53+
string('='),
54+
string('+='),
55+
string('-='),
56+
string('*='),
57+
string('/='),
58+
string('%='),
59+
string('.='),
60+
string('..='),
61+
);
62+
63+
const letVarParser = alt<LetCommandVariable>(
64+
variableParser,
65+
optionParser,
66+
envVariableParser,
67+
registerParser,
68+
);
69+
70+
export class LetCommand extends ExCommand {
71+
// TODO: Support unpacking
72+
// TODO: Support indexing
73+
// TODO: Support slicing
74+
public static readonly argParser = (lock: boolean) =>
75+
alt<LetCommand>(
76+
// `:let {var} = {expr}`
77+
// `:let {var} += {expr}`
78+
// `:let {var} -= {expr}`
79+
// `:let {var} .= {expr}`
80+
whitespace.then(
81+
seq(letVarParser, operationParser.wrap(optWhitespace, optWhitespace), expressionParser).map(
82+
([variable, operation, expression]) =>
83+
new LetCommand({
84+
operation,
85+
variable,
86+
expression,
87+
lock,
88+
}),
89+
),
90+
),
91+
// `:let`
92+
// `:let {var-name} ...`
93+
optWhitespace
94+
.then(letVarParser.sepBy(whitespace))
95+
.map((variables) => new LetCommand({ operation: 'print', variables })),
96+
);
97+
98+
private args: LetCommandArgs;
99+
constructor(args: LetCommandArgs) {
100+
super();
101+
this.args = args;
102+
}
103+
104+
async execute(vimState: VimState): Promise<void> {
105+
const context = new EvaluationContext();
106+
if (this.args.operation === 'print') {
107+
if (this.args.variables.length === 0) {
108+
// TODO
109+
} else {
110+
const variable = this.args.variables[this.args.variables.length - 1];
111+
const value = context.evaluate(variable);
112+
const prefix = value.type === 'number' ? '#' : value.type === 'funcref' ? '*' : '';
113+
StatusBar.setText(vimState, `${variable.name} ${prefix}${displayValue(value)}`);
114+
}
115+
} else {
116+
const variable = this.args.variable;
117+
118+
if (this.args.lock) {
119+
if (this.args.operation !== '=') {
120+
throw VimError.fromCode(ErrorCode.CannotModifyExistingVariable);
121+
} else if (this.args.variable.type !== 'variable') {
122+
// TODO: this error message should vary by type
123+
throw VimError.fromCode(ErrorCode.CannotLockARegister);
124+
}
125+
}
126+
127+
let value = context.evaluate(this.args.expression);
128+
if (variable.type === 'variable') {
129+
if (this.args.operation === '+=') {
130+
value = context.evaluate(add(variable, value));
131+
} else if (this.args.operation === '-=') {
132+
value = context.evaluate(subtract(variable, value));
133+
} else if (this.args.operation === '*=') {
134+
value = context.evaluate(multiply(variable, value));
135+
} else if (this.args.operation === '/=') {
136+
value = context.evaluate(divide(variable, value));
137+
} else if (this.args.operation === '%=') {
138+
value = context.evaluate(modulo(variable, value));
139+
} else if (this.args.operation === '.=') {
140+
value = context.evaluate(concat(variable, value));
141+
} else if (this.args.operation === '..=') {
142+
value = context.evaluate(concat(variable, value));
143+
}
144+
context.setVariable(variable, value, this.args.lock);
145+
} else if (variable.type === 'register') {
146+
// TODO
147+
} else if (variable.type === 'option') {
148+
// TODO
149+
} else if (variable.type === 'env_variable') {
150+
value = str(env[variable.name] ?? '');
151+
}
152+
}
153+
}
154+
}

src/cmd_line/commands/marks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export class DeleteMarksCommand extends ExCommand {
9696
private static resolveMarkList(vimState: VimState, args: DeleteMarksArgs) {
9797
const asciiRange = (start: string, end: string) => {
9898
if (start > end) {
99-
throw VimError.fromCode(ErrorCode.InvalidArgument);
99+
throw VimError.fromCode(ErrorCode.InvalidArgument474);
100100
}
101101

102102
const [asciiStart, asciiEnd] = [start.charCodeAt(0), end.charCodeAt(0)];
@@ -120,7 +120,7 @@ export class DeleteMarksCommand extends ExCommand {
120120
} else {
121121
const range = asciiRange(x.start, x.end);
122122
if (range === undefined) {
123-
throw VimError.fromCode(ErrorCode.InvalidArgument);
123+
throw VimError.fromCode(ErrorCode.InvalidArgument474);
124124
}
125125
marks.push(...range.concat());
126126
}

src/cmd_line/commands/put.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { configuration } from '../../configuration/configuration';
22
import { VimState } from '../../state/vimState';
33

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

1618
export interface IPutCommandArguments {
1719
bang: boolean;
1820
register?: string;
19-
fromExpression?: string;
21+
fromExpression?: Expression;
2022
}
2123

2224
//
@@ -27,15 +29,20 @@ export interface IPutCommandArguments {
2729
export class PutExCommand extends ExCommand {
2830
public static readonly argParser: Parser<PutExCommand> = seq(
2931
bangParser,
30-
alt(
31-
expressionParser,
32-
optWhitespace
33-
.then(any)
34-
.map((x) => ({ register: x }))
35-
.fallback({ register: undefined }),
32+
optWhitespace.then(
33+
alt<Partial<IPutCommandArguments>>(
34+
string('=')
35+
.then(optWhitespace)
36+
.then(expressionParser)
37+
.map((expression) => ({ fromExpression: expression })),
38+
// eslint-disable-next-line id-denylist
39+
any.map((register) => ({ register })).fallback({ register: undefined }),
40+
),
3641
),
3742
).map(([bang, register]) => new PutExCommand({ bang, ...register }));
3843

44+
private static lastExpression: Expression | undefined;
45+
3946
public readonly arguments: IPutCommandArguments;
4047

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

5057
async doPut(vimState: VimState, position: Position): Promise<void> {
51-
if (this.arguments.fromExpression && this.arguments.register) {
52-
// set the register to the value of the expression
53-
Register.overwriteRegister(
54-
vimState,
55-
this.arguments.register,
56-
this.arguments.fromExpression,
57-
0,
58-
);
58+
if (this.arguments.register === '=' && this.arguments.fromExpression === undefined) {
59+
if (PutExCommand.lastExpression === undefined) {
60+
return;
61+
}
62+
this.arguments.fromExpression = PutExCommand.lastExpression;
63+
}
64+
65+
if (this.arguments.fromExpression) {
66+
PutExCommand.lastExpression = this.arguments.fromExpression;
67+
68+
this.arguments.register = '=';
69+
70+
const value = new EvaluationContext().evaluate(this.arguments.fromExpression);
71+
const stringified =
72+
value.type === 'list' ? value.items.map(toString).join('\n') : toString(value);
73+
Register.overwriteRegister(vimState, this.arguments.register, stringified, 0);
5974
}
6075

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

0 commit comments

Comments
 (0)