Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/tokens/sparseMultilineTokens.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { CharCode } from '../../../base/common/charCode.js';
7
import { Position } from '../core/position.js';
8
import { IRange, Range } from '../core/range.js';
9
import { countEOL } from '../core/misc/eolCounter.js';
10
import { ITextModel } from '../model.js';
11
import { RateLimiter } from './common.js';
12
13
/**
14
* Represents sparse tokens over a contiguous range of lines.
15
*/
16
export class SparseMultilineTokens {
17
18
public static create(startLineNumber: number, tokens: Uint32Array): SparseMultilineTokens {
19
return new SparseMultilineTokens(startLineNumber, new SparseMultilineTokensStorage(tokens));
20
}
21
22
private _startLineNumber: number;
23
private _endLineNumber: number;
24
private readonly _tokens: SparseMultilineTokensStorage;
25
26
/**
27
* (Inclusive) start line number for these tokens.
28
*/
29
public get startLineNumber(): number {
30
return this._startLineNumber;
31
}
32
33
/**
34
* (Inclusive) end line number for these tokens.
35
*/
36
public get endLineNumber(): number {
37
return this._endLineNumber;
38
}
39
40
private constructor(startLineNumber: number, tokens: SparseMultilineTokensStorage) {
41
this._startLineNumber = startLineNumber;
42
this._tokens = tokens;
43
this._endLineNumber = this._startLineNumber + this._tokens.getMaxDeltaLine();
44
}
45
46
public toString(): string {
47
return this._tokens.toString(this._startLineNumber);
48
}
49
50
private _updateEndLineNumber(): void {
51
this._endLineNumber = this._startLineNumber + this._tokens.getMaxDeltaLine();
52
}
53
54
public isEmpty(): boolean {
55
return this._tokens.isEmpty();
56
}
57
58
public getLineTokens(lineNumber: number): SparseLineTokens | null {
59
if (this._startLineNumber <= lineNumber && lineNumber <= this._endLineNumber) {
60
return this._tokens.getLineTokens(lineNumber - this._startLineNumber);
61
}
62
return null;
63
}
64
65
public getRange(): Range | null {
66
const deltaRange = this._tokens.getRange();
67
if (!deltaRange) {
68
return deltaRange;
69
}
70
return new Range(this._startLineNumber + deltaRange.startLineNumber, deltaRange.startColumn, this._startLineNumber + deltaRange.endLineNumber, deltaRange.endColumn);
71
}
72
73
public removeTokens(range: Range): void {
74
const startLineIndex = range.startLineNumber - this._startLineNumber;
75
const endLineIndex = range.endLineNumber - this._startLineNumber;
76
77
this._startLineNumber += this._tokens.removeTokens(startLineIndex, range.startColumn - 1, endLineIndex, range.endColumn - 1);
78
this._updateEndLineNumber();
79
}
80
81
public split(range: Range): [SparseMultilineTokens, SparseMultilineTokens] {
82
// split tokens to two:
83
// a) all the tokens before `range`
84
// b) all the tokens after `range`
85
const startLineIndex = range.startLineNumber - this._startLineNumber;
86
const endLineIndex = range.endLineNumber - this._startLineNumber;
87
88
const [a, b, bDeltaLine] = this._tokens.split(startLineIndex, range.startColumn - 1, endLineIndex, range.endColumn - 1);
89
return [new SparseMultilineTokens(this._startLineNumber, a), new SparseMultilineTokens(this._startLineNumber + bDeltaLine, b)];
90
}
91
92
public applyEdit(range: IRange, text: string): void {
93
const [eolCount, firstLineLength, lastLineLength] = countEOL(text);
94
this.acceptEdit(range, eolCount, firstLineLength, lastLineLength, text.length > 0 ? text.charCodeAt(0) : CharCode.Null);
95
}
96
97
public acceptEdit(range: IRange, eolCount: number, firstLineLength: number, lastLineLength: number, firstCharCode: number): void {
98
this._acceptDeleteRange(range);
99
this._acceptInsertText(new Position(range.startLineNumber, range.startColumn), eolCount, firstLineLength, lastLineLength, firstCharCode);
100
this._updateEndLineNumber();
101
}
102
103
private _acceptDeleteRange(range: IRange): void {
104
if (range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn) {
105
// Nothing to delete
106
return;
107
}
108
109
const firstLineIndex = range.startLineNumber - this._startLineNumber;
110
const lastLineIndex = range.endLineNumber - this._startLineNumber;
111
112
if (lastLineIndex < 0) {
113
// this deletion occurs entirely before this block, so we only need to adjust line numbers
114
const deletedLinesCount = lastLineIndex - firstLineIndex;
115
this._startLineNumber -= deletedLinesCount;
116
return;
117
}
118
119
const tokenMaxDeltaLine = this._tokens.getMaxDeltaLine();
120
121
if (firstLineIndex >= tokenMaxDeltaLine + 1) {
122
// this deletion occurs entirely after this block, so there is nothing to do
123
return;
124
}
125
126
if (firstLineIndex < 0 && lastLineIndex >= tokenMaxDeltaLine + 1) {
127
// this deletion completely encompasses this block
128
this._startLineNumber = 0;
129
this._tokens.clear();
130
return;
131
}
132
133
if (firstLineIndex < 0) {
134
const deletedBefore = -firstLineIndex;
135
this._startLineNumber -= deletedBefore;
136
137
this._tokens.acceptDeleteRange(range.startColumn - 1, 0, 0, lastLineIndex, range.endColumn - 1);
138
} else {
139
this._tokens.acceptDeleteRange(0, firstLineIndex, range.startColumn - 1, lastLineIndex, range.endColumn - 1);
140
}
141
}
142
143
private _acceptInsertText(position: Position, eolCount: number, firstLineLength: number, lastLineLength: number, firstCharCode: number): void {
144
145
if (eolCount === 0 && firstLineLength === 0) {
146
// Nothing to insert
147
return;
148
}
149
150
const lineIndex = position.lineNumber - this._startLineNumber;
151
152
if (lineIndex < 0) {
153
// this insertion occurs before this block, so we only need to adjust line numbers
154
this._startLineNumber += eolCount;
155
return;
156
}
157
158
const tokenMaxDeltaLine = this._tokens.getMaxDeltaLine();
159
160
if (lineIndex >= tokenMaxDeltaLine + 1) {
161
// this insertion occurs after this block, so there is nothing to do
162
return;
163
}
164
165
this._tokens.acceptInsertText(lineIndex, position.column - 1, eolCount, firstLineLength, lastLineLength, firstCharCode);
166
}
167
168
public reportIfInvalid(model: ITextModel): void {
169
this._tokens.reportIfInvalid(model, this._startLineNumber);
170
}
171
}
172
173
class SparseMultilineTokensStorage {
174
/**
175
* The encoding of tokens is:
176
* 4*i deltaLine (from `startLineNumber`)
177
* 4*i+1 startCharacter (from the line start)
178
* 4*i+2 endCharacter (from the line start)
179
* 4*i+3 metadata
180
*/
181
private readonly _tokens: Uint32Array;
182
private _tokenCount: number;
183
184
constructor(tokens: Uint32Array) {
185
this._tokens = tokens;
186
this._tokenCount = tokens.length / 4;
187
}
188
189
public toString(startLineNumber: number): string {
190
const pieces: string[] = [];
191
for (let i = 0; i < this._tokenCount; i++) {
192
pieces.push(`(${this._getDeltaLine(i) + startLineNumber},${this._getStartCharacter(i)}-${this._getEndCharacter(i)})`);
193
}
194
return `[${pieces.join(',')}]`;
195
}
196
197
public getMaxDeltaLine(): number {
198
const tokenCount = this._getTokenCount();
199
if (tokenCount === 0) {
200
return -1;
201
}
202
return this._getDeltaLine(tokenCount - 1);
203
}
204
205
public getRange(): Range | null {
206
const tokenCount = this._getTokenCount();
207
if (tokenCount === 0) {
208
return null;
209
}
210
const startChar = this._getStartCharacter(0);
211
const maxDeltaLine = this._getDeltaLine(tokenCount - 1);
212
const endChar = this._getEndCharacter(tokenCount - 1);
213
return new Range(0, startChar + 1, maxDeltaLine, endChar + 1);
214
}
215
216
private _getTokenCount(): number {
217
return this._tokenCount;
218
}
219
220
private _getDeltaLine(tokenIndex: number): number {
221
return this._tokens[4 * tokenIndex];
222
}
223
224
private _getStartCharacter(tokenIndex: number): number {
225
return this._tokens[4 * tokenIndex + 1];
226
}
227
228
private _getEndCharacter(tokenIndex: number): number {
229
return this._tokens[4 * tokenIndex + 2];
230
}
231
232
public isEmpty(): boolean {
233
return (this._getTokenCount() === 0);
234
}
235
236
public getLineTokens(deltaLine: number): SparseLineTokens | null {
237
let low = 0;
238
let high = this._getTokenCount() - 1;
239
240
while (low < high) {
241
const mid = low + Math.floor((high - low) / 2);
242
const midDeltaLine = this._getDeltaLine(mid);
243
244
if (midDeltaLine < deltaLine) {
245
low = mid + 1;
246
} else if (midDeltaLine > deltaLine) {
247
high = mid - 1;
248
} else {
249
let min = mid;
250
while (min > low && this._getDeltaLine(min - 1) === deltaLine) {
251
min--;
252
}
253
let max = mid;
254
while (max < high && this._getDeltaLine(max + 1) === deltaLine) {
255
max++;
256
}
257
return new SparseLineTokens(this._tokens.subarray(4 * min, 4 * max + 4));
258
}
259
}
260
261
if (this._getDeltaLine(low) === deltaLine) {
262
return new SparseLineTokens(this._tokens.subarray(4 * low, 4 * low + 4));
263
}
264
265
return null;
266
}
267
268
public clear(): void {
269
this._tokenCount = 0;
270
}
271
272
public removeTokens(startDeltaLine: number, startChar: number, endDeltaLine: number, endChar: number): number {
273
const tokens = this._tokens;
274
const tokenCount = this._tokenCount;
275
let newTokenCount = 0;
276
let hasDeletedTokens = false;
277
let firstDeltaLine = 0;
278
for (let i = 0; i < tokenCount; i++) {
279
const srcOffset = 4 * i;
280
const tokenDeltaLine = tokens[srcOffset];
281
const tokenStartCharacter = tokens[srcOffset + 1];
282
const tokenEndCharacter = tokens[srcOffset + 2];
283
const tokenMetadata = tokens[srcOffset + 3];
284
285
if (
286
(tokenDeltaLine > startDeltaLine || (tokenDeltaLine === startDeltaLine && tokenEndCharacter >= startChar))
287
&& (tokenDeltaLine < endDeltaLine || (tokenDeltaLine === endDeltaLine && tokenStartCharacter <= endChar))
288
) {
289
hasDeletedTokens = true;
290
} else {
291
if (newTokenCount === 0) {
292
firstDeltaLine = tokenDeltaLine;
293
}
294
if (hasDeletedTokens) {
295
// must move the token to the left
296
const destOffset = 4 * newTokenCount;
297
tokens[destOffset] = tokenDeltaLine - firstDeltaLine;
298
tokens[destOffset + 1] = tokenStartCharacter;
299
tokens[destOffset + 2] = tokenEndCharacter;
300
tokens[destOffset + 3] = tokenMetadata;
301
}
302
newTokenCount++;
303
}
304
}
305
306
this._tokenCount = newTokenCount;
307
308
return firstDeltaLine;
309
}
310
311
public split(startDeltaLine: number, startChar: number, endDeltaLine: number, endChar: number): [SparseMultilineTokensStorage, SparseMultilineTokensStorage, number] {
312
const tokens = this._tokens;
313
const tokenCount = this._tokenCount;
314
const aTokens: number[] = [];
315
const bTokens: number[] = [];
316
let destTokens: number[] = aTokens;
317
let destOffset = 0;
318
let destFirstDeltaLine: number = 0;
319
for (let i = 0; i < tokenCount; i++) {
320
const srcOffset = 4 * i;
321
const tokenDeltaLine = tokens[srcOffset];
322
const tokenStartCharacter = tokens[srcOffset + 1];
323
const tokenEndCharacter = tokens[srcOffset + 2];
324
const tokenMetadata = tokens[srcOffset + 3];
325
326
if ((tokenDeltaLine > startDeltaLine || (tokenDeltaLine === startDeltaLine && tokenEndCharacter >= startChar))) {
327
if ((tokenDeltaLine < endDeltaLine || (tokenDeltaLine === endDeltaLine && tokenStartCharacter <= endChar))) {
328
// this token is touching the range
329
continue;
330
} else {
331
// this token is after the range
332
if (destTokens !== bTokens) {
333
// this token is the first token after the range
334
destTokens = bTokens;
335
destOffset = 0;
336
destFirstDeltaLine = tokenDeltaLine;
337
}
338
}
339
}
340
341
destTokens[destOffset++] = tokenDeltaLine - destFirstDeltaLine;
342
destTokens[destOffset++] = tokenStartCharacter;
343
destTokens[destOffset++] = tokenEndCharacter;
344
destTokens[destOffset++] = tokenMetadata;
345
}
346
347
return [new SparseMultilineTokensStorage(new Uint32Array(aTokens)), new SparseMultilineTokensStorage(new Uint32Array(bTokens)), destFirstDeltaLine];
348
}
349
350
public acceptDeleteRange(horizontalShiftForFirstLineTokens: number, startDeltaLine: number, startCharacter: number, endDeltaLine: number, endCharacter: number): void {
351
// This is a bit complex, here are the cases I used to think about this:
352
//
353
// 1. The token starts before the deletion range
354
// 1a. The token is completely before the deletion range
355
// -----------
356
// xxxxxxxxxxx
357
// 1b. The token starts before, the deletion range ends after the token
358
// -----------
359
// xxxxxxxxxxx
360
// 1c. The token starts before, the deletion range ends precisely with the token
361
// ---------------
362
// xxxxxxxx
363
// 1d. The token starts before, the deletion range is inside the token
364
// ---------------
365
// xxxxx
366
//
367
// 2. The token starts at the same position with the deletion range
368
// 2a. The token starts at the same position, and ends inside the deletion range
369
// -------
370
// xxxxxxxxxxx
371
// 2b. The token starts at the same position, and ends at the same position as the deletion range
372
// ----------
373
// xxxxxxxxxx
374
// 2c. The token starts at the same position, and ends after the deletion range
375
// -------------
376
// xxxxxxx
377
//
378
// 3. The token starts inside the deletion range
379
// 3a. The token is inside the deletion range
380
// -------
381
// xxxxxxxxxxxxx
382
// 3b. The token starts inside the deletion range, and ends at the same position as the deletion range
383
// ----------
384
// xxxxxxxxxxxxx
385
// 3c. The token starts inside the deletion range, and ends after the deletion range
386
// ------------
387
// xxxxxxxxxxx
388
//
389
// 4. The token starts after the deletion range
390
// -----------
391
// xxxxxxxx
392
//
393
const tokens = this._tokens;
394
const tokenCount = this._tokenCount;
395
const deletedLineCount = (endDeltaLine - startDeltaLine);
396
let newTokenCount = 0;
397
let hasDeletedTokens = false;
398
for (let i = 0; i < tokenCount; i++) {
399
const srcOffset = 4 * i;
400
let tokenDeltaLine = tokens[srcOffset];
401
let tokenStartCharacter = tokens[srcOffset + 1];
402
let tokenEndCharacter = tokens[srcOffset + 2];
403
const tokenMetadata = tokens[srcOffset + 3];
404
405
if (tokenDeltaLine < startDeltaLine || (tokenDeltaLine === startDeltaLine && tokenEndCharacter <= startCharacter)) {
406
// 1a. The token is completely before the deletion range
407
// => nothing to do
408
newTokenCount++;
409
continue;
410
} else if (tokenDeltaLine === startDeltaLine && tokenStartCharacter < startCharacter) {
411
// 1b, 1c, 1d
412
// => the token survives, but it needs to shrink
413
if (tokenDeltaLine === endDeltaLine && tokenEndCharacter > endCharacter) {
414
// 1d. The token starts before, the deletion range is inside the token
415
// => the token shrinks by the deletion character count
416
tokenEndCharacter -= (endCharacter - startCharacter);
417
} else {
418
// 1b. The token starts before, the deletion range ends after the token
419
// 1c. The token starts before, the deletion range ends precisely with the token
420
// => the token shrinks its ending to the deletion start
421
tokenEndCharacter = startCharacter;
422
}
423
} else if (tokenDeltaLine === startDeltaLine && tokenStartCharacter === startCharacter) {
424
// 2a, 2b, 2c
425
if (tokenDeltaLine === endDeltaLine && tokenEndCharacter > endCharacter) {
426
// 2c. The token starts at the same position, and ends after the deletion range
427
// => the token shrinks by the deletion character count
428
tokenEndCharacter -= (endCharacter - startCharacter);
429
} else {
430
// 2a. The token starts at the same position, and ends inside the deletion range
431
// 2b. The token starts at the same position, and ends at the same position as the deletion range
432
// => the token is deleted
433
hasDeletedTokens = true;
434
continue;
435
}
436
} else if (tokenDeltaLine < endDeltaLine || (tokenDeltaLine === endDeltaLine && tokenStartCharacter < endCharacter)) {
437
// 3a, 3b, 3c
438
if (tokenDeltaLine === endDeltaLine && tokenEndCharacter > endCharacter) {
439
// 3c. The token starts inside the deletion range, and ends after the deletion range
440
// => the token moves to continue right after the deletion
441
tokenDeltaLine = startDeltaLine;
442
tokenStartCharacter = startCharacter;
443
tokenEndCharacter = tokenStartCharacter + (tokenEndCharacter - endCharacter);
444
} else {
445
// 3a. The token is inside the deletion range
446
// 3b. The token starts inside the deletion range, and ends at the same position as the deletion range
447
// => the token is deleted
448
hasDeletedTokens = true;
449
continue;
450
}
451
} else if (tokenDeltaLine > endDeltaLine) {
452
// 4. (partial) The token starts after the deletion range, on a line below...
453
if (deletedLineCount === 0 && !hasDeletedTokens) {
454
// early stop, there is no need to walk all the tokens and do nothing...
455
newTokenCount = tokenCount;
456
break;
457
}
458
tokenDeltaLine -= deletedLineCount;
459
} else if (tokenDeltaLine === endDeltaLine && tokenStartCharacter >= endCharacter) {
460
// 4. (continued) The token starts after the deletion range, on the last line where a deletion occurs
461
if (horizontalShiftForFirstLineTokens && tokenDeltaLine === 0) {
462
tokenStartCharacter += horizontalShiftForFirstLineTokens;
463
tokenEndCharacter += horizontalShiftForFirstLineTokens;
464
}
465
tokenDeltaLine -= deletedLineCount;
466
tokenStartCharacter -= (endCharacter - startCharacter);
467
tokenEndCharacter -= (endCharacter - startCharacter);
468
} else {
469
throw new Error(`Not possible!`);
470
}
471
472
const destOffset = 4 * newTokenCount;
473
tokens[destOffset] = tokenDeltaLine;
474
tokens[destOffset + 1] = tokenStartCharacter;
475
tokens[destOffset + 2] = tokenEndCharacter;
476
tokens[destOffset + 3] = tokenMetadata;
477
newTokenCount++;
478
}
479
480
this._tokenCount = newTokenCount;
481
}
482
483
public acceptInsertText(deltaLine: number, character: number, eolCount: number, firstLineLength: number, lastLineLength: number, firstCharCode: number): void {
484
// Here are the cases I used to think about this:
485
//
486
// 1. The token is completely before the insertion point
487
// ----------- |
488
// 2. The token ends precisely at the insertion point
489
// -----------|
490
// 3. The token contains the insertion point
491
// -----|------
492
// 4. The token starts precisely at the insertion point
493
// |-----------
494
// 5. The token is completely after the insertion point
495
// | -----------
496
//
497
const isInsertingPreciselyOneWordCharacter = (
498
eolCount === 0
499
&& firstLineLength === 1
500
&& (
501
(firstCharCode >= CharCode.Digit0 && firstCharCode <= CharCode.Digit9)
502
|| (firstCharCode >= CharCode.A && firstCharCode <= CharCode.Z)
503
|| (firstCharCode >= CharCode.a && firstCharCode <= CharCode.z)
504
)
505
);
506
const tokens = this._tokens;
507
const tokenCount = this._tokenCount;
508
for (let i = 0; i < tokenCount; i++) {
509
const offset = 4 * i;
510
let tokenDeltaLine = tokens[offset];
511
let tokenStartCharacter = tokens[offset + 1];
512
let tokenEndCharacter = tokens[offset + 2];
513
514
if (tokenDeltaLine < deltaLine || (tokenDeltaLine === deltaLine && tokenEndCharacter < character)) {
515
// 1. The token is completely before the insertion point
516
// => nothing to do
517
continue;
518
} else if (tokenDeltaLine === deltaLine && tokenEndCharacter === character) {
519
// 2. The token ends precisely at the insertion point
520
// => expand the end character only if inserting precisely one character that is a word character
521
if (isInsertingPreciselyOneWordCharacter) {
522
tokenEndCharacter += 1;
523
} else {
524
continue;
525
}
526
} else if (tokenDeltaLine === deltaLine && tokenStartCharacter < character && character < tokenEndCharacter) {
527
// 3. The token contains the insertion point
528
if (eolCount === 0) {
529
// => just expand the end character
530
tokenEndCharacter += firstLineLength;
531
} else {
532
// => cut off the token
533
tokenEndCharacter = character;
534
}
535
} else {
536
// 4. or 5.
537
if (tokenDeltaLine === deltaLine && tokenStartCharacter === character) {
538
// 4. The token starts precisely at the insertion point
539
// => grow the token (by keeping its start constant) only if inserting precisely one character that is a word character
540
// => otherwise behave as in case 5.
541
if (isInsertingPreciselyOneWordCharacter) {
542
continue;
543
}
544
}
545
// => the token must move and keep its size constant
546
if (tokenDeltaLine === deltaLine) {
547
tokenDeltaLine += eolCount;
548
// this token is on the line where the insertion is taking place
549
if (eolCount === 0) {
550
tokenStartCharacter += firstLineLength;
551
tokenEndCharacter += firstLineLength;
552
} else {
553
const tokenLength = tokenEndCharacter - tokenStartCharacter;
554
tokenStartCharacter = lastLineLength + (tokenStartCharacter - character);
555
tokenEndCharacter = tokenStartCharacter + tokenLength;
556
}
557
} else {
558
tokenDeltaLine += eolCount;
559
}
560
}
561
562
tokens[offset] = tokenDeltaLine;
563
tokens[offset + 1] = tokenStartCharacter;
564
tokens[offset + 2] = tokenEndCharacter;
565
}
566
}
567
568
private static _rateLimiter = new RateLimiter(10 / 60); // limit to 10 times per minute
569
570
public reportIfInvalid(model: ITextModel, startLineNumber: number): void {
571
for (let i = 0; i < this._tokenCount; i++) {
572
const lineNumber = this._getDeltaLine(i) + startLineNumber;
573
574
if (lineNumber < 1) {
575
SparseMultilineTokensStorage._rateLimiter.runIfNotLimited(() => {
576
console.error('Invalid Semantic Tokens Data From Extension: lineNumber < 1');
577
});
578
} else if (lineNumber > model.getLineCount()) {
579
SparseMultilineTokensStorage._rateLimiter.runIfNotLimited(() => {
580
console.error('Invalid Semantic Tokens Data From Extension: lineNumber > model.getLineCount()');
581
});
582
} else if (this._getEndCharacter(i) > model.getLineLength(lineNumber)) {
583
SparseMultilineTokensStorage._rateLimiter.runIfNotLimited(() => {
584
console.error('Invalid Semantic Tokens Data From Extension: end character > model.getLineLength(lineNumber)');
585
});
586
}
587
}
588
}
589
}
590
591
export class SparseLineTokens {
592
593
private readonly _tokens: Uint32Array;
594
595
constructor(tokens: Uint32Array) {
596
this._tokens = tokens;
597
}
598
599
public getCount(): number {
600
return this._tokens.length / 4;
601
}
602
603
public getStartCharacter(tokenIndex: number): number {
604
return this._tokens[4 * tokenIndex + 1];
605
}
606
607
public getEndCharacter(tokenIndex: number): number {
608
return this._tokens[4 * tokenIndex + 2];
609
}
610
611
public getMetadata(tokenIndex: number): number {
612
return this._tokens[4 * tokenIndex + 3];
613
}
614
}
615
616