Skip to content

Commit 256422b

Browse files
committed
Support unpacking with :let
1 parent 6f80ad4 commit 256422b

File tree

4 files changed

+59
-7
lines changed

4 files changed

+59
-7
lines changed

src/cmd_line/commands/let.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// eslint-disable-next-line id-denylist
2-
import { alt, optWhitespace, Parser, seq, string, whitespace } from 'parsimmon';
2+
import { alt, optWhitespace, Parser, sepBy, seq, string, whitespace } from 'parsimmon';
33
import { env } from 'process';
44
import { VimState } from '../../state/vimState';
55
import { StatusBar } from '../../statusBar';
@@ -20,17 +20,24 @@ import {
2020
optionParser,
2121
registerParser,
2222
variableParser,
23+
varNameParser,
2324
} from '../../vimscript/expression/parser';
2425
import {
2526
EnvVariableExpression,
2627
Expression,
2728
OptionExpression,
2829
RegisterExpression,
30+
Value,
2931
VariableExpression,
3032
} from '../../vimscript/expression/types';
3133
import { displayValue } from '../../vimscript/expression/displayValue';
3234
import { ErrorCode, VimError } from '../../error';
3335

36+
type Unpack = {
37+
type: 'unpack';
38+
names: string[];
39+
};
40+
3441
export type LetCommandOperation = '=' | '+=' | '-=' | '*=' | '/=' | '%=' | '.=' | '..=';
3542
export type LetCommandVariable =
3643
| VariableExpression
@@ -40,7 +47,7 @@ export type LetCommandVariable =
4047
export type LetCommandArgs =
4148
| {
4249
operation: LetCommandOperation;
43-
variable: LetCommandVariable;
50+
variable: LetCommandVariable | Unpack;
4451
expression: Expression;
4552
lock: boolean;
4653
}
@@ -67,8 +74,14 @@ const letVarParser = alt<LetCommandVariable>(
6774
registerParser,
6875
);
6976

77+
const unpackParser: Parser<Unpack> = sepBy(varNameParser, string(',').trim(optWhitespace))
78+
.wrap(string('[').then(optWhitespace), optWhitespace.then(string(']')))
79+
.map((names) => ({
80+
type: 'unpack',
81+
names,
82+
}));
83+
7084
export class LetCommand extends ExCommand {
71-
// TODO: Support unpacking
7285
// TODO: Support indexing
7386
// TODO: Support slicing
7487
public static readonly argParser = (lock: boolean) =>
@@ -78,7 +91,11 @@ export class LetCommand extends ExCommand {
7891
// `:let {var} -= {expr}`
7992
// `:let {var} .= {expr}`
8093
whitespace.then(
81-
seq(letVarParser, operationParser.wrap(optWhitespace, optWhitespace), expressionParser).map(
94+
seq(
95+
alt<LetCommandVariable | Unpack>(letVarParser, unpackParser),
96+
operationParser.trim(optWhitespace),
97+
expressionParser,
98+
).map(
8299
([variable, operation, expression]) =>
83100
new LetCommand({
84101
operation,
@@ -107,7 +124,7 @@ export class LetCommand extends ExCommand {
107124
if (this.args.variables.length === 0) {
108125
// TODO
109126
} else {
110-
const variable = this.args.variables[this.args.variables.length - 1];
127+
const variable = this.args.variables.at(-1)!;
111128
const value = context.evaluate(variable);
112129
const prefix = value.type === 'number' ? '#' : value.type === 'funcref' ? '*' : '';
113130
StatusBar.setText(vimState, `${variable.name} ${prefix}${displayValue(value)}`);
@@ -118,7 +135,7 @@ export class LetCommand extends ExCommand {
118135
if (this.args.lock) {
119136
if (this.args.operation !== '=') {
120137
throw VimError.fromCode(ErrorCode.CannotModifyExistingVariable);
121-
} else if (this.args.variable.type !== 'variable') {
138+
} else if (variable.type !== 'variable') {
122139
// TODO: this error message should vary by type
123140
throw VimError.fromCode(ErrorCode.CannotLockARegister);
124141
}
@@ -148,6 +165,25 @@ export class LetCommand extends ExCommand {
148165
// TODO
149166
} else if (variable.type === 'env_variable') {
150167
value = str(env[variable.name] ?? '');
168+
} else if (variable.type === 'unpack') {
169+
// TODO: Support :let [a, b; rest] = ["aval", "bval", 3, 4]
170+
if (value.type !== 'list') {
171+
throw VimError.fromCode(ErrorCode.ListRequired);
172+
}
173+
if (variable.names.length < value.items.length) {
174+
throw VimError.fromCode(ErrorCode.LessTargetsThanListItems);
175+
}
176+
if (variable.names.length > value.items.length) {
177+
throw VimError.fromCode(ErrorCode.MoreTargetsThanListItems);
178+
}
179+
for (let i = 0; i < variable.names.length; i++) {
180+
await new LetCommand({
181+
operation: this.args.operation,
182+
variable: { type: 'variable', namespace: undefined, name: variable.names[i] },
183+
expression: value.items[i],
184+
lock: this.args.lock,
185+
}).execute(vimState);
186+
}
151187
}
152188
}
153189
}

src/error.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export enum ErrorCode {
4646
ChangeListIsEmpty = 664,
4747
ListIndexOutOfRange = 684,
4848
ArgumentOfSortMustBeAList = 686,
49+
LessTargetsThanListItems = 687,
50+
MoreTargetsThanListItems = 688,
4951
CanOnlyCompareListWithList = 691,
5052
InvalidOperationForList = 692,
5153
CannotIndexAFuncref = 695,
@@ -128,6 +130,8 @@ export const ErrorMessage: IErrorMessage = {
128130
664: 'changelist is empty',
129131
684: 'list index out of range',
130132
686: 'Argument of sort() must be a List',
133+
687: 'Less targets than List items',
134+
688: 'More targets than List items',
131135
691: 'Can only compare List with List',
132136
692: 'Invalid operation for List',
133137
695: 'Cannot index a Funcref',

src/vimscript/expression/parser.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ const nestedExpressionParser: Parser<Expression> = lazy(() => expressionParser)
185185
.wrap(string('('), string(')'))
186186
.desc('a nested expression');
187187

188+
export const varNameParser = regexp(/[a-zA-Z0-9_]+/);
189+
188190
export const variableParser: Parser<VariableExpression> = seq(
189191
alt(
190192
string('b'),
@@ -198,7 +200,7 @@ export const variableParser: Parser<VariableExpression> = seq(
198200
)
199201
.skip(string(':'))
200202
.fallback(undefined),
201-
regexp(/[a-zA-Z][a-zA-Z0-9]*/).desc('a variable'),
203+
varNameParser.desc('a variable'),
202204
).map(([namespace, name]) => {
203205
return { type: 'variable', namespace, name };
204206
});

test/vimscript/exCommandParse.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,16 @@ suite('Ex command parsing', () => {
369369
}),
370370
);
371371

372+
exParseTest(
373+
':let [a, b, c] = [1, 2, 3]',
374+
new LetCommand({
375+
operation: '=',
376+
variable: { type: 'unpack', names: ['a', 'b', 'c'] },
377+
expression: list([int(1), int(2), int(3)]),
378+
lock: false,
379+
}),
380+
);
381+
372382
exParseTest(
373383
':const foo = 5',
374384
new LetCommand({ operation: '=', variable: variable('foo'), expression: int(5), lock: true }),

0 commit comments

Comments
 (0)