@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 +21 -0
- package/README.md +47 -0
- package/dist/complete.d.ts +93 -0
- package/dist/complete.d.ts.map +1 -0
- package/dist/complete.js +225 -0
- package/dist/complete.js.map +1 -0
- package/dist/cursor.d.ts +59 -0
- package/dist/cursor.d.ts.map +1 -0
- package/dist/cursor.js +286 -0
- package/dist/cursor.js.map +1 -0
- package/dist/declaration.d.ts +70 -0
- package/dist/declaration.d.ts.map +1 -0
- package/dist/declaration.js +184 -0
- package/dist/declaration.js.map +1 -0
- package/dist/explain.d.ts +97 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +130 -0
- package/dist/explain.js.map +1 -0
- package/dist/findings.d.ts +58 -0
- package/dist/findings.d.ts.map +1 -0
- package/dist/findings.js +325 -0
- package/dist/findings.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
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"}
|
package/dist/complete.js
ADDED
|
@@ -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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"}
|
package/dist/cursor.d.ts
ADDED
|
@@ -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"}
|