@citisen/litearea 0.1.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.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +514 -0
  3. package/README.zh.md +362 -0
  4. package/dist/grammars.cjs +1228 -0
  5. package/dist/grammars.cjs.map +1 -0
  6. package/dist/grammars.js +1213 -0
  7. package/dist/grammars.js.map +1 -0
  8. package/dist/index.cjs +3103 -0
  9. package/dist/index.cjs.map +1 -0
  10. package/dist/index.js +3040 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/react.cjs +3032 -0
  13. package/dist/react.cjs.map +1 -0
  14. package/dist/react.js +3010 -0
  15. package/dist/react.js.map +1 -0
  16. package/dist/styles.cjs +453 -0
  17. package/dist/styles.cjs.map +1 -0
  18. package/dist/styles.css +432 -0
  19. package/dist/styles.js +447 -0
  20. package/dist/styles.js.map +1 -0
  21. package/dist/types/core/complete.d.ts +70 -0
  22. package/dist/types/core/complete.d.ts.map +1 -0
  23. package/dist/types/core/format.d.ts +34 -0
  24. package/dist/types/core/format.d.ts.map +1 -0
  25. package/dist/types/core/grammar.d.ts +14 -0
  26. package/dist/types/core/grammar.d.ts.map +1 -0
  27. package/dist/types/core/hover.d.ts +23 -0
  28. package/dist/types/core/hover.d.ts.map +1 -0
  29. package/dist/types/core/index.d.ts +12 -0
  30. package/dist/types/core/index.d.ts.map +1 -0
  31. package/dist/types/core/inspect.d.ts +34 -0
  32. package/dist/types/core/inspect.d.ts.map +1 -0
  33. package/dist/types/core/rank.d.ts +82 -0
  34. package/dist/types/core/rank.d.ts.map +1 -0
  35. package/dist/types/core/scan.d.ts +51 -0
  36. package/dist/types/core/scan.d.ts.map +1 -0
  37. package/dist/types/core/segments.d.ts +44 -0
  38. package/dist/types/core/segments.d.ts.map +1 -0
  39. package/dist/types/core/text.d.ts +113 -0
  40. package/dist/types/core/text.d.ts.map +1 -0
  41. package/dist/types/core/types.d.ts +604 -0
  42. package/dist/types/core/types.d.ts.map +1 -0
  43. package/dist/types/core/vocabulary.d.ts +82 -0
  44. package/dist/types/core/vocabulary.d.ts.map +1 -0
  45. package/dist/types/dom/create.d.ts +17 -0
  46. package/dist/types/dom/create.d.ts.map +1 -0
  47. package/dist/types/dom/editing.d.ts +89 -0
  48. package/dist/types/dom/editing.d.ts.map +1 -0
  49. package/dist/types/dom/editor.d.ts +366 -0
  50. package/dist/types/dom/editor.d.ts.map +1 -0
  51. package/dist/types/dom/index.d.ts +9 -0
  52. package/dist/types/dom/index.d.ts.map +1 -0
  53. package/dist/types/dom/mirror.d.ts +107 -0
  54. package/dist/types/dom/mirror.d.ts.map +1 -0
  55. package/dist/types/dom/overlay.d.ts +52 -0
  56. package/dist/types/dom/overlay.d.ts.map +1 -0
  57. package/dist/types/dom/popup.d.ts +95 -0
  58. package/dist/types/dom/popup.d.ts.map +1 -0
  59. package/dist/types/dom/support.d.ts +41 -0
  60. package/dist/types/dom/support.d.ts.map +1 -0
  61. package/dist/types/dom/tooltip.d.ts +39 -0
  62. package/dist/types/dom/tooltip.d.ts.map +1 -0
  63. package/dist/types/grammars/dshFont.d.ts +127 -0
  64. package/dist/types/grammars/dshFont.d.ts.map +1 -0
  65. package/dist/types/grammars/dshSentry.d.ts +84 -0
  66. package/dist/types/grammars/dshSentry.d.ts.map +1 -0
  67. package/dist/types/grammars/index.d.ts +3 -0
  68. package/dist/types/grammars/index.d.ts.map +1 -0
  69. package/dist/types/index.d.ts +15 -0
  70. package/dist/types/index.d.ts.map +1 -0
  71. package/dist/types/react/index.d.ts +91 -0
  72. package/dist/types/react/index.d.ts.map +1 -0
  73. package/dist/types/styles.d.ts +29 -0
  74. package/dist/types/styles.d.ts.map +1 -0
  75. package/docs/architecture.md +316 -0
  76. package/docs/completion.md +320 -0
  77. package/docs/grammar.md +823 -0
  78. package/package.json +105 -0
  79. package/scripts/browser-check.mjs +838 -0
  80. package/scripts/build-css.mjs +35 -0
  81. package/scripts/release.mjs +91 -0
  82. package/scripts/verify-package.mjs +253 -0
  83. package/src/core/complete.ts +286 -0
  84. package/src/core/format.ts +71 -0
  85. package/src/core/grammar.ts +40 -0
  86. package/src/core/hover.ts +129 -0
  87. package/src/core/index.ts +98 -0
  88. package/src/core/inspect.ts +198 -0
  89. package/src/core/rank.ts +317 -0
  90. package/src/core/scan.ts +720 -0
  91. package/src/core/segments.ts +185 -0
  92. package/src/core/text.ts +238 -0
  93. package/src/core/types.ts +681 -0
  94. package/src/core/vocabulary.ts +196 -0
  95. package/src/dom/create.ts +31 -0
  96. package/src/dom/editing.ts +213 -0
  97. package/src/dom/editor.ts +1143 -0
  98. package/src/dom/index.ts +46 -0
  99. package/src/dom/mirror.ts +305 -0
  100. package/src/dom/overlay.ts +106 -0
  101. package/src/dom/popup.ts +323 -0
  102. package/src/dom/support.ts +88 -0
  103. package/src/dom/tooltip.ts +112 -0
  104. package/src/grammars/dshFont.ts +1004 -0
  105. package/src/grammars/dshSentry.ts +742 -0
  106. package/src/grammars/index.ts +57 -0
  107. package/src/index.ts +122 -0
  108. package/src/react/index.tsx +248 -0
  109. package/src/styles.ts +529 -0
@@ -0,0 +1,681 @@
1
+ // ─── litearea: the grammar contract ─────────────────────────────────────────
2
+ //
3
+ // Everything litearea knows about a language lives in ONE object, and the engine
4
+ // knows nothing else. There is no built-in syntax, no bundled tokenizer, and no
5
+ // language identifier to switch on: a grammar is data the caller writes, and the
6
+ // editor is a function of it.
7
+ //
8
+ // The reason the whole language is one object rather than several registrations
9
+ // is the failure this library exists to correct. The editors it replaces derived
10
+ // the painted tokens, the diagnostics, and the completion list three separate
11
+ // times from the same text, by three separate passes that could — and did —
12
+ // disagree: a word painted as a family while the completer thought it was
13
+ // unknown, a caret offset counted in one coordinate system and applied in
14
+ // another. Here `analyze` runs ONCE per text, its result is handed to every
15
+ // feature that needs it, and the features cannot drift because they are looking
16
+ // at the same value.
17
+ //
18
+ // rules → what is painted (lexical, declarative)
19
+ // analyze → what the document MEANS (structural, one pass, optional)
20
+ // diagnose → what is wrong with it (declarative checks + a hook)
21
+ // compose → what can come next (completion sources)
22
+ // describe → what a thing is (hover tooltips)
23
+ // decorate → what is semantically true (ranges that are not tokens)
24
+ //
25
+ // Every hook is optional. A grammar with nothing but `rules` is a plain
26
+ // highlighter; add `compose` and it completes; add `diagnose` and it complains.
27
+
28
+ /** A half-open range of character offsets into the document. `to` is exclusive. */
29
+ export interface Range {
30
+ /** The first character, inclusive. */
31
+ from: number
32
+ /** The character after the last one, exclusive. */
33
+ to: number
34
+ }
35
+
36
+ /**
37
+ * The name a token is painted under.
38
+ *
39
+ * A scope is a plain string and the engine never interprets it: it becomes a CSS
40
+ * class (`litearea-scope-<scope>`), and a stylesheet decides what that looks
41
+ * like. Dots are kept as written so a grammar can group its own vocabulary
42
+ * (`value.color`, `value.shape`) and theme the group at once.
43
+ */
44
+ export type Scope = string
45
+
46
+ /** How loudly a diagnostic speaks. Mirrors the four levels an editor shows. */
47
+ export type Severity = 'error' | 'warning' | 'info' | 'hint'
48
+
49
+ /** One painted span. `text` always equals `text.slice(from, to)`. */
50
+ export interface Token extends Range {
51
+ scope: Scope
52
+ text: string
53
+ /** Zero-based line number. */
54
+ line: number
55
+ /** Zero-based column, counted in characters rather than in display width. */
56
+ column: number
57
+ /**
58
+ * The scope of the innermost region this token sits inside, when it is inside
59
+ * one. A grammar's validator reads it to tell a word in a comment from the same
60
+ * word in the code around it.
61
+ */
62
+ region?: Scope
63
+ }
64
+
65
+ /**
66
+ * A span the grammar marks for a reason of its own — VSCode calls these semantic
67
+ * tokens and they are deliberately not tokens here.
68
+ *
69
+ * The distinction earns its keep. A token is what the *characters* are; a
70
+ * decoration is what they *mean*, and the two change on different schedules. The
71
+ * font-query grammar paints `Geist Mono` as a family from the characters alone,
72
+ * but which family is *in effect* depends on the installed catalogue — the same
73
+ * characters mean something else on another machine. Painting that as a token
74
+ * would mean re-lexing the document whenever the catalogue changed; painting it
75
+ * as a decoration means recomputing one range list, which is what it is.
76
+ */
77
+ export interface Decoration extends Range {
78
+ /** The class suffix: the span is rendered as `litearea-dec-<kind>`. */
79
+ kind: string
80
+ /**
81
+ * A short label for the mark, shown as the tooltip's heading.
82
+ *
83
+ * It is combined with whatever the grammar's `describe` says about the token rather than
84
+ * replacing it, because the two answer different questions: the mark says what the
85
+ * grammar concluded ("in effect: Geist Mono") and the description says what the token is
86
+ * (its faces, its weight). A hover that showed only one of them would always be missing
87
+ * the half the reader wanted.
88
+ */
89
+ title?: string
90
+ }
91
+
92
+ /** A problem the grammar found, with the range that should be underlined. */
93
+ export interface Diagnostic extends Range {
94
+ severity: Severity
95
+ message: string
96
+ /**
97
+ * A stable machine-readable tag, so a host can react to a specific problem
98
+ * without matching on prose, and so tests can assert on meaning rather than on
99
+ * wording.
100
+ */
101
+ code?: string
102
+ /** A second paragraph, shown under the message in a tooltip. */
103
+ detail?: string
104
+ /** Who raised it. Defaults to the grammar id. */
105
+ source?: string
106
+ }
107
+
108
+ // ── the vocabulary ──────────────────────────────────────────────────────────
109
+
110
+ /** What one word of a vocabulary explains about itself when the pointer rests on it. */
111
+ export interface VocabularyEntry {
112
+ /** A short signature line, shown first and emphasised. */
113
+ title?: string
114
+ /** A dimmed line under the title. */
115
+ detail?: string
116
+ /** The body text. Plain text: litearea does not render markdown. */
117
+ body?: string
118
+ }
119
+
120
+ /**
121
+ * The words a rule may accept, resolved at scan time.
122
+ *
123
+ * A function is how a vocabulary that lives outside the document — the fonts
124
+ * actually installed, the states the host actually has — reaches the grammar
125
+ * without the grammar holding state of its own. It is called once per scan with
126
+ * the grammar's analysis, so a dynamic vocabulary costs one call, not one per
127
+ * token.
128
+ */
129
+ export type WordsSource<State = unknown> =
130
+ | readonly string[]
131
+ | ResolvedVocabulary<State>
132
+ | ((context: VocabularyContext<State>) => readonly string[])
133
+
134
+ /** What a vocabulary resolution is allowed to look at. */
135
+ export interface VocabularyContext<State = unknown> {
136
+ text: string
137
+ /** Whatever `Grammar.analyze` returned. */
138
+ state: State
139
+ }
140
+
141
+ /**
142
+ * A vocabulary after {@link defineVocabulary} has filled in its defaults.
143
+ *
144
+ * It carries four facts that must agree — the word set, the scope a member is
145
+ * painted with, the message a non-member earns, and the documentation a member
146
+ * shows — which is exactly why they are declared together. Split across four
147
+ * places they drift; declared once they cannot.
148
+ */
149
+ export interface ResolvedVocabulary<State = unknown> {
150
+ readonly id: string
151
+ /** Whether membership is case-sensitive. */
152
+ readonly caseSensitive: boolean
153
+ /** Every member, in declaration order. */
154
+ resolve(context: VocabularyContext<State>): readonly string[]
155
+ /** Whether a word is a member. */
156
+ has(word: string, context: VocabularyContext<State>): boolean
157
+ /** The scope a member is painted with. */
158
+ scopeFor(word: string): Scope
159
+ /** The scope a non-member is painted with, when the vocabulary rejects one. */
160
+ readonly unknownScope: Scope | undefined
161
+ /** The diagnostic a non-member earns, or `undefined` for silence. */
162
+ reject(
163
+ word: string,
164
+ context: VocabularyContext<State>,
165
+ ): { message: string; severity: Severity; code: string } | undefined
166
+ /** What a member explains about itself, for hover. */
167
+ entryFor(word: string): VocabularyEntry | undefined
168
+ /** How a word is written into the document when it is accepted. */
169
+ format(word: string): string
170
+ }
171
+
172
+ // ── rules: what gets painted ────────────────────────────────────────────────
173
+
174
+ /**
175
+ * Where a rule is allowed to match. Every predicate that is present must hold.
176
+ *
177
+ * The predicates are about *position*, not about the parse. That boundary is
178
+ * deliberate: a rule that could ask "am I inside a rule named running?" would
179
+ * make the highlighter a parser, and the whole point of `analyze` is that the
180
+ * parse happens once, in a function the grammar author can read and test.
181
+ */
182
+ export interface RuleContext {
183
+ /** Only when nothing but whitespace precedes it on its line. */
184
+ firstOnLine?: boolean
185
+ /** Only when the nearest preceding token has one of these scopes. */
186
+ after?: readonly Scope[]
187
+ /** Only when the nearest preceding token does NOT have one of these scopes. */
188
+ notAfter?: readonly Scope[]
189
+ /** Only when the whole line matches. Anchored at the start. */
190
+ line?: RegExp
191
+ /** Only when the caret-facing column is at least this many characters in. */
192
+ minColumn?: number
193
+ /** Only when the column is at most this many characters in. */
194
+ maxColumn?: number
195
+ /**
196
+ * Only when the character immediately before the match is not one of these.
197
+ * `prevNot: '\\w'` is the common case: it is how a keyword rule is stopped
198
+ * from matching the tail of a longer word.
199
+ */
200
+ prevNot?: string
201
+ }
202
+
203
+ /** A scope is either fixed, or decided per match by a function. */
204
+ export type ScopeSpec<State = unknown> =
205
+ | Scope
206
+ | ((match: RuleMatch<State>) => Scope)
207
+
208
+ /** What a scope function is told about the match it is naming. */
209
+ export interface RuleMatch<State = unknown> {
210
+ /** The matched text. */
211
+ text: string
212
+ /** The document. */
213
+ source: string
214
+ /** Where the match starts. */
215
+ from: number
216
+ /** The capture groups, when the pattern had any. Index 0 is the whole match. */
217
+ groups: readonly (string | undefined)[]
218
+ /** Whatever `Grammar.analyze` returned. */
219
+ state: State
220
+ }
221
+
222
+ /** A regular-expression token: the workhorse. */
223
+ export interface MatchRule<State = unknown> {
224
+ kind: 'match'
225
+ scope: ScopeSpec<State>
226
+ /**
227
+ * Tried at the current position. The engine adds the sticky flag, so the
228
+ * pattern never has to be anchored by hand and can never skip ahead.
229
+ */
230
+ pattern: RegExp
231
+ when?: RuleContext
232
+ }
233
+
234
+ /**
235
+ * A vocabulary token: a set of words, matched as a unit.
236
+ *
237
+ * It is its own rule kind rather than sugar over `match` because a word has
238
+ * three properties a regex cannot express. It can be resolved at scan time from
239
+ * data outside the document; it can span several words when a name contains a
240
+ * space; and a word that is NOT in the set is a fact worth reporting, which is
241
+ * where most diagnostics come from.
242
+ */
243
+ export interface WordsRule<State = unknown> {
244
+ kind: 'words'
245
+ words: WordsSource<State>
246
+ /** Overrides the vocabulary's own scope for this rule only. */
247
+ scope?: ScopeSpec<State>
248
+ /**
249
+ * Allows one entry to span several words (`IBM Plex Mono`).
250
+ *
251
+ * The engine tries the longest span first and keeps the longest member it finds, so a
252
+ * catalogue holding both `IBM Plex` and `IBM Plex Mono` resolves the longer name. Words
253
+ * are separated by any run of whitespace that does not contain a newline; the separators
254
+ * are part of the matched span and are painted with the member's own scope.
255
+ *
256
+ * Setting `phrase` also stops a multi-word member being matched by the literal fallback,
257
+ * so `max` really is a cap rather than a suggestion.
258
+ */
259
+ phrase?: {
260
+ /**
261
+ * The most words one entry may span. Default 4, which covers every real font family in
262
+ * the shipped catalogue (`Source Han Serif SC` is four).
263
+ */
264
+ max?: number
265
+ }
266
+ when?: RuleContext
267
+ /**
268
+ * What to do with a word-shaped token that is not a member.
269
+ *
270
+ * Absent, the rule simply does not match and a later rule gets its turn —
271
+ * which is what a grammar wants when two vocabularies overlap. Present, the
272
+ * token is painted with `scope` and earns `message`, which is how a typo
273
+ * becomes a red underline instead of quietly falling through to plain text.
274
+ */
275
+ unknown?: {
276
+ scope?: Scope
277
+ message?: string | ((word: string, context: RuleMatch<State>) => string)
278
+ severity?: Severity
279
+ code?: string
280
+ }
281
+ }
282
+
283
+ /**
284
+ * A delimited region: a block comment, a multi-line string, a heredoc.
285
+ *
286
+ * The engine scans for `end` from the end of `begin` and never re-enters the rule list in
287
+ * between (unless `transparent`), so the contents of a string cannot be mistaken for the
288
+ * language around it. `nested` widens that by one delimiter — the region may open inside
289
+ * itself — and no further. An `end` that never arrives runs the region to the end of the
290
+ * document and raises `unclosed`, because an unterminated block comment is a mistake and not
291
+ * an invitation to tint the rest of the file.
292
+ */
293
+ export interface RegionRule<State = unknown> {
294
+ kind: 'region'
295
+ /** The scope of the whole region when the other three are not given. */
296
+ scope?: ScopeSpec<State>
297
+ /** The opening delimiter. */
298
+ begin: RegExp
299
+ /** The closing delimiter, searched for rather than anchored. */
300
+ end: RegExp
301
+ /** The scope of the opening delimiter. Defaults to `scope`. */
302
+ openScope?: ScopeSpec<State>
303
+ /** The scope of the closing delimiter. Defaults to `scope`. */
304
+ closeScope?: ScopeSpec<State>
305
+ /** The scope of what lies between. Defaults to `scope`. */
306
+ contentScope?: ScopeSpec<State>
307
+ /**
308
+ * Whether the same rule may open again inside itself. Default `false`.
309
+ *
310
+ * Nesting and transparency are different things, and conflating them is how a block
311
+ * comment stops being a comment: a language whose comments nest wants the delimiters
312
+ * matched and everything else inside treated as prose. So this flag adds ONLY the
313
+ * delimiter — the contents stay opaque unless `transparent` says otherwise.
314
+ */
315
+ nested?: boolean
316
+ /**
317
+ * Whether the grammar's other rules also apply inside this region. Default `false`.
318
+ *
319
+ * The flag for a region that is not prose: a template in another language, or a string
320
+ * with escapes to colour. It implies nesting, because the rule list this exposes includes
321
+ * the rule that opened the region.
322
+ */
323
+ transparent?: boolean
324
+ when?: RuleContext
325
+ /** What an unterminated region reports. */
326
+ unclosed?: {
327
+ scope?: Scope
328
+ message?: string | ((match: RuleMatch<State>) => string)
329
+ severity?: Severity
330
+ code?: string
331
+ }
332
+ }
333
+
334
+ /** One production of a grammar's lexical rules. Tried in order; first match wins. */
335
+ export type Rule<State = unknown> = MatchRule<State> | WordsRule<State> | RegionRule<State>
336
+
337
+ // ── diagnostics ─────────────────────────────────────────────────────────────
338
+
339
+ /** A declarative check: a shape a token must have, or a vocabulary it must join. */
340
+ export interface CheckRule<State = unknown> {
341
+ /** A stable code, copied onto every diagnostic the check raises. */
342
+ code: string
343
+ severity?: Severity
344
+ /**
345
+ * The tokens this check applies to, by scope. `*` matches every scope.
346
+ * The check runs on the token's own text.
347
+ */
348
+ scopes: readonly Scope[]
349
+ /**
350
+ * The words that are acceptable. A token in `scopes` whose text is not a
351
+ * member is reported.
352
+ */
353
+ allow?: WordsSource<State>
354
+ /** The message. `{word}` and `{allowed}` are substituted. */
355
+ message: string
356
+ detail?: string
357
+ /** Skips tokens whose text matches, so a check can carve out its exceptions. */
358
+ except?: RegExp
359
+ /** Runs at most once per line, on the first token that matches `scopes`. */
360
+ perLine?: boolean
361
+ }
362
+
363
+ /** What a grammar's own validator is handed. */
364
+ export interface DiagnoseContext<State = unknown> {
365
+ text: string
366
+ tokens: readonly Token[]
367
+ /** The result of `Grammar.analyze`, or the grammar's initial state. */
368
+ state: State
369
+ /** Raises a diagnostic. The grammar supplies the range. */
370
+ report(problem: {
371
+ from: number
372
+ to: number
373
+ message: string
374
+ severity?: Severity
375
+ code?: string
376
+ detail?: string
377
+ }): void
378
+ }
379
+
380
+ // ── completion ──────────────────────────────────────────────────────────────
381
+
382
+ /** One row of the completion list. */
383
+ export interface SuggestionItem {
384
+ /** The text the row shows and, unless `insert` says otherwise, writes. */
385
+ label: string
386
+ /** The text written into the document. Defaults to `label`. */
387
+ insert?: string
388
+ /** A short dimmed annotation at the right edge, as VSCode's `detail` is. */
389
+ detail?: string
390
+ /** The paragraph shown in the documentation panel beside or below the list. */
391
+ documentation?: string
392
+ /** A kind name, for the icon and the colour: `litearea-kind-<kind>`. */
393
+ kind?: string
394
+ /**
395
+ * The ranking bucket.
396
+ *
397
+ * Rows are ordered by this key first and by match score only within an equal key, which
398
+ * is how a grammar puts a whole group on top — the value already in effect, say —
399
+ * without pretending its label starts with a `0`.
400
+ *
401
+ * Set it on every row or on none. When some rows set it and others do not, the ones that
402
+ * do not are keyed by their LABEL, so their labels are compared against the other rows'
403
+ * sort keys rather than against their labels. That is a comparison between two different
404
+ * things, and it is not what anyone means.
405
+ */
406
+ sortText?: string
407
+ /** Matched against the needle instead of `label`. */
408
+ filterText?: string
409
+ /**
410
+ * `replace` (the default) writes over the replaced range; `before` writes
411
+ * ahead of it and keeps it. `before` is how a value is inserted in front of an
412
+ * existing one without destroying it — promoting a fallback font, or adding a
413
+ * state above a line that already exists.
414
+ */
415
+ mode?: 'replace' | 'before'
416
+ /** Text appended after the insert, such as `', '` to invite another entry. */
417
+ append?: string
418
+ /** Characters that accept this row when typed, as VSCode's commit characters are. */
419
+ commitCharacters?: string
420
+ /**
421
+ * Where the caret lands, counted back from the end of everything written.
422
+ * Negative moves it left, which is how a grammar leaves the caret inside a
423
+ * pair of delimiters it just wrote.
424
+ */
425
+ caretOffset?: number
426
+ /** Opaque payload handed back to `Grammar.onAccept`. */
427
+ data?: unknown
428
+ }
429
+
430
+ /** The word the caret sits in, and the range a completion would replace. */
431
+ export interface WordInfo extends Range {
432
+ /** The word's text. */
433
+ text: string
434
+ /** What precedes the caret inside the word. Empty at the word's start. */
435
+ prefix: string
436
+ /** What follows the caret inside the word. Empty at the word's end. */
437
+ suffix: string
438
+ }
439
+
440
+ /** One line of the document, and where the caret is on it. */
441
+ export interface LineInfo extends Range {
442
+ text: string
443
+ /** Zero-based line number. */
444
+ number: number
445
+ /** The caret's zero-based offset within the line. */
446
+ column: number
447
+ /** The line's text before the caret. */
448
+ before: string
449
+ /** The line's text after the caret. */
450
+ after: string
451
+ }
452
+
453
+ /**
454
+ * How the popup came to be open — which decides what may keep it open.
455
+ *
456
+ * There is no `'commit'` member, and that is deliberate: the DOM layer never produced
457
+ * one, and a union member nothing can return is a promise the code does not keep. A
458
+ * commit character accepts the row and closes the list; whatever the user types next is
459
+ * an ordinary `'auto'` trigger.
460
+ */
461
+ export type CompletionTrigger =
462
+ /** Opened by typing a character. */
463
+ | 'auto'
464
+ /** Opened by the user asking (Ctrl+Space), with no needle. */
465
+ | 'explicit'
466
+
467
+ /** Everything a completion source may look at. */
468
+ export interface CompletionContext<State = unknown> {
469
+ text: string
470
+ caret: number
471
+ /** The word around the caret. */
472
+ word: WordInfo
473
+ /** The line around the caret. */
474
+ line: LineInfo
475
+ /** The tokens for the whole document. */
476
+ tokens: readonly Token[]
477
+ /** The diagnostics for the whole document. */
478
+ diagnostics: readonly Diagnostic[]
479
+ /** Whatever `Grammar.analyze` returned. */
480
+ state: State
481
+ /** The scope of the token under the caret, when the caret is in one. */
482
+ scope: Scope | undefined
483
+ /** The scope of the nearest token before the caret. */
484
+ scopeBefore: Scope | undefined
485
+ /** Whether nothing but whitespace precedes the caret on its line. */
486
+ firstOnLine: boolean
487
+ /**
488
+ * Whether the caret is inside the FIRST word of its line, or before it.
489
+ *
490
+ * The difference from `firstOnLine` is the whole reason both exist. A
491
+ * line-oriented language puts something special at the head of every line, and a
492
+ * completion for it has to stay eligible while that head is being spelled out:
493
+ * `runn|` is no longer "at the start of the line" in the strict sense, but it is
494
+ * unmistakably completing the first word. A source that asked `firstOnLine` would
495
+ * switch itself off after the very first keystroke — which is exactly the bug this
496
+ * field was added to fix, and exactly the kind of thing a reference grammar is
497
+ * for finding.
498
+ */
499
+ firstWord: boolean
500
+ /** The first non-whitespace token on the caret's line, when there is one. */
501
+ firstToken: Token | undefined
502
+ trigger: CompletionTrigger
503
+ }
504
+
505
+ /**
506
+ * One way the grammar offers completions.
507
+ *
508
+ * Several sources may be declared. The first eligible one that returns rows wins
509
+ * unless it asks to merge, which keeps the common case — "here is the list" —
510
+ * free of any merging logic while still allowing a document-wide source (every
511
+ * property already used elsewhere) to sit alongside a vocabulary.
512
+ */
513
+ export interface CompletionSource<State = unknown> {
514
+ /** A stable id, for tests and for the host's telemetry. */
515
+ id: string
516
+ /** Eligible only when every predicate holds. Default: always eligible. */
517
+ when?: (context: CompletionContext<State>) => boolean
518
+ /**
519
+ * The range the chosen row replaces.
520
+ *
521
+ * A function of the context, so it is recomputed from the caret EVERY time the list is
522
+ * filtered. It has to grow with the word being typed: type `ru`, accept `running`, and the
523
+ * replacement must cover both letters, or the result is the completion followed by a
524
+ * leftover character. What stays stable while the user types is the SOURCE — see
525
+ * `CompletionRequest.previousSourceId` — not the range.
526
+ */
527
+ range: Range | ((context: CompletionContext<State>) => Range)
528
+ /** The rows, in whatever order the grammar wants them scored. */
529
+ items: (context: CompletionContext<State>) => readonly SuggestionItem[]
530
+ /** Higher wins when several sources are eligible and none merges. Default 0. */
531
+ priority?: number
532
+ /** Adds this source's rows to the others' instead of competing with them. */
533
+ merge?: boolean
534
+ }
535
+
536
+ /** One row of a resolved completion list, with the evidence for its position. */
537
+ export interface CompletionRow {
538
+ item: SuggestionItem
539
+ /** The ranking score. Higher is better. */
540
+ score: number
541
+ /** The offsets in the row's label the needle matched, for emphasis. */
542
+ indices: number[]
543
+ }
544
+
545
+ /** A resolved completion: the range recomputed for this filter, and the rows worth showing. */
546
+ export interface Completion {
547
+ /**
548
+ * The range a chosen row replaces.
549
+ *
550
+ * Recomputed from the caret every time the list is filtered, never held over from an
551
+ * earlier keystroke. That is the whole difference between a completer that works and
552
+ * one that leaves debris: type `ru`, accept `running`, and the range has to cover both
553
+ * letters. Holding the range resolved when the list opened replaces only the `r` and
554
+ * produces `running u`.
555
+ */
556
+ range: Range
557
+ rows: readonly CompletionRow[]
558
+ /** The needle the rows were filtered against. */
559
+ needle: string
560
+ /** The source that produced them, for tests and for the host. */
561
+ sourceId: string
562
+ }
563
+
564
+ // ── hover ───────────────────────────────────────────────────────────────────
565
+
566
+ /** What a hover shows. Every field is plain text; nothing is rendered as markdown. */
567
+ export interface HoverInfo {
568
+ /** A short signature line, shown first and emphasised. */
569
+ title?: string
570
+ /** A dimmed line under the title. */
571
+ detail?: string
572
+ /** The paragraph below. */
573
+ body?: string
574
+ /** The range the hover describes. Defaults to the token under the pointer. */
575
+ range?: Range
576
+ /** Which part of the editor raised it, for styling. */
577
+ kind?: 'token' | 'diagnostic' | 'decoration' | 'grammar'
578
+ }
579
+
580
+ /** Everything a grammar's hover may look at. */
581
+ export interface HoverContext<State = unknown> {
582
+ text: string
583
+ /** The offset the pointer resolved to, or the caret for a keyboard hover. */
584
+ offset: number
585
+ /** The token under the offset, when there is one. */
586
+ token: Token | undefined
587
+ /** The word under the offset. */
588
+ word: WordInfo
589
+ /** The line under the offset. */
590
+ line: LineInfo
591
+ tokens: readonly Token[]
592
+ /**
593
+ * Every problem the document has.
594
+ *
595
+ * The one UNDER the offset is not offered separately, and that is a fact about the order
596
+ * of business rather than an omission: a diagnostic outranks a description, so `describe`
597
+ * is only ever called when nothing is wrong at that offset. A grammar that wants to know
598
+ * what else is flagged can read this list.
599
+ */
600
+ diagnostics: readonly Diagnostic[]
601
+ state: State
602
+ }
603
+
604
+ // ── the grammar ─────────────────────────────────────────────────────────────
605
+
606
+ /**
607
+ * A language, complete.
608
+ *
609
+ * `State` is whatever `analyze` returns and is threaded to every other hook, so
610
+ * a grammar author writes the parse once and reads it everywhere with full
611
+ * types. Leaving it out makes the state `unknown`, which is what a grammar with
612
+ * no `analyze` wants.
613
+ */
614
+ export interface Grammar<State = unknown> {
615
+ /** A stable id, used as the default diagnostic `source` and for debugging. */
616
+ id: string
617
+ /** A human name, for a status line or a demo. */
618
+ name?: string
619
+
620
+ /**
621
+ * The lexical rules, tried in order at each position. First match wins, so a
622
+ * rule that must not shadow another belongs above it.
623
+ */
624
+ rules: readonly Rule<State>[]
625
+
626
+ /** The scope for a character no rule claimed. Default `'text'`. */
627
+ fallbackScope?: Scope
628
+
629
+ /**
630
+ * Which characters form a word.
631
+ *
632
+ * It matters more than it looks: this predicate decides what a completion
633
+ * replaces, what a diagnostic underlines, and where a double-click puts the
634
+ * selection. A language whose names contain dots or hyphens must say so, or
635
+ * every completion will replace one segment of a name instead of the name.
636
+ */
637
+ wordChars?: RegExp
638
+
639
+ /** The initial state, used before `analyze` exists and when it is skipped. */
640
+ initialState?: State
641
+
642
+ /**
643
+ * The single structural pass over the document.
644
+ *
645
+ * Called once per text, before the scan, and its result is handed to every rule and to
646
+ * `checks`, `validate`, `compose`, `describe`, and `decorate` alike — which is what keeps
647
+ * them from disagreeing about what the document says.
648
+ *
649
+ * It receives the text and NOT the tokens, deliberately. Handing it tokens
650
+ * would make the scan depend on the analysis that depends on the scan, and the
651
+ * only ways out of that cycle are a second pass or a fixpoint, both of which
652
+ * mean the analysis can disagree with the paint. A structural pass over a line
653
+ * oriented language does not need the lexical result anyway: splitting lines
654
+ * and words is cheaper than asking the scanner to do it again.
655
+ */
656
+ analyze?: (text: string) => State
657
+
658
+ /** Declarative checks, run over the tokens. */
659
+ checks?: readonly CheckRule<State>[]
660
+
661
+ /**
662
+ * The grammar's own validator, for what a check cannot express: a duplicated
663
+ * value, a weight the chosen family does not have, a slot filled twice.
664
+ */
665
+ validate?: (context: DiagnoseContext<State>) => void
666
+
667
+ /** The ways this grammar offers completions. */
668
+ compose?: readonly CompletionSource<State>[]
669
+
670
+ /** What a thing is, when the pointer rests on it. */
671
+ describe?: (context: HoverContext<State>) => HoverInfo | null | undefined
672
+
673
+ /** Semantic ranges that are not tokens. */
674
+ decorate?: (text: string, state: State) => readonly Decoration[]
675
+
676
+ /**
677
+ * Called after a row is accepted, with the text that resulted. The place to
678
+ * sync a host's own state — a stored value, a preview — without re-parsing.
679
+ */
680
+ onAccept?: (result: { text: string; caret: number; item: SuggestionItem; state: State }) => void
681
+ }