@idfkit/language 0.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Samuel Letellier-Duchesne
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # @idfkit/language
2
+
3
+ The opt-in language service for IDF text: what completes here, what this means,
4
+ what this points at, and where a finding sits in the characters the reader is
5
+ looking at.
6
+
7
+ Peer-depends on [`@idfkit/core`](../core) and on nothing else. Everything
8
+ exported here is synchronous and free of I/O, so it runs unchanged in Node, a
9
+ browser, a worker, or behind an editor server.
10
+
11
+ **[Documentation](https://js.idfkit.com/)** ·
12
+ [API reference](https://js.idfkit.com/reference/language/)
13
+
14
+ ```bash
15
+ npm install @idfkit/language
16
+ ```
17
+
18
+ ## Reaching it from the shared name
19
+
20
+ A project that installs `idfkit` reaches this package through a subpath:
21
+
22
+ ```ts
23
+ import { completionsAt } from 'idfkit/language';
24
+ ```
25
+
26
+ The subpath stays in the export map whether or not this package is installed.
27
+ Importing it without this package names the install to run rather than failing
28
+ with a bare module-resolution error, which is why the service costs a reader who
29
+ never asks for it zero bytes on disk and zero bytes in a bundle.
30
+
31
+ ## No protocol
32
+
33
+ This package imports, depends on, and names nothing from any editor protocol
34
+ library. It answers in its own vocabulary and a consumer translates: `envelop`
35
+ to its own editor's shape, `idfkit-lsp` to the Language Server Protocol. The
36
+ regions it reports never cross a line boundary, because no editor token encoding
37
+ can express one that does, so the translation stays a rename rather than
38
+ arithmetic.
39
+
40
+ ## Versioning
41
+
42
+ This package joins the repository's release lockstep. `@idfkit/core`,
43
+ `@idfkit/schemas`, `@idfkit/weather` and this package are versioned and released
44
+ together, and the dependency on `@idfkit/core` is an exact peer range rather than
45
+ a caret one. A service paired with a syntax layer it disagrees with would put
46
+ findings on the wrong characters, silently; the lockstep is what makes that
47
+ install unsupported rather than merely unlikely.
@@ -0,0 +1,93 @@
1
+ import { type IdfDocument, type ProsePool, type Region, type Schema } from '@idfkit/core';
2
+ /**
3
+ * One thing that may go where the cursor is.
4
+ *
5
+ * Everything a consumer needs to render the offer and to apply it, so that a list can be shown and
6
+ * accepted without a second call and without the consumer measuring anything itself.
7
+ */
8
+ export interface Offer {
9
+ /** The text to insert. */
10
+ readonly value: string;
11
+ /**
12
+ * The region this offer would replace.
13
+ *
14
+ * Carried rather than left to the consumer (FR-048), and this is not optional politeness. An
15
+ * editor's own word rules break on both halves of this format: type names contain colons, so
16
+ * `BuildingSurface:Detailed` is two words to most of them, and values contain spaces, so
17
+ * `Office Zone 1` is three. A consumer left to derive the replaced span would get it wrong on
18
+ * the majority of real completions. Working it out needs the format's rules, which live here.
19
+ *
20
+ * The region is empty where nothing has been written yet, which is an insertion at that point.
21
+ */
22
+ readonly replaces: Region;
23
+ /** What kind of thing this is. */
24
+ readonly kind: 'objectType' | 'enumValue' | 'referenceTarget';
25
+ /** Whether the schema marks the field required. Undefined for object types. */
26
+ readonly required: boolean | undefined;
27
+ /**
28
+ * The schema's own prose, when the caller supplied the pool.
29
+ *
30
+ * The type's memo for a type-name offer and the field's note for a value offer, because those
31
+ * are the two things the pool holds. It carries no sentence about an individual permitted value
32
+ * and neither does this: deriving one from the value's spelling is the one thing FR-022 forbids
33
+ * by name.
34
+ */
35
+ readonly prose: string | undefined;
36
+ }
37
+ /**
38
+ * What completes here, or why nothing does.
39
+ *
40
+ * A discriminated union rather than a list that is sometimes empty, because "the schema permits
41
+ * anything here" and "I could not consult a schema" are different states and an editor that
42
+ * rendered them identically would teach the reader that the tool is broken in the first case and
43
+ * silently wrong in the second (FR-020, FR-031). A consumer that only wants the happy path matches
44
+ * `'ok'` and ignores the rest; a consumer that wants to tell a reader why there is nothing has the
45
+ * reason.
46
+ *
47
+ * `'ok'` with no offers is a legitimate state and a different one from all four below: it says the
48
+ * schema constrains this field to names the model has not declared yet.
49
+ */
50
+ export type CompletionResult = {
51
+ readonly status: 'ok';
52
+ readonly offers: readonly Offer[];
53
+ } | {
54
+ readonly status: 'unconstrained';
55
+ } | {
56
+ readonly status: 'noSchema';
57
+ } | {
58
+ readonly status: 'unknownType';
59
+ readonly typeName: string;
60
+ } | {
61
+ readonly status: 'notApplicable';
62
+ };
63
+ /** What a caller can supply beyond the text, the offset and the schema. */
64
+ export interface CompletionOptions {
65
+ /** Supplies candidate names for reference fields. Omit and none are offered. */
66
+ readonly document?: IdfDocument;
67
+ /** Supplies prose for the offers. Omit and offers carry none. */
68
+ readonly prose?: ProsePool;
69
+ }
70
+ /**
71
+ * What may be written where the cursor is.
72
+ *
73
+ * Bounded local work: the cursor is placed by {@link contextAt}, which scans one statement, and
74
+ * the answer then comes from the schema, whose cost is the type rather than the file. The one
75
+ * exception is a reference field, whose candidates are the names other objects declare, which is a
76
+ * whole-document question by nature; it is answered from the document the caller already holds and
77
+ * never by parsing one (research R9). A service that quietly parsed a document when the argument
78
+ * was omitted would make one function on the keystroke path eighty milliseconds slower depending
79
+ * on an argument nobody passed, which is the worst failure mode available.
80
+ *
81
+ * The whole schema's type list is offered where a statement begins, unfiltered by what has been
82
+ * typed so far. Filtering is the consumer's, and it has what it needs to do it: `replaces` says
83
+ * exactly which characters the offer stands in for, which is the span an editor's own word rules
84
+ * get wrong on this format.
85
+ *
86
+ * `schema` is written as possibly absent rather than required, because `'noSchema'` is a state
87
+ * FR-031 requires this to report and a signature that forbade the input would make it unreachable
88
+ * from typed code.
89
+ *
90
+ * Nothing throws, for any input.
91
+ */
92
+ export declare function completionsAt(text: string, offset: number, schema: Schema | undefined, options?: CompletionOptions): CompletionResult;
93
+ //# sourceMappingURL=complete.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"complete.d.ts","sourceRoot":"","sources":["../src/complete.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,MAAM,EACX,KAAK,MAAM,EAGZ,MAAM,cAAc,CAAC;AAItB;;;;;GAKG;AACH,MAAM,WAAW,KAAK;IACpB,0BAA0B;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,kCAAkC;IAClC,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,WAAW,GAAG,iBAAiB,CAAC;IAC9D,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAA;CAAE,GAC5D;IAAE,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GACpC;IAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;CAAE,GAC/B;IAAE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC7D;IAAE,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAA;CAAE,CAAC;AAEzC,2EAA2E;AAC3E,MAAM,WAAW,iBAAiB;IAChC,gFAAgF;IAChF,QAAQ,CAAC,QAAQ,CAAC,EAAE,WAAW,CAAC;IAChC,iEAAiE;IACjE,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC;CAC5B;AAMD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,GAAE,iBAAsB,GAC9B,gBAAgB,CAqElB"}
@@ -0,0 +1,225 @@
1
+ import { describeObjectType, } from '@idfkit/core';
2
+ import { contextAt, fieldNameAt } from './cursor.js';
3
+ const NOT_APPLICABLE = { status: 'notApplicable' };
4
+ const NO_SCHEMA = { status: 'noSchema' };
5
+ const UNCONSTRAINED = { status: 'unconstrained' };
6
+ /**
7
+ * What may be written where the cursor is.
8
+ *
9
+ * Bounded local work: the cursor is placed by {@link contextAt}, which scans one statement, and
10
+ * the answer then comes from the schema, whose cost is the type rather than the file. The one
11
+ * exception is a reference field, whose candidates are the names other objects declare, which is a
12
+ * whole-document question by nature; it is answered from the document the caller already holds and
13
+ * never by parsing one (research R9). A service that quietly parsed a document when the argument
14
+ * was omitted would make one function on the keystroke path eighty milliseconds slower depending
15
+ * on an argument nobody passed, which is the worst failure mode available.
16
+ *
17
+ * The whole schema's type list is offered where a statement begins, unfiltered by what has been
18
+ * typed so far. Filtering is the consumer's, and it has what it needs to do it: `replaces` says
19
+ * exactly which characters the offer stands in for, which is the span an editor's own word rules
20
+ * get wrong on this format.
21
+ *
22
+ * `schema` is written as possibly absent rather than required, because `'noSchema'` is a state
23
+ * FR-031 requires this to report and a signature that forbade the input would make it unreachable
24
+ * from typed code.
25
+ *
26
+ * Nothing throws, for any input.
27
+ */
28
+ export function completionsAt(text, offset, schema, options = {}) {
29
+ const context = contextAt(text, offset, schema);
30
+ // Inside a comment nothing completes, with a schema or without one. Reporting `'noSchema'` here
31
+ // would send a caller off to load one that would change this answer not at all.
32
+ if (context.at === 'comment')
33
+ return NOT_APPLICABLE;
34
+ if (schema === undefined)
35
+ return NO_SCHEMA;
36
+ // `'typeName'` and `'betweenStatements'` are the two states that carry no field index, and both
37
+ // are a statement beginning: half a type name written, or nothing written yet.
38
+ if (context.fieldIndex === undefined) {
39
+ return { status: 'ok', offers: typeNameOffers(schema, context, options.prose) };
40
+ }
41
+ const typeName = context.typeName;
42
+ if (typeName === undefined) {
43
+ return { status: 'unknownType', typeName: context.statement.typeNameText };
44
+ }
45
+ const type = schema.get(typeName);
46
+ if (type === undefined)
47
+ return { status: 'unknownType', typeName };
48
+ const facts = fieldFactsAt(schema, typeName, type, context.fieldIndex, options.prose);
49
+ // Past the type's last field, with no extensible group to repeat. The schema constrains a field
50
+ // it does not define in no way at all; that the field should not be there is a finding, and
51
+ // saying so is that finding's job rather than this one's.
52
+ if (facts === undefined)
53
+ return UNCONSTRAINED;
54
+ const written = context.statement.fields[context.fieldIndex];
55
+ // A field index is counted from the separators of this same statement, so it always names a
56
+ // written field. An insertion at the statement's end is the harmless answer if that ever stops
57
+ // being true; replacing the statement's whole region would not be.
58
+ const replaces = written ?? {
59
+ start: context.statement.region.end,
60
+ end: context.statement.region.end,
61
+ };
62
+ if (facts.values !== undefined && facts.values.length > 0) {
63
+ return {
64
+ status: 'ok',
65
+ offers: facts.values.map((value) => ({
66
+ // Numeric on the handful of fields that express a choice numerically, and the offer is
67
+ // text to insert, so it is spelled the way it would be written.
68
+ value: String(value),
69
+ replaces,
70
+ kind: 'enumValue',
71
+ required: facts.required,
72
+ prose: facts.prose,
73
+ })),
74
+ };
75
+ }
76
+ if (facts.objectList !== undefined && facts.objectList.length > 0) {
77
+ const document = options.document;
78
+ // No document, so no candidates can exist. Saying so is the point: an empty `'ok'` list here
79
+ // would be indistinguishable from a model that has declared nothing yet.
80
+ if (document === undefined)
81
+ return NOT_APPLICABLE;
82
+ return {
83
+ status: 'ok',
84
+ offers: declaredNames(document, schema, facts.objectList).map((value) => ({
85
+ value,
86
+ replaces,
87
+ kind: 'referenceTarget',
88
+ required: facts.required,
89
+ prose: facts.prose,
90
+ })),
91
+ };
92
+ }
93
+ return UNCONSTRAINED;
94
+ }
95
+ /**
96
+ * Every object type the schema defines, as offers replacing the type name as written.
97
+ *
98
+ * The replaced region is the statement's type name, which is empty where nothing has been typed
99
+ * yet, so accepting an offer between two statements inserts and accepting one over a half-written
100
+ * name replaces the whole of it, colons included.
101
+ */
102
+ function typeNameOffers(schema, context, prose) {
103
+ const replaces = context.statement.typeName;
104
+ return schema.typeNames.map((value) => ({
105
+ value,
106
+ replaces,
107
+ kind: 'objectType',
108
+ // A type is not required or optional; only a field is.
109
+ required: undefined,
110
+ prose: memoOf(schema, value, prose),
111
+ }));
112
+ }
113
+ /**
114
+ * The names objects in this document declare into any of `lists`.
115
+ *
116
+ * A walk of the document the caller handed in, which is a document cost rather than a text cost:
117
+ * nothing is parsed, nothing is scanned, and a document a keystroke behind the text is the correct
118
+ * input rather than a stale one, because the statement being typed is incomplete by definition and
119
+ * names harvested from it would be garbage (research R9).
120
+ *
121
+ * A name reaches a list two ways, and both are read here so that no third way to resolve a name is
122
+ * invented (FR-029): the object's own name, when its type's `nref` contributes to the list, and an
123
+ * ordinary field's value, when that field's `ref` does. The second is how anonymous types such as
124
+ * `FluidProperties:Name` carry their identity, and treating those as nameless would offer nothing
125
+ * where the model plainly declares something.
126
+ */
127
+ function declaredNames(document, schema, lists) {
128
+ const wanted = new Set(lists);
129
+ const seen = new Set();
130
+ const names = [];
131
+ const keep = (value) => {
132
+ if (value === undefined || value === '')
133
+ return;
134
+ // Deduplicated case-insensitively, the way EnergyPlus resolves a name, but offered in the
135
+ // casing the model wrote it in, which is what a reader expects to see inserted.
136
+ const key = value.toLowerCase();
137
+ if (seen.has(key))
138
+ return;
139
+ seen.add(key);
140
+ names.push(value);
141
+ };
142
+ for (const object of document.objects()) {
143
+ const type = schema.get(object.typeName);
144
+ if (type === undefined)
145
+ continue;
146
+ if (contributes(type.nref, wanted))
147
+ keep(object.name);
148
+ for (const [field, definition] of Object.entries(type.p)) {
149
+ if (!contributes(definition.ref, wanted))
150
+ continue;
151
+ const value = object.get(field);
152
+ if (typeof value === 'string')
153
+ keep(value);
154
+ }
155
+ }
156
+ return names;
157
+ }
158
+ /** Whether any list this record contributes to is one the field points into. */
159
+ function contributes(declared, wanted) {
160
+ if (declared === undefined)
161
+ return false;
162
+ return declared.some((list) => wanted.has(list));
163
+ }
164
+ /**
165
+ * What the schema says about the field at a positional index.
166
+ *
167
+ * Taken from `describeObjectType`, which is the one place a field's facts are read (FR-029), so
168
+ * that a completion offers what the reference page documents rather than a second opinion about
169
+ * the same bundle.
170
+ *
171
+ * With one hole, and it is worth naming rather than hiding. `describeObjectType` drops the type's
172
+ * positional first field, because Python's `get_field_names` drops it on the assumption that it is
173
+ * always the name. On a named type it is, and the name is free text, so `'unconstrained'` is the
174
+ * right answer there anyway. On the 164 anonymous types it is a real field, and 41 of those are
175
+ * choice fields: `GlobalGeometryRules.starting_vertex_position` is one an author edits by hand.
176
+ * Reporting nothing for those would be silently wrong, so this reads that one field's record
177
+ * directly. Closing the hole properly means changing `describeObjectType` in `@idfkit/core`, which
178
+ * this package does not own.
179
+ */
180
+ function fieldFactsAt(schema, typeName, type, index, prose) {
181
+ const name = fieldNameAt(type, index);
182
+ if (name === undefined)
183
+ return undefined;
184
+ const described = describeObjectType(schema, typeName, prose).fields.find((field) => field.name === name);
185
+ if (described !== undefined) {
186
+ return {
187
+ values: described.enumValues,
188
+ objectList: described.objectList,
189
+ required: described.required,
190
+ prose: described.note,
191
+ };
192
+ }
193
+ const definition = type.p[name] ?? type.x?.p[name];
194
+ if (definition === undefined)
195
+ return undefined;
196
+ return {
197
+ values: permittedValues(definition),
198
+ objectList: definition.ol,
199
+ required: (type.r ?? []).includes(name),
200
+ prose: definition.n === undefined ? undefined : prose?.[definition.n],
201
+ };
202
+ }
203
+ /**
204
+ * The values a field accepts, for the one field `describeObjectType` cannot describe.
205
+ *
206
+ * Reproduces that function's `acceptedValues` exactly, and only exists because it is not reachable
207
+ * from here. `se`, the collapsed `anyOf` string branch, carries the sizing sentinels and wins when
208
+ * it is present; `e`, the choice list, has had the empty string filtered out by the bundle because
209
+ * `e` is what validation checks against, and `eb` records that it was there.
210
+ */
211
+ function permittedValues(field) {
212
+ if (field.se !== undefined)
213
+ return [...field.se];
214
+ if (field.e === undefined)
215
+ return undefined;
216
+ return field.eb === 1 ? ['', ...field.e] : [...field.e];
217
+ }
218
+ /** A type's own prose, resolved against the pool. Nothing is hydrated when no pool was supplied. */
219
+ function memoOf(schema, typeName, prose) {
220
+ if (prose === undefined)
221
+ return undefined;
222
+ const memo = schema.get(typeName)?.m;
223
+ return memo === undefined ? undefined : prose[memo];
224
+ }
225
+ //# sourceMappingURL=complete.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"complete.js","sourceRoot":"","sources":["../src/complete.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ import type { Schema, Statement } from '@idfkit/core';
2
+ /**
3
+ * Which statement an offset falls in, which field, and which part.
4
+ *
5
+ * Everything a cursor answer needs, and nothing a whole file would have to be read to know.
6
+ * `statement` is the one statement the offset sits in, scanned locally; `at` says which part of it
7
+ * the offset is on; `fieldIndex` positions that part; and `typeName` and `fieldName` are what the
8
+ * schema calls them, when a schema was supplied and knows.
9
+ */
10
+ export interface CursorContext {
11
+ /** The statement the offset falls in, scanned locally. */
12
+ readonly statement: Statement;
13
+ /** Where in the statement the offset is. */
14
+ readonly at: 'typeName' | 'field' | 'comment' | 'betweenStatements';
15
+ /**
16
+ * Positional index of the field, when `at` is `'field'`.
17
+ *
18
+ * Counted the same way `Statement.fields` is indexed, so it maps onto schema field order
19
+ * directly: index 0 is the first field after the type name, which on a named type is the name.
20
+ */
21
+ readonly fieldIndex: number | undefined;
22
+ /** The canonical type name, when the schema defines it. */
23
+ readonly typeName: string | undefined;
24
+ /** The schema field name, when the type is known and the index is in range. */
25
+ readonly fieldName: string | undefined;
26
+ }
27
+ /**
28
+ * Where the cursor is, found by scanning outwards from it.
29
+ *
30
+ * **This never builds or consults a `SyntaxLayer`, and it must not start.** Building a layer to
31
+ * answer a cursor is the reparse the whole design exists to avoid: it costs the file, on a
32
+ * keystroke, for an answer that concerns one statement. The committed measurement asks the same
33
+ * question of a large model and of a file a hundredth its size and requires the two to come out
34
+ * within a small factor, which is a check a reparse cannot pass on any machine.
35
+ *
36
+ * The method is a backward scan to the nearest semicolon that is not inside a comment, then a
37
+ * forward scan over that one statement. Both halves are sound only because of what this grammar
38
+ * lacks: IDF has no nesting, no string literals and no escape sequences, so a semicolon terminates
39
+ * a statement unconditionally unless a comment swallowed it, and a comment is an exclamation mark
40
+ * running to the end of its line. Deciding whether a candidate semicolon is real therefore costs
41
+ * the length of its line, and finding the statement costs the distance back to the previous real
42
+ * terminator. Neither number grows with the file. Almost no other grammar would permit this, and
43
+ * a language that gained a string literal would break it silently, so the property is stated here
44
+ * rather than left for a reader to rediscover.
45
+ *
46
+ * The honest bound is the distance back to the previous real terminator rather than the length of
47
+ * the statement. Those are the same number in ordinary files and differ in one real case: a cursor
48
+ * in the first statement of a file that opens with a large comment header scans back through the
49
+ * header. Headers are small, and the case is still linear with a tiny constant.
50
+ *
51
+ * Nothing throws, for any input. An offset outside `[0, text.length]` is clamped into range rather
52
+ * than refused (FR-032): a cursor arrives from an editor that may be a keystroke ahead of the text
53
+ * it was measured against, and answering about the nearest character beats answering nothing.
54
+ *
55
+ * The schema is optional. Without one the context still reports the statement, the part, and the
56
+ * field index, which is what positions a finding about a type no schema defines.
57
+ */
58
+ export declare function contextAt(text: string, offset: number, schema?: Schema): CursorContext;
59
+ //# sourceMappingURL=cursor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor.d.ts","sourceRoot":"","sources":["../src/cursor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,MAAM,EAAY,SAAS,EAAE,MAAM,cAAc,CAAC;AAExE;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,EAAE,UAAU,GAAG,OAAO,GAAG,SAAS,GAAG,mBAAmB,CAAC;IACpE;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,2DAA2D;IAC3D,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,+EAA+E;IAC/E,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CActF"}