Skip to content

Commit c2111fa

Browse files
committed
Support slicing with :let
1 parent f5faad4 commit c2111fa

File tree

3 files changed

+77
-5
lines changed

3 files changed

+77
-5
lines changed

src/cmd_line/commands/let.ts

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// eslint-disable-next-line id-denylist
22
import { alt, optWhitespace, Parser, sepBy, seq, string, whitespace } from 'parsimmon';
3-
import { env } from 'process';
43
import { VimState } from '../../state/vimState';
54
import { StatusBar } from '../../statusBar';
65
import { ExCommand } from '../../vimscript/exCommand';
@@ -11,7 +10,6 @@ import {
1110
int,
1211
modulo,
1312
multiply,
14-
str,
1513
subtract,
1614
} from '../../vimscript/expression/build';
1715
import { EvaluationContext, toInt, toString } from '../../vimscript/expression/evaluate';
@@ -42,6 +40,12 @@ type Index = {
4240
variable: VariableExpression;
4341
index: Expression;
4442
};
43+
type Slice = {
44+
type: 'slice';
45+
variable: VariableExpression;
46+
start: Expression | undefined;
47+
end: Expression | undefined;
48+
};
4549

4650
export type LetCommandOperation = '=' | '+=' | '-=' | '*=' | '/=' | '%=' | '.=' | '..=';
4751
export type LetCommandVariable =
@@ -52,7 +56,7 @@ export type LetCommandVariable =
5256
export type LetCommandArgs =
5357
| {
5458
operation: LetCommandOperation;
55-
variable: LetCommandVariable | Unpack | Index;
59+
variable: LetCommandVariable | Unpack | Index | Slice;
5660
expression: Expression;
5761
lock: boolean;
5862
}
@@ -95,8 +99,19 @@ const indexParser: Parser<Index> = seq(
9599
index,
96100
}));
97101

102+
const sliceParser: Parser<Slice> = seq(
103+
variableParser,
104+
string('[').then(optWhitespace).then(expressionParser).fallback(undefined),
105+
string(':').trim(optWhitespace),
106+
expressionParser.fallback(undefined).skip(optWhitespace.then(string(']'))),
107+
).map(([variable, start, _, end]) => ({
108+
type: 'slice',
109+
variable,
110+
start,
111+
end,
112+
}));
113+
98114
export class LetCommand extends ExCommand {
99-
// TODO: Support slicing
100115
public static readonly argParser = (lock: boolean) =>
101116
alt<LetCommand>(
102117
// `:let {var} = {expr}`
@@ -105,7 +120,12 @@ export class LetCommand extends ExCommand {
105120
// `:let {var} .= {expr}`
106121
whitespace.then(
107122
seq(
108-
alt<LetCommandVariable | Unpack | Index>(unpackParser, indexParser, letVarParser),
123+
alt<LetCommandVariable | Unpack | Index | Slice>(
124+
unpackParser,
125+
sliceParser,
126+
indexParser,
127+
letVarParser,
128+
),
109129
operationParser.trim(optWhitespace),
110130
expressionParser,
111131
).map(
@@ -221,6 +241,37 @@ export class LetCommand extends ExCommand {
221241
// TODO: Support blobs
222242
throw VimError.fromCode(ErrorCode.CanOnlyIndexAListDictionaryOrBlob);
223243
}
244+
} else if (variable.type === 'slice') {
245+
// TODO: Operations other than `=`?
246+
// TODO: Support blobs
247+
const varValue = context.evaluate(variable.variable);
248+
if (varValue.type !== 'list' || value.type !== 'list') {
249+
throw VimError.fromCode(ErrorCode.CanOnlyIndexAListDictionaryOrBlob);
250+
}
251+
if (value.type !== 'list') {
252+
throw VimError.fromCode(ErrorCode.SliceRequiresAListOrBlobValue);
253+
}
254+
const start = variable.start ? toInt(context.evaluate(variable.start)) : 0;
255+
if (start > varValue.items.length - 1) {
256+
throw VimError.fromCode(ErrorCode.ListIndexOutOfRange, start.toString());
257+
}
258+
// NOTE: end is inclusive, unlike in JS
259+
const end = variable.end
260+
? toInt(context.evaluate(variable.end))
261+
: varValue.items.length - 1;
262+
const slots = end - start + 1;
263+
if (slots > value.items.length) {
264+
throw VimError.fromCode(ErrorCode.ListValueHasNotEnoughItems);
265+
} else if (slots < value.items.length) {
266+
// TODO: Allow this when going past end of list and end === undefined
267+
throw VimError.fromCode(ErrorCode.ListValueHasMoreItemsThanTarget);
268+
}
269+
let i = start;
270+
for (const item of value.items) {
271+
varValue.items[i] = item;
272+
++i;
273+
}
274+
context.setVariable(variable.variable, varValue, this.args.lock);
224275
}
225276
}
226277
}

src/error.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ export enum ErrorCode {
5656
InvalidTypeForLen = 701,
5757
UsingAFuncrefAsANumber = 703,
5858
FuncrefVariableNameMustStartWithACapital = 704,
59+
SliceRequiresAListOrBlobValue = 709,
60+
ListValueHasMoreItemsThanTarget = 710,
61+
ListValueHasNotEnoughItems = 711,
5962
ArgumentOfMaxMustBeAListOrDictionary = 712, // TODO: This should be different for min(), count()
6063
ListRequired = 714,
6164
DictionaryRequired = 715,
@@ -141,6 +144,9 @@ export const ErrorMessage: IErrorMessage = {
141144
701: 'Invalid type for len()',
142145
703: 'Using a Funcref as a Number',
143146
704: 'Funcref variable name must start with a capital',
147+
709: '[:] requires a List or Blob value',
148+
710: 'List value has more items than target',
149+
711: 'List value has not enough items',
144150
712: 'Argument of max() must be a List or Dictionary',
145151
714: 'List required',
146152
715: 'Dictionary required',

test/vimscript/exCommandParse.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,21 @@ suite('Ex command parsing', () => {
393393
}),
394394
);
395395

396+
exParseTest(
397+
':let s:arr[start:end] = [1, 2, 3]',
398+
new LetCommand({
399+
operation: '=',
400+
variable: {
401+
type: 'slice',
402+
variable: { type: 'variable', namespace: 's', name: 'arr' },
403+
start: { type: 'variable', namespace: undefined, name: 'start' },
404+
end: { type: 'variable', namespace: undefined, name: 'end' },
405+
},
406+
expression: list([int(1), int(2), int(3)]),
407+
lock: false,
408+
}),
409+
);
410+
396411
exParseTest(
397412
':const foo = 5',
398413
new LetCommand({ operation: '=', variable: variable('foo'), expression: int(5), lock: true }),

0 commit comments

Comments
 (0)