@prisma-next/psl-parser 0.14.0 → 0.15.0-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -10
- package/dist/declarations-DR6To8_k.mjs +1060 -0
- package/dist/declarations-DR6To8_k.mjs.map +1 -0
- package/dist/format.d.mts +19 -0
- package/dist/format.d.mts.map +1 -0
- package/dist/format.mjs +470 -0
- package/dist/format.mjs.map +1 -0
- package/dist/index.d.mts +144 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +849 -3
- package/dist/index.mjs.map +1 -1
- package/dist/interpret.d.mts +39 -0
- package/dist/interpret.d.mts.map +1 -0
- package/dist/interpret.mjs +25 -0
- package/dist/interpret.mjs.map +1 -0
- package/dist/parse-BazJr7Ye.d.mts +426 -0
- package/dist/parse-BazJr7Ye.d.mts.map +1 -0
- package/dist/parse-CdeXr0T3.mjs +626 -0
- package/dist/parse-CdeXr0T3.mjs.map +1 -0
- package/dist/symbol-table-C-AH04Ug.d.mts +127 -0
- package/dist/symbol-table-C-AH04Ug.d.mts.map +1 -0
- package/dist/syntax.d.mts +21 -347
- package/dist/syntax.d.mts.map +1 -1
- package/dist/syntax.mjs +26 -1401
- package/dist/syntax.mjs.map +1 -1
- package/package.json +11 -8
- package/src/attribute-spec/combinators/bool.ts +19 -0
- package/src/attribute-spec/combinators/diagnostic.ts +15 -0
- package/src/attribute-spec/combinators/entity-ref.ts +24 -0
- package/src/attribute-spec/combinators/field-ref.ts +36 -0
- package/src/attribute-spec/combinators/func-call.ts +65 -0
- package/src/attribute-spec/combinators/identifier.ts +16 -0
- package/src/attribute-spec/combinators/int.ts +35 -0
- package/src/attribute-spec/combinators/list.ts +43 -0
- package/src/attribute-spec/combinators/num.ts +26 -0
- package/src/attribute-spec/combinators/one-of.ts +29 -0
- package/src/attribute-spec/combinators/record.ts +43 -0
- package/src/attribute-spec/combinators/str.ts +19 -0
- package/src/attribute-spec/field-attribute.ts +27 -0
- package/src/attribute-spec/interpret.ts +177 -0
- package/src/attribute-spec/model-attribute.ts +27 -0
- package/src/attribute-spec/optional.ts +8 -0
- package/src/attribute-spec/types.ts +72 -0
- package/src/block-reconstruction.ts +140 -0
- package/src/exports/format.ts +3 -0
- package/src/exports/index.ts +61 -3
- package/src/exports/interpret.ts +2 -0
- package/src/exports/syntax.ts +25 -5
- package/src/extension-block.ts +107 -0
- package/src/format/emit.ts +603 -0
- package/src/format/error.ts +13 -0
- package/src/format/format.ts +13 -0
- package/src/format/options.ts +28 -0
- package/src/interpret.ts +67 -0
- package/src/parse.ts +32 -5
- package/src/resolve.ts +123 -0
- package/src/source-file.ts +25 -0
- package/src/symbol-table.ts +446 -0
- package/src/syntax/ast/attributes.ts +5 -6
- package/src/syntax/ast/declarations.ts +51 -26
- package/src/syntax/ast/expressions.ts +12 -13
- package/src/syntax/ast/identifier.ts +2 -3
- package/src/syntax/ast/qualified-name.ts +28 -19
- package/src/syntax/ast/type-annotation.ts +4 -5
- package/src/syntax/ast-helpers.ts +27 -3
- package/src/syntax/navigation.ts +55 -0
- package/src/syntax/red.ts +317 -42
- package/dist/parser-CaplKvRs.mjs +0 -1145
- package/dist/parser-CaplKvRs.mjs.map +0 -1
- package/dist/parser-Dfi3Wfdq.d.mts +0 -7
- package/dist/parser-Dfi3Wfdq.d.mts.map +0 -1
- package/dist/parser.d.mts +0 -2
- package/dist/parser.mjs +0 -2
- package/src/exports/parser.ts +0 -1
- package/src/parser.ts +0 -1642
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ParseDiagnostic } from '../parse';
|
|
2
|
+
|
|
3
|
+
export class PslFormatError extends Error {
|
|
4
|
+
readonly diagnostics: readonly ParseDiagnostic[];
|
|
5
|
+
|
|
6
|
+
constructor(diagnostics: readonly ParseDiagnostic[]) {
|
|
7
|
+
const summary = diagnostics[0]?.message ?? 'unknown parse error';
|
|
8
|
+
const more = diagnostics.length > 1 ? ` (and ${diagnostics.length - 1} more)` : '';
|
|
9
|
+
super(`Cannot format PSL with parse errors: ${summary}${more}`);
|
|
10
|
+
this.name = 'PslFormatError';
|
|
11
|
+
this.diagnostics = diagnostics;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { parse } from '../parse';
|
|
2
|
+
import { emitDocument } from './emit';
|
|
3
|
+
import { PslFormatError } from './error';
|
|
4
|
+
import { type FormatOptions, resolveFormatOptions } from './options';
|
|
5
|
+
|
|
6
|
+
export function format(source: string, options?: FormatOptions): string {
|
|
7
|
+
const resolved = resolveFormatOptions(options);
|
|
8
|
+
const { document, diagnostics } = parse(source);
|
|
9
|
+
if (diagnostics.length > 0) {
|
|
10
|
+
throw new PslFormatError(diagnostics);
|
|
11
|
+
}
|
|
12
|
+
return emitDocument(document, resolved.indentUnit, resolved.newline);
|
|
13
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface FormatOptions {
|
|
2
|
+
readonly indent?: number | 'tab';
|
|
3
|
+
readonly newline?: 'LF' | 'CRLF';
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface ResolvedFormatOptions {
|
|
7
|
+
readonly indentUnit: string;
|
|
8
|
+
readonly newline: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveFormatOptions(options: FormatOptions | undefined): ResolvedFormatOptions {
|
|
12
|
+
const indent = options?.indent ?? 2;
|
|
13
|
+
if (indent !== 'tab' && (typeof indent !== 'number' || !Number.isInteger(indent) || indent < 1)) {
|
|
14
|
+
throw new TypeError(
|
|
15
|
+
`Invalid format options: indent must be a positive integer or 'tab', got ${String(indent)}`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
const newline = options?.newline ?? 'LF';
|
|
19
|
+
if (newline !== 'LF' && newline !== 'CRLF') {
|
|
20
|
+
throw new TypeError(
|
|
21
|
+
`Invalid format options: newline must be 'LF' or 'CRLF', got ${String(newline)}`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
indentUnit: indent === 'tab' ? '\t' : ' '.repeat(indent),
|
|
26
|
+
newline: newline === 'CRLF' ? '\r\n' : '\n',
|
|
27
|
+
};
|
|
28
|
+
}
|
package/src/interpret.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ContractSourceContext,
|
|
3
|
+
ContractSourceDiagnostic,
|
|
4
|
+
ContractSourceDiagnostics,
|
|
5
|
+
ContractSourceProvider,
|
|
6
|
+
PslContractSourceProvider,
|
|
7
|
+
} from '@prisma-next/config/config-types';
|
|
8
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
9
|
+
import { notOk, type Result } from '@prisma-next/utils/result';
|
|
10
|
+
import type { SourceFile } from './source-file';
|
|
11
|
+
import type { SymbolTable } from './symbol-table';
|
|
12
|
+
import type { DocumentAst } from './syntax/ast/declarations';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Lets editor tooling that already parses incrementally (e.g. the language
|
|
16
|
+
* server) hand cached artifacts to the interpreter instead of forcing a
|
|
17
|
+
* disk re-parse.
|
|
18
|
+
*/
|
|
19
|
+
export interface PslInterpretInput {
|
|
20
|
+
readonly document: DocumentAst;
|
|
21
|
+
readonly sourceFile: SourceFile;
|
|
22
|
+
readonly symbolTable: SymbolTable;
|
|
23
|
+
readonly sourceId: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Declared here — the authoring layer that owns `DocumentAst` / `SourceFile` /
|
|
28
|
+
* `SymbolTable` — because `@prisma-next/config` (core) cannot name authoring
|
|
29
|
+
* types. `interpret` must not read disk or `context.resolvedInputs` — those
|
|
30
|
+
* are load-path concerns.
|
|
31
|
+
*/
|
|
32
|
+
export interface PslInterpretCapable extends PslContractSourceProvider {
|
|
33
|
+
interpret(
|
|
34
|
+
input: PslInterpretInput,
|
|
35
|
+
context: ContractSourceContext,
|
|
36
|
+
): Result<Contract, ContractSourceDiagnostics>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Merges caller-side diagnostics (e.g. parse / symbol-table findings) into an
|
|
41
|
+
* interpretation result. The authored headline is deliberately uniform — it
|
|
42
|
+
* reports how many errors the schema has, never which pipeline stage found
|
|
43
|
+
* them.
|
|
44
|
+
*/
|
|
45
|
+
export function withSeedDiagnostics(
|
|
46
|
+
result: Result<Contract, ContractSourceDiagnostics>,
|
|
47
|
+
seedDiagnostics: readonly ContractSourceDiagnostic[],
|
|
48
|
+
): Result<Contract, ContractSourceDiagnostics> {
|
|
49
|
+
if (seedDiagnostics.length === 0) {
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
const diagnostics = result.ok
|
|
53
|
+
? seedDiagnostics
|
|
54
|
+
: [...seedDiagnostics, ...result.failure.diagnostics];
|
|
55
|
+
return notOk({
|
|
56
|
+
summary: `Schema has ${diagnostics.length} error${diagnostics.length === 1 ? '' : 's'}`,
|
|
57
|
+
diagnostics,
|
|
58
|
+
...(result.ok || result.failure.meta === undefined ? {} : { meta: result.failure.meta }),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The single seam that narrows a contract source to the interpret capability. */
|
|
63
|
+
export function hasPslInterpreter(source: ContractSourceProvider): source is PslInterpretCapable {
|
|
64
|
+
return (
|
|
65
|
+
source.sourceFormat === 'psl' && 'interpret' in source && typeof source.interpret === 'function'
|
|
66
|
+
);
|
|
67
|
+
}
|
package/src/parse.ts
CHANGED
|
@@ -399,14 +399,24 @@ function parseParenArgs(cursor: Cursor): void {
|
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
-
export function parseAttributeArg(cursor: Cursor):
|
|
402
|
+
export function parseAttributeArg(cursor: Cursor): void {
|
|
403
|
+
const kind = cursor.peekKind();
|
|
404
|
+
if (
|
|
405
|
+
kind !== 'Ident' &&
|
|
406
|
+
kind !== 'StringLiteral' &&
|
|
407
|
+
kind !== 'NumberLiteral' &&
|
|
408
|
+
kind !== 'LBracket' &&
|
|
409
|
+
kind !== 'LBrace'
|
|
410
|
+
) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
403
413
|
cursor.startNode('AttributeArg');
|
|
404
414
|
if (cursor.peekKind() === 'Ident' && cursor.peekKind(1) === 'Colon') {
|
|
405
415
|
parseIdentifier(cursor);
|
|
406
416
|
cursor.bump();
|
|
407
417
|
}
|
|
408
418
|
parseArgValue(cursor);
|
|
409
|
-
|
|
419
|
+
cursor.finishNode();
|
|
410
420
|
}
|
|
411
421
|
|
|
412
422
|
function parseArgValue(cursor: Cursor): void {
|
|
@@ -435,8 +445,16 @@ export function parseAttribute(cursor: Cursor): GreenNode {
|
|
|
435
445
|
return cursor.finishNode();
|
|
436
446
|
}
|
|
437
447
|
|
|
438
|
-
/**
|
|
439
|
-
|
|
448
|
+
/**
|
|
449
|
+
* A type annotation: `QualifiedName (argList)? ([])? (?)?`, e.g.
|
|
450
|
+
* `pgvector.Vector(1536)[]?`. When the field has no type, no node is emitted —
|
|
451
|
+
* a missing type is the absence of a `TypeAnnotation`, not a zero-width one.
|
|
452
|
+
*/
|
|
453
|
+
export function parseTypeAnnotation(cursor: Cursor): void {
|
|
454
|
+
const kind = cursor.peekKind();
|
|
455
|
+
if (kind !== 'Ident' && kind !== 'LBracket' && kind !== 'Question') {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
440
458
|
cursor.startNode('TypeAnnotation');
|
|
441
459
|
if (cursor.peekKind() === 'Ident') {
|
|
442
460
|
parseQualifiedName(cursor);
|
|
@@ -453,7 +471,7 @@ export function parseTypeAnnotation(cursor: Cursor): GreenNode {
|
|
|
453
471
|
if (cursor.peekKind() === 'Question') {
|
|
454
472
|
cursor.bump();
|
|
455
473
|
}
|
|
456
|
-
|
|
474
|
+
cursor.finishNode();
|
|
457
475
|
}
|
|
458
476
|
|
|
459
477
|
type MemberParser = (cursor: Cursor) => void;
|
|
@@ -693,7 +711,16 @@ function invalidMember(cursor: Cursor, code: PslDiagnosticCode, message: string)
|
|
|
693
711
|
export function parseField(cursor: Cursor): GreenNode | undefined {
|
|
694
712
|
if (cursor.peekKind() !== 'Ident') return undefined;
|
|
695
713
|
cursor.startNode('FieldDeclaration');
|
|
714
|
+
const nameMark = cursor.mark();
|
|
715
|
+
const nameText = cursor.peekToken().text;
|
|
696
716
|
parseIdentifier(cursor);
|
|
717
|
+
if (cursor.peekKind() !== 'Ident') {
|
|
718
|
+
cursor.diagnostic(
|
|
719
|
+
'PSL_INVALID_MODEL_MEMBER',
|
|
720
|
+
`Expected a type after field "${nameText}"`,
|
|
721
|
+
nameMark,
|
|
722
|
+
);
|
|
723
|
+
}
|
|
697
724
|
parseTypeAnnotation(cursor);
|
|
698
725
|
while (cursor.peekKind() === 'At') {
|
|
699
726
|
parseAttribute(cursor);
|
package/src/resolve.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { PslSpan } from '@prisma-next/framework-components/psl-ast';
|
|
2
|
+
import type { Position, Range, SourceFile } from './source-file';
|
|
3
|
+
import type {
|
|
4
|
+
AttributeArgListAst,
|
|
5
|
+
FieldAttributeAst,
|
|
6
|
+
ModelAttributeAst,
|
|
7
|
+
} from './syntax/ast/attributes';
|
|
8
|
+
import type { ExpressionAst } from './syntax/ast/expressions';
|
|
9
|
+
import type { QualifiedNameAst } from './syntax/ast/qualified-name';
|
|
10
|
+
import type { TypeAnnotationAst } from './syntax/ast/type-annotation';
|
|
11
|
+
import { printSyntax } from './syntax/ast-helpers';
|
|
12
|
+
import type { SyntaxNode } from './syntax/red';
|
|
13
|
+
|
|
14
|
+
export interface ResolvedAttributeArg {
|
|
15
|
+
readonly kind: 'positional' | 'named';
|
|
16
|
+
readonly name?: string;
|
|
17
|
+
readonly value: string;
|
|
18
|
+
readonly expression?: ExpressionAst;
|
|
19
|
+
readonly span: PslSpan;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ResolvedAttribute {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly args: readonly ResolvedAttributeArg[];
|
|
25
|
+
readonly span: PslSpan;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ResolvedTypeConstructorCall {
|
|
29
|
+
readonly path: readonly string[];
|
|
30
|
+
readonly args: readonly ResolvedAttributeArg[];
|
|
31
|
+
readonly span: PslSpan;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function readResolvedAttribute(
|
|
35
|
+
attribute: FieldAttributeAst | ModelAttributeAst,
|
|
36
|
+
sourceFile: SourceFile,
|
|
37
|
+
): ResolvedAttribute {
|
|
38
|
+
return {
|
|
39
|
+
name: attributeName(attribute.name()),
|
|
40
|
+
args: readResolvedArgList(attribute.argList(), sourceFile),
|
|
41
|
+
span: nodePslSpan(attribute.syntax, sourceFile),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function readResolvedAttributes(
|
|
46
|
+
attributes: Iterable<FieldAttributeAst | ModelAttributeAst>,
|
|
47
|
+
sourceFile: SourceFile,
|
|
48
|
+
): readonly ResolvedAttribute[] {
|
|
49
|
+
return Array.from(attributes, (attribute) => readResolvedAttribute(attribute, sourceFile));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function readResolvedConstructorCall(
|
|
53
|
+
annotation: TypeAnnotationAst | undefined,
|
|
54
|
+
sourceFile: SourceFile,
|
|
55
|
+
): ResolvedTypeConstructorCall | undefined {
|
|
56
|
+
const argList = annotation?.argList();
|
|
57
|
+
if (annotation === undefined || argList === undefined) return undefined;
|
|
58
|
+
return {
|
|
59
|
+
path: annotation.name()?.path() ?? [],
|
|
60
|
+
args: readResolvedArgList(argList, sourceFile),
|
|
61
|
+
span: nodePslSpan(annotation.syntax, sourceFile),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readResolvedArgList(
|
|
66
|
+
argList: AttributeArgListAst | undefined,
|
|
67
|
+
sourceFile: SourceFile,
|
|
68
|
+
): readonly ResolvedAttributeArg[] {
|
|
69
|
+
if (argList === undefined) return [];
|
|
70
|
+
const args: ResolvedAttributeArg[] = [];
|
|
71
|
+
for (const arg of argList.args()) {
|
|
72
|
+
const name = arg.name()?.name();
|
|
73
|
+
const expression = arg.value();
|
|
74
|
+
args.push({
|
|
75
|
+
kind: name !== undefined ? 'named' : 'positional',
|
|
76
|
+
...(name !== undefined ? { name } : {}),
|
|
77
|
+
value: renderExpression(expression),
|
|
78
|
+
...(expression !== undefined ? { expression } : {}),
|
|
79
|
+
span: nodePslSpan(arg.syntax, sourceFile),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return args;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function attributeName(name: QualifiedNameAst | undefined): string {
|
|
86
|
+
return name?.path().join('.') ?? '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderExpression(expression: ExpressionAst | undefined): string {
|
|
90
|
+
if (expression === undefined) return '';
|
|
91
|
+
return printSyntax(expression.syntax).trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function nodePslSpan(node: SyntaxNode, sourceFile: SourceFile): PslSpan {
|
|
95
|
+
const start = node.offset;
|
|
96
|
+
const end = start + node.green.textLength;
|
|
97
|
+
return {
|
|
98
|
+
start: offsetToPslPosition(start, sourceFile),
|
|
99
|
+
end: offsetToPslPosition(end, sourceFile),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Unsupported-top-level-block diagnostics are anchored to the keyword token. */
|
|
104
|
+
export function keywordPslSpan(node: SyntaxNode, keyword: string, sourceFile: SourceFile): PslSpan {
|
|
105
|
+
const start = node.offset;
|
|
106
|
+
const end = start + keyword.length;
|
|
107
|
+
return {
|
|
108
|
+
start: offsetToPslPosition(start, sourceFile),
|
|
109
|
+
end: offsetToPslPosition(end, sourceFile),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function rangeToPslSpan(range: Range, sourceFile: SourceFile): PslSpan {
|
|
114
|
+
return {
|
|
115
|
+
start: offsetToPslPosition(sourceFile.offsetAt(range.start), sourceFile),
|
|
116
|
+
end: offsetToPslPosition(sourceFile.offsetAt(range.end), sourceFile),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function offsetToPslPosition(offset: number, sourceFile: SourceFile): PslSpan['start'] {
|
|
121
|
+
const position: Position = sourceFile.positionAt(offset);
|
|
122
|
+
return { offset, line: position.line + 1, column: position.character + 1 };
|
|
123
|
+
}
|
package/src/source-file.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const CARRIAGE_RETURN = 13;
|
|
1
2
|
const LINE_FEED = 10;
|
|
2
3
|
|
|
3
4
|
export interface Position {
|
|
@@ -41,6 +42,30 @@ export class SourceFile {
|
|
|
41
42
|
return this.#lineStarts;
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
lineStartOffset(line: number): number {
|
|
46
|
+
if (line <= 0) {
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
return this.#lineStarts[line] ?? this.#text.length;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
lineEndOffset(line: number): number {
|
|
53
|
+
if (line < 0) {
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const nextLineStart = this.#lineStarts[line + 1];
|
|
58
|
+
if (nextLineStart === undefined) {
|
|
59
|
+
return this.#text.length;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const lineFeedOffset = nextLineStart - 1;
|
|
63
|
+
const carriageReturnOffset = lineFeedOffset - 1;
|
|
64
|
+
return this.#text.charCodeAt(carriageReturnOffset) === CARRIAGE_RETURN
|
|
65
|
+
? carriageReturnOffset
|
|
66
|
+
: lineFeedOffset;
|
|
67
|
+
}
|
|
68
|
+
|
|
44
69
|
positionAt(offset: number): Position {
|
|
45
70
|
const clamped = clamp(offset, 0, this.#text.length);
|
|
46
71
|
const line = this.#lineIndexAt(clamped);
|