@kalada/codemirror 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @kalada/codemirror
2
2
 
3
- Thin CodeMirror 6 lifecycle and protocol translation for `@kalada/language-service`.
3
+ Reusable CodeMirror 6 lifecycle and tooling integration, with a backward-compatible Kalada facade.
4
4
 
5
5
  ```ts
6
6
  const session = createKaladaEditorSession({
@@ -15,6 +15,131 @@ including undo and redo, become monotonic language-service revisions. `replaceDo
15
15
  `refreshEnvironment`, and `dispose` preserve request identity and stale/cancellation checks. Dispose is
16
16
  idempotent; destroying a view detaches it without closing the session.
17
17
 
18
+ ### Kalada bridge failures
19
+
20
+ The bridge assumes exclusive ownership of its URI and a truthful, synchronous `getDocument`.
21
+ If a detached update throws, it rethrows the original error and does not notify for that attempt.
22
+ When the service still exactly matches the previous URI/version/text and no neutral snapshot change
23
+ intervened, the provisional snapshot is restored. When the service matches the latest neutral snapshot
24
+ (including committed nested edits), that state remains usable. Successful descendants retain FIFO
25
+ notifications with their exact historical service snapshots. Unmatched or unobservable state makes
26
+ the session terminally failed; intervening environment refreshes or edits are never rolled back.
27
+
28
+ An attached bridge exception is always terminal, even after a service commit: CodeMirror cannot undo
29
+ its view update and may disable the throwing plugin. The original exception is reported through
30
+ CodeMirror's exception sink (`EditorView.exceptionSink`); `view.dispatch` need not throw. Subsequent
31
+ replacement, formatting, environment refresh and attachment explicitly fail. Tooling is invalidated
32
+ without dispatch inside the plugin update; presentation cleanup runs after the update boundary.
33
+ Earlier successful transactions in a batch still notify, but the failed and later transactions do not.
34
+ No further service synchronization or tooling publication occurs, and disposal remains idempotent.
35
+ There is no retry, compensating update, or close/reopen recovery. Arbitrary lying/proxy services and
36
+ transactional redesign are outside this bounded contract.
37
+
18
38
  CodeMirror packages are peers so consumers use one set of state classes. This package has no parser,
19
39
  schema traversal, inference, evaluation, filesystem, or network behavior. Tooltips use DOM text nodes,
20
40
  and the extension includes keyboard completion, keyboard hover, Escape handling, and accessible labels.
41
+
42
+ ## Language-neutral entry point
43
+
44
+ ```ts
45
+ import { createEditorSession, type EditorProvider } from "@kalada/codemirror/editor";
46
+ import { EditorView } from "@codemirror/view";
47
+
48
+ // Independent color fixture: no Kalada language-service contracts.
49
+ const provider: EditorProvider = {
50
+ diagnostics: ({ snapshot }) => ({
51
+ ...snapshot,
52
+ value: snapshot.text === "red" ? [] : [
53
+ { from: 0, to: snapshot.text.length, severity: "warning", message: "Expected red" },
54
+ ],
55
+ }),
56
+ completion: ({ snapshot }) => ({
57
+ ...snapshot,
58
+ value: [{ label: "red", edit: { from: 0, to: snapshot.text.length, text: "red" } }],
59
+ }),
60
+ hover: ({ snapshot }) => ({
61
+ ...snapshot, value: { from: 0, to: snapshot.text.length, content: "A color" },
62
+ }),
63
+ format: ({ snapshot }) => ({
64
+ ...snapshot, value: { from: 0, to: snapshot.text.length, text: snapshot.text.trim() },
65
+ }),
66
+ };
67
+ const session = createEditorSession({
68
+ document: { uri: "memory:///color.txt", version: 1, text: "red" }, provider,
69
+ ariaLabel: "Color source", onDocumentChange: (snapshot) => console.log(snapshot.text),
70
+ });
71
+ const view = new EditorView({ doc: session.getSnapshot().text, extensions: session.extension, parent });
72
+ const didFormat = await session.format();
73
+ ```
74
+
75
+ `EditorSessionOptions` accepts `document`, optional `provider`, `ariaLabel`, presentation `extensions`,
76
+ and `onDocumentChange`. `EditorSession` exposes `extension`, `getSnapshot`, `replaceDocument`,
77
+ `refreshEnvironment`, `format`, and `dispose`. Each provider capability is optional; no provider is
78
+ required for editing/history. Neutral sessions own their snapshots, not a compiler's open-document store.
79
+ The Kalada facade still owns its language-service URI, callback snapshots and synchronous `format(): boolean`.
80
+
81
+ Providers receive `{ snapshot, signal }` plus a source offset for completion/hover. Return `null` or
82
+ `{ uri, version, environmentGeneration, value }`, synchronously or as a native Promise. Tag results
83
+ with the **captured request snapshot**, never a later snapshot. `format()` returns `boolean` for sync
84
+ providers and `Promise<boolean>` for async providers (false if absent, detached, invalid or stale).
85
+ Rejected promises are discarded; synchronous provider exceptions remain visible to callers/CodeMirror.
86
+ Signals abort superseded feature requests and requests invalidated by edits, environment refresh,
87
+ detach or disposal. Identity is rechecked after provider return and immediately before completion apply.
88
+ Call `refreshEnvironment()` whenever external catalogs, bindings or admission evidence change.
89
+ Providers should return immutable result payloads and honor cancellation to avoid wasted work.
90
+ Neutral completion providers own candidate filtering and ordering; CodeMirror does not re-filter
91
+ those results against replacement text. The Kalada facade retains its existing fuzzy filtering/boosts.
92
+
93
+ Diagnostics are animation-frame scheduled. Returned ranges and edits must be ordered, bounded,
94
+ half-open **UTF-16 source offsets**, including CRLF, and may not split CRLF or surrogate pairs.
95
+ Invalid entries are dropped and invalid formatting edits return false. Editing uses the document's
96
+ detected line separator; replacement can switch it. Use consistently separated documents (mixed
97
+ line endings are not supported losslessly). Hover accepts plain text or `{ heading, text: string[] }`
98
+ sections, never HTML. Consumers may supply trusted CodeMirror presentation extensions, but these
99
+ do not determine embedded routing. One session attaches to at most one view; view destruction permits
100
+ reattachment with the current text. Disposal does not destroy the host's view; later view edits are inert
101
+ with respect to the disposed session. Host change-callback exceptions are isolated.
102
+
103
+ The neutral runtime and declaration graph imports only CodeMirror, not Kalada language contracts,
104
+ parsers, ASTs or component catalogs. The package retains its language-service dependency for the legacy
105
+ root entry: subpath isolation is not a claim that npm omits that dependency. Packed smoke runs ESM/CJS,
106
+ declaration checking and neutral browser bundling with the Kalada language packages removed.
107
+
108
+ ## Formbar FSX: diagnostics first, runtime still explicitly applied
109
+
110
+ Formbar's `@formbar/fsx-authoring` provides `compileFsx(source, options)`, **not** a complete tooling
111
+ language service. An FSX host can supply just diagnostics initially:
112
+
113
+ ```ts
114
+ import { compileFsx } from "@formbar/fsx-authoring";
115
+ import { createEditorSession, type EditorProvider } from "@kalada/codemirror/editor";
116
+
117
+ const provider: EditorProvider = {
118
+ diagnostics: ({ snapshot, signal }) => {
119
+ const compiled = compileFsx(snapshot.text, authoringOptions());
120
+ if (signal.aborted) return null;
121
+ return { ...snapshot, value: toEditorDiagnostics(compiled) };
122
+ },
123
+ };
124
+ ```
125
+
126
+ This is integration guidance, not a shipped Formbar adapter. `authoringOptions` is host-owned renderer,
127
+ binding and admission evidence; `toEditorDiagnostics` is a host adapter over the installed compiler's
128
+ actual result schema, producing `{ from, to, severity, message }`. Preserve its half-open UTF-16 offsets;
129
+ do not convert them to code-point indexes or normalize CRLF first. The host's existing **Apply** action
130
+ should compile/apply `session.getSnapshot().text` separately. Diagnostic requests must not mount a
131
+ renderer or change runtime state. On catalog/evidence changes, update host options and refresh the session.
132
+ FSX owns attribute guest slots: any future completion/hover/format adapter must route through those
133
+ slots with captured host/guest identity and map returned edits back to host source. This editor neither
134
+ extends the v1 diagnostic-routing protocol into a full language service nor supplies production FSX completion.
135
+
136
+ ## Projection and future UPDF
137
+
138
+ The existing projection representation is an AST/data JSON API, not a new textual grammar. A consumer
139
+ may edit a serialized structured representation using its chosen JSON tooling and validate against the
140
+ actual projection API. Serialization, parse validation, AST conversion and explicit application belong
141
+ to that consumer; this package does not invent a projection syntax.
142
+
143
+ UPDF integration needs an agreed declarative syntax/parser and tooling provider before completion,
144
+ hover or formatting can be promised. Component catalogs and runtime evaluation stay outside the editor.
145
+ Embedded routing belongs to the host-language adapter; optional syntax highlighting is presentation only.
@@ -0,0 +1,566 @@
1
+ import { history, historyKeymap } from '@codemirror/commands';
2
+ import { StateEffect, StateField, Compartment, EditorState, Transaction, EditorSelection, Text } from '@codemirror/state';
3
+ import { showTooltip, ViewPlugin, EditorView, keymap, hoverTooltip, closeHoverTooltips } from '@codemirror/view';
4
+ import { autocompletion, completionKeymap, closeCompletion } from '@codemirror/autocomplete';
5
+ import { setDiagnostics } from '@codemirror/lint';
6
+
7
+ // src/editor-session.ts
8
+
9
+ // src/editor-presentation.ts
10
+ function editorTooltip(hover, from, to) {
11
+ return {
12
+ pos: from,
13
+ end: to,
14
+ above: true,
15
+ create(view) {
16
+ const document = view.dom.ownerDocument;
17
+ const dom = document.createElement("section");
18
+ dom.className = "cm-editor-hover";
19
+ dom.setAttribute("role", "tooltip");
20
+ if (typeof hover.content === "string") dom.textContent = hover.content;
21
+ else for (const section of hover.content) appendSection(document, dom, section);
22
+ return { dom };
23
+ }
24
+ };
25
+ }
26
+ function appendSection(document, dom, section) {
27
+ const heading = document.createElement("strong");
28
+ heading.textContent = section.heading;
29
+ dom.append(heading);
30
+ const list = document.createElement("ul");
31
+ for (const text of section.text.length ? section.text : ["None"]) {
32
+ const item = document.createElement("li");
33
+ item.textContent = text;
34
+ list.append(item);
35
+ }
36
+ dom.append(list);
37
+ }
38
+
39
+ // src/editor-requests.ts
40
+ var EditorRequests = class {
41
+ pending = /* @__PURE__ */ new Map();
42
+ // Published completion edits remain guarded until supersession, not just settlement.
43
+ latest = /* @__PURE__ */ new Map();
44
+ generation = 0;
45
+ start(kind, snapshot, current, alive) {
46
+ const previous = this.pending.get(kind);
47
+ const controller = new AbortController();
48
+ this.pending.set(kind, controller);
49
+ this.latest.set(kind, controller);
50
+ const generation = this.generation;
51
+ previous?.abort();
52
+ return {
53
+ snapshot,
54
+ signal: controller.signal,
55
+ settle: () => {
56
+ if (this.pending.get(kind) === controller) this.pending.delete(kind);
57
+ },
58
+ current: () => !controller.signal.aborted && this.latest.get(kind) === controller && generation === this.generation && alive() && sameIdentity(snapshot, current())
59
+ };
60
+ }
61
+ invalidate() {
62
+ this.generation += 1;
63
+ const controllers = [...this.pending.values()];
64
+ this.pending.clear();
65
+ this.latest.clear();
66
+ for (const controller of controllers) controller.abort();
67
+ }
68
+ };
69
+ function sameIdentity(a, b) {
70
+ return !!a && a.uri === b.uri && a.version === b.version && a.environmentGeneration === b.environmentGeneration;
71
+ }
72
+ function resolveResponse(response, ticket, publish, fallback) {
73
+ const accept = (result) => {
74
+ ticket.settle();
75
+ return result && ticket.current() && sameIdentity(result, ticket.snapshot) ? publish(result) : fallback;
76
+ };
77
+ if (response instanceof Promise)
78
+ return response.then(accept, () => {
79
+ ticket.settle();
80
+ return fallback;
81
+ });
82
+ return accept(response);
83
+ }
84
+
85
+ // src/editor-validation.ts
86
+ function validRange(range, text) {
87
+ return !!range && Number.isSafeInteger(range.from) && Number.isSafeInteger(range.to) && range.from >= 0 && range.to >= range.from && range.to <= text.length && !insidePair(text, range.from) && !insidePair(text, range.to);
88
+ }
89
+ function insidePair(text, offset) {
90
+ const previous = text.charCodeAt(offset - 1);
91
+ const next = text.charCodeAt(offset);
92
+ return previous === 13 && next === 10 || previous >= 55296 && previous <= 56319 && next >= 56320 && next <= 57343;
93
+ }
94
+ function validEdit(edit, text) {
95
+ return validRange(edit, text) && typeof edit.text === "string";
96
+ }
97
+ function sourceOffset(doc, separator, offset) {
98
+ return offset + (doc.lineAt(offset).number - 1) * (separator.length - 1);
99
+ }
100
+ function editorOffset(doc, separator, offset) {
101
+ for (let number = 1; number <= doc.lines; number += 1) {
102
+ const line = doc.line(number);
103
+ const start = line.from + (number - 1) * (separator.length - 1);
104
+ if (offset <= start + line.length) return line.from + offset - start;
105
+ }
106
+ return doc.length;
107
+ }
108
+ function detectLineSeparator(text) {
109
+ if (text.includes("\r\n")) return "\r\n";
110
+ if (text.includes("\r")) return "\r";
111
+ return "\n";
112
+ }
113
+ function textForEditor(text) {
114
+ return Text.of(text.split(/\r\n|\r|\n/u));
115
+ }
116
+ function textFromEditor(document, lineSeparator) {
117
+ return document.sliceString(0, document.length, lineSeparator);
118
+ }
119
+ var setKeyboardTooltip = StateEffect.define();
120
+ var keyboardTooltipField = StateField.define({
121
+ create: () => [],
122
+ update(value, transaction) {
123
+ let next = transaction.docChanged ? [] : value;
124
+ for (const effect of transaction.effects) {
125
+ if (effect.is(setKeyboardTooltip)) next = effect.value ? [effect.value] : [];
126
+ }
127
+ return next;
128
+ },
129
+ provide: (field) => showTooltip.computeN([field], (state) => state.field(field))
130
+ });
131
+
132
+ // src/editor-tooling.ts
133
+ var EditorTooling = class {
134
+ requests = new EditorRequests();
135
+ frame = null;
136
+ hoverIntent = 0;
137
+ host;
138
+ provider;
139
+ constructor(host, provider = {}) {
140
+ this.host = host;
141
+ this.provider = provider;
142
+ }
143
+ extension() {
144
+ return [
145
+ autocompletion({ override: [(context) => this.complete(context)] }),
146
+ hoverTooltip((view, offset) => this.hover(view, offset)),
147
+ this.hoverIntentExtension(),
148
+ keyboardTooltipField,
149
+ keymap.of([
150
+ ...completionKeymap,
151
+ { key: "Mod-Shift-h", run: (view) => this.keyboardHover(view) },
152
+ {
153
+ key: "Escape",
154
+ run: (view) => {
155
+ this.clear(view);
156
+ view.focus();
157
+ return true;
158
+ }
159
+ }
160
+ ])
161
+ ];
162
+ }
163
+ hoverIntentExtension() {
164
+ return ViewPlugin.define((view) => {
165
+ const changed = () => {
166
+ this.hoverIntent += 1;
167
+ };
168
+ const events = ["mousemove", "mouseleave", "pointerdown"];
169
+ for (const event of events) view.dom.addEventListener(event, changed);
170
+ return {
171
+ update: (update) => {
172
+ if (!update.startState.selection.eq(update.state.selection)) changed();
173
+ },
174
+ destroy: () => {
175
+ for (const event of events) view.dom.removeEventListener(event, changed);
176
+ }
177
+ };
178
+ });
179
+ }
180
+ invalidate(view = this.host.view()) {
181
+ if (this.frame !== null && view)
182
+ view.dom.ownerDocument.defaultView?.cancelAnimationFrame(this.frame);
183
+ this.frame = null;
184
+ this.requests.invalidate();
185
+ }
186
+ clear(view, diagnostics = false) {
187
+ this.invalidate();
188
+ closeCompletion(view);
189
+ const interactions = { effects: [setKeyboardTooltip.of(null), closeHoverTooltips] };
190
+ if (diagnostics) view.dispatch(interactions, setDiagnostics(view.state, []));
191
+ else view.dispatch(interactions);
192
+ }
193
+ schedule(view) {
194
+ const diagnosticsProvider = this.provider.diagnostics?.bind(this.provider);
195
+ const window = view.dom.ownerDocument.defaultView;
196
+ if (!diagnosticsProvider || !window) return;
197
+ const ticket = this.ticket("diagnostics", view);
198
+ this.frame = window.requestAnimationFrame(() => {
199
+ this.frame = null;
200
+ if (!ticket.current()) return;
201
+ const response = diagnosticsProvider(ticket);
202
+ void this.resolve(
203
+ response,
204
+ ticket,
205
+ (result) => {
206
+ const diagnostics = result.value.filter(
207
+ (entry) => validRange(entry, ticket.snapshot.text)
208
+ );
209
+ view.dispatch(
210
+ setDiagnostics(
211
+ view.state,
212
+ diagnostics.map((entry) => ({
213
+ ...this.range(view, entry),
214
+ message: entry.message,
215
+ severity: entry.severity
216
+ }))
217
+ )
218
+ );
219
+ return true;
220
+ },
221
+ false
222
+ );
223
+ });
224
+ }
225
+ format() {
226
+ const view = this.host.view();
227
+ if (!view || !this.provider.format) return false;
228
+ const ticket = this.ticket("format", view);
229
+ return this.resolve(
230
+ this.provider.format(ticket),
231
+ ticket,
232
+ (result, guarded) => this.apply(view, result.value, guarded),
233
+ false
234
+ );
235
+ }
236
+ complete(context) {
237
+ const view = this.host.view();
238
+ if (!view || view.state !== context.state || !this.provider.completion) return null;
239
+ const ticket = this.ticket("completion", view);
240
+ const offset = sourceOffset(view.state.doc, view.state.lineBreak, context.pos);
241
+ return this.resolve(
242
+ this.provider.completion(ticket, offset),
243
+ ticket,
244
+ (result, guarded) => {
245
+ const entries = result.value.filter((item) => validEdit(item.edit, ticket.snapshot.text));
246
+ const first = entries[0];
247
+ if (!first) return null;
248
+ return {
249
+ ...this.range(view, first.edit),
250
+ filter: this.host.filterCompletions ?? false,
251
+ options: entries.map((item) => this.option(view, item, guarded))
252
+ };
253
+ },
254
+ null
255
+ );
256
+ }
257
+ option(view, item, ticket) {
258
+ return {
259
+ label: item.label,
260
+ type: item.type,
261
+ detail: item.detail,
262
+ boost: item.boost,
263
+ apply: (target) => {
264
+ if (target === view) this.apply(target, item.edit, ticket);
265
+ }
266
+ };
267
+ }
268
+ apply(view, edit, ticket) {
269
+ if (!ticket.current() || !validEdit(edit, ticket.snapshot.text)) return false;
270
+ view.dispatch({
271
+ changes: { ...this.range(view, edit), insert: textForEditor(edit.text) },
272
+ annotations: Transaction.addToHistory.of(true)
273
+ });
274
+ return true;
275
+ }
276
+ hover(view, offset, publish) {
277
+ if (this.host.view() !== view || !this.provider.hover) return null;
278
+ const intent = this.hoverIntent;
279
+ const selection = view.state.selection;
280
+ const request = this.ticket("hover", view);
281
+ const ticket = {
282
+ ...request,
283
+ current: () => request.current() && intent === this.hoverIntent && selection.eq(view.state.selection)
284
+ };
285
+ const source = sourceOffset(view.state.doc, view.state.lineBreak, offset);
286
+ return this.resolve(
287
+ this.provider.hover(ticket, source),
288
+ ticket,
289
+ (result, guarded) => {
290
+ if (!validRange(result.value, ticket.snapshot.text)) return null;
291
+ const range = this.range(view, result.value);
292
+ const tooltip = (this.host.renderHover ?? editorTooltip)(
293
+ result.value,
294
+ range.from,
295
+ range.to
296
+ );
297
+ if (!guarded.current()) return null;
298
+ publish?.(tooltip);
299
+ return tooltip;
300
+ },
301
+ null
302
+ );
303
+ }
304
+ keyboardHover(view) {
305
+ const tooltip = this.hover(
306
+ view,
307
+ view.state.selection.main.head,
308
+ (value) => view.dispatch({ effects: setKeyboardTooltip.of(value) })
309
+ );
310
+ return tooltip instanceof Promise || tooltip !== null;
311
+ }
312
+ ticket(kind, view) {
313
+ return this.requests.start(
314
+ kind,
315
+ this.host.snapshot(),
316
+ () => this.host.snapshot(),
317
+ () => this.host.view() === view
318
+ );
319
+ }
320
+ resolve(response, ticket, publish, fallback) {
321
+ return resolveResponse(
322
+ response,
323
+ ticket,
324
+ (result) => {
325
+ const guarded = {
326
+ ...ticket,
327
+ current: () => ticket.current() && (this.host.isCurrent?.(result) ?? true)
328
+ };
329
+ return guarded.current() ? publish(result, guarded) : fallback;
330
+ },
331
+ fallback
332
+ );
333
+ }
334
+ range(view, range) {
335
+ return {
336
+ from: editorOffset(view.state.doc, view.state.lineBreak, range.from),
337
+ to: editorOffset(view.state.doc, view.state.lineBreak, range.to)
338
+ };
339
+ }
340
+ };
341
+
342
+ // src/editor-session.ts
343
+ function createEditorSession(options) {
344
+ return new NeutralEditorSession(options);
345
+ }
346
+ var NeutralEditorSession = class {
347
+ extension;
348
+ snapshot;
349
+ attached = null;
350
+ disposed = false;
351
+ failed = false;
352
+ failureView = null;
353
+ notifications = [];
354
+ publishingChanges = false;
355
+ changing = 0;
356
+ separator = new Compartment();
357
+ tooling;
358
+ options;
359
+ bridge;
360
+ constructor(options, bridge = {}) {
361
+ this.options = options;
362
+ this.bridge = bridge;
363
+ const document = options.document;
364
+ if (typeof document.text !== "string" || typeof document.uri !== "string" || document.uri.length === 0 || !Number.isSafeInteger(document.version) || document.version < 0)
365
+ throw new TypeError("Invalid editor document");
366
+ this.snapshot = Object.freeze({ ...document, environmentGeneration: 0 });
367
+ this.tooling = new EditorTooling(
368
+ {
369
+ snapshot: () => this.snapshot,
370
+ view: () => this.disposed || this.failed ? null : this.attached,
371
+ renderHover: bridge.renderHover,
372
+ isCurrent: bridge.isCurrent,
373
+ filterCompletions: bridge.filterCompletions
374
+ },
375
+ options.provider
376
+ );
377
+ this.extension = this.createExtension();
378
+ }
379
+ getSnapshot() {
380
+ return this.snapshot;
381
+ }
382
+ refreshEnvironment() {
383
+ this.assertOpen();
384
+ if (this.snapshot.environmentGeneration >= Number.MAX_SAFE_INTEGER)
385
+ throw new RangeError("Editor environment generation exhausted");
386
+ this.snapshot = Object.freeze({
387
+ ...this.snapshot,
388
+ environmentGeneration: this.snapshot.environmentGeneration + 1
389
+ });
390
+ if (!this.attached) {
391
+ this.tooling.invalidate();
392
+ return;
393
+ }
394
+ this.tooling.clear(this.attached, true);
395
+ this.tooling.schedule(this.attached);
396
+ }
397
+ replaceDocument(text) {
398
+ this.assertOpen();
399
+ if (typeof text !== "string") throw new TypeError("Replacement text must be a string");
400
+ const view = this.attached;
401
+ if (!view) {
402
+ this.changed(text);
403
+ return;
404
+ }
405
+ const separator = detectLineSeparator(text);
406
+ view.dispatch({
407
+ changes: { from: 0, to: view.state.doc.length, insert: textForEditor(text) },
408
+ selection: EditorSelection.cursor(0),
409
+ annotations: Transaction.addToHistory.of(true),
410
+ effects: separator === view.state.lineBreak ? void 0 : this.separator.reconfigure(EditorState.lineSeparator.of(separator))
411
+ });
412
+ }
413
+ format() {
414
+ this.assertOpen();
415
+ return this.tooling.format();
416
+ }
417
+ dispose() {
418
+ if (this.disposed) return;
419
+ this.disposed = true;
420
+ this.tooling.invalidate(this.attached);
421
+ if (this.attached && !this.failed) this.tooling.clear(this.attached, true);
422
+ this.attached = null;
423
+ }
424
+ createExtension() {
425
+ const session = this;
426
+ return [
427
+ ViewPlugin.define((view) => {
428
+ this.attach(view);
429
+ return {
430
+ update: (update) => this.update(update),
431
+ destroy: () => this.detach(view)
432
+ };
433
+ }),
434
+ // Provider callbacks may dispatch; run them only after CodeMirror leaves its update phase.
435
+ EditorView.updateListener.of(() => {
436
+ this.publishChanges();
437
+ this.cleanupFailure();
438
+ }),
439
+ this.separator.of({
440
+ // Resolve when CodeMirror builds a state, including reuse of a cached extension.
441
+ get extension() {
442
+ return EditorState.lineSeparator.of(detectLineSeparator(session.snapshot.text));
443
+ }
444
+ }),
445
+ history(),
446
+ this.tooling.extension(),
447
+ keymap.of(historyKeymap),
448
+ EditorView.contentAttributes.of({ "aria-label": this.options.ariaLabel ?? "Source editor" }),
449
+ this.options.extensions ?? []
450
+ ];
451
+ }
452
+ attach(view) {
453
+ this.assertOpen();
454
+ if (this.attached && this.attached !== view)
455
+ throw new Error("Editor session already has a view");
456
+ if (textFromEditor(view.state.doc, view.state.lineBreak) !== this.snapshot.text)
457
+ throw new Error("CodeMirror document does not match the opened document");
458
+ this.attached = view;
459
+ this.tooling.schedule(view);
460
+ }
461
+ detach(view) {
462
+ if (this.attached !== view) return;
463
+ this.attached = null;
464
+ this.tooling.invalidate(view);
465
+ }
466
+ update(update) {
467
+ if (this.disposed || this.failed || !update.docChanged) return;
468
+ try {
469
+ this.updateTransactions(update);
470
+ } catch (error) {
471
+ this.fail(update.view);
472
+ throw error;
473
+ }
474
+ }
475
+ updateTransactions(update) {
476
+ for (const transaction of update.transactions) {
477
+ if (!transaction.docChanged) continue;
478
+ if (textFromEditor(transaction.startState.doc, transaction.startState.lineBreak) !== this.snapshot.text)
479
+ throw new Error("CodeMirror and editor snapshots diverged");
480
+ this.changed(textFromEditor(transaction.newDoc, transaction.state.lineBreak), transaction);
481
+ }
482
+ }
483
+ changed(text, transaction) {
484
+ if (this.snapshot.version >= Number.MAX_SAFE_INTEGER)
485
+ throw new RangeError("Editor document revision exhausted");
486
+ const previous = this.snapshot;
487
+ const snapshot = Object.freeze({ ...previous, version: previous.version + 1, text });
488
+ this.snapshot = snapshot;
489
+ this.notifications.push(snapshot);
490
+ this.changing += 1;
491
+ let failed = false;
492
+ let failure;
493
+ try {
494
+ this.bridge.changed?.(snapshot, previous, transaction);
495
+ } catch (error) {
496
+ this.notifications = this.notifications.filter((pending) => pending !== snapshot);
497
+ this.reconcileFailure(snapshot, previous, !!transaction);
498
+ failed = true;
499
+ failure = error;
500
+ } finally {
501
+ this.changing -= 1;
502
+ }
503
+ try {
504
+ if (!transaction) this.publishChanges();
505
+ } catch (error) {
506
+ if (!failed) throw error;
507
+ }
508
+ if (failed) throw failure;
509
+ }
510
+ publishChanges() {
511
+ if (this.changing || this.publishingChanges || !this.notifications.length) return;
512
+ this.publishingChanges = true;
513
+ try {
514
+ while (this.notifications.length) {
515
+ const notifications = this.notifications;
516
+ this.notifications = [];
517
+ this.tooling.invalidate();
518
+ for (const snapshot of notifications) this.notifyChange(snapshot);
519
+ }
520
+ } finally {
521
+ this.publishingChanges = false;
522
+ }
523
+ if (!this.disposed && !this.failed && this.attached) this.tooling.schedule(this.attached);
524
+ }
525
+ reconcileFailure(attempt, previous, attached) {
526
+ if (attached) {
527
+ this.fail(this.attached);
528
+ return;
529
+ }
530
+ try {
531
+ if (this.bridge.matches?.(this.snapshot)) return;
532
+ if (this.snapshot === attempt && this.bridge.matches?.(previous)) {
533
+ this.snapshot = previous;
534
+ this.tooling.invalidate();
535
+ return;
536
+ }
537
+ } catch {
538
+ }
539
+ this.fail(null);
540
+ this.notifications = [];
541
+ }
542
+ fail(view) {
543
+ this.failed = true;
544
+ this.failureView = view;
545
+ this.tooling.invalidate(view);
546
+ }
547
+ cleanupFailure() {
548
+ const view = this.failureView;
549
+ this.failureView = null;
550
+ if (view) this.tooling.clear(view, true);
551
+ }
552
+ notifyChange(snapshot) {
553
+ try {
554
+ this.options.onDocumentChange?.(snapshot);
555
+ } catch {
556
+ }
557
+ }
558
+ assertOpen() {
559
+ if (this.disposed) throw new Error("Editor session is disposed");
560
+ if (this.failed) throw new Error("Editor session has failed");
561
+ }
562
+ };
563
+
564
+ export { NeutralEditorSession, createEditorSession };
565
+ //# sourceMappingURL=chunk-GCHU6EL7.js.map
566
+ //# sourceMappingURL=chunk-GCHU6EL7.js.map