@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/dist/index.cjs CHANGED
@@ -1,12 +1,112 @@
1
1
  'use strict';
2
2
 
3
- var autocomplete = require('@codemirror/autocomplete');
4
3
  var commands = require('@codemirror/commands');
5
- var lint = require('@codemirror/lint');
6
4
  var state = require('@codemirror/state');
7
5
  var view = require('@codemirror/view');
6
+ var autocomplete = require('@codemirror/autocomplete');
7
+ var lint = require('@codemirror/lint');
8
8
 
9
- // src/session.ts
9
+ // src/editor-session.ts
10
+
11
+ // src/editor-presentation.ts
12
+ function editorTooltip(hover, from, to) {
13
+ return {
14
+ pos: from,
15
+ end: to,
16
+ above: true,
17
+ create(view) {
18
+ const document = view.dom.ownerDocument;
19
+ const dom = document.createElement("section");
20
+ dom.className = "cm-editor-hover";
21
+ dom.setAttribute("role", "tooltip");
22
+ if (typeof hover.content === "string") dom.textContent = hover.content;
23
+ else for (const section of hover.content) appendSection(document, dom, section);
24
+ return { dom };
25
+ }
26
+ };
27
+ }
28
+ function appendSection(document, dom, section) {
29
+ const heading = document.createElement("strong");
30
+ heading.textContent = section.heading;
31
+ dom.append(heading);
32
+ const list = document.createElement("ul");
33
+ for (const text of section.text.length ? section.text : ["None"]) {
34
+ const item = document.createElement("li");
35
+ item.textContent = text;
36
+ list.append(item);
37
+ }
38
+ dom.append(list);
39
+ }
40
+
41
+ // src/editor-requests.ts
42
+ var EditorRequests = class {
43
+ pending = /* @__PURE__ */ new Map();
44
+ // Published completion edits remain guarded until supersession, not just settlement.
45
+ latest = /* @__PURE__ */ new Map();
46
+ generation = 0;
47
+ start(kind, snapshot, current, alive) {
48
+ const previous = this.pending.get(kind);
49
+ const controller = new AbortController();
50
+ this.pending.set(kind, controller);
51
+ this.latest.set(kind, controller);
52
+ const generation = this.generation;
53
+ previous?.abort();
54
+ return {
55
+ snapshot,
56
+ signal: controller.signal,
57
+ settle: () => {
58
+ if (this.pending.get(kind) === controller) this.pending.delete(kind);
59
+ },
60
+ current: () => !controller.signal.aborted && this.latest.get(kind) === controller && generation === this.generation && alive() && sameIdentity(snapshot, current())
61
+ };
62
+ }
63
+ invalidate() {
64
+ this.generation += 1;
65
+ const controllers = [...this.pending.values()];
66
+ this.pending.clear();
67
+ this.latest.clear();
68
+ for (const controller of controllers) controller.abort();
69
+ }
70
+ };
71
+ function sameIdentity(a, b) {
72
+ return !!a && a.uri === b.uri && a.version === b.version && a.environmentGeneration === b.environmentGeneration;
73
+ }
74
+ function resolveResponse(response, ticket, publish, fallback) {
75
+ const accept = (result) => {
76
+ ticket.settle();
77
+ return result && ticket.current() && sameIdentity(result, ticket.snapshot) ? publish(result) : fallback;
78
+ };
79
+ if (response instanceof Promise)
80
+ return response.then(accept, () => {
81
+ ticket.settle();
82
+ return fallback;
83
+ });
84
+ return accept(response);
85
+ }
86
+
87
+ // src/editor-validation.ts
88
+ function validRange(range, text) {
89
+ 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);
90
+ }
91
+ function insidePair(text, offset) {
92
+ const previous = text.charCodeAt(offset - 1);
93
+ const next = text.charCodeAt(offset);
94
+ return previous === 13 && next === 10 || previous >= 55296 && previous <= 56319 && next >= 56320 && next <= 57343;
95
+ }
96
+ function validEdit(edit, text) {
97
+ return validRange(edit, text) && typeof edit.text === "string";
98
+ }
99
+ function sourceOffset(doc, separator, offset) {
100
+ return offset + (doc.lineAt(offset).number - 1) * (separator.length - 1);
101
+ }
102
+ function editorOffset(doc, separator, offset) {
103
+ for (let number = 1; number <= doc.lines; number += 1) {
104
+ const line = doc.line(number);
105
+ const start = line.from + (number - 1) * (separator.length - 1);
106
+ if (offset <= start + line.length) return line.from + offset - start;
107
+ }
108
+ return doc.length;
109
+ }
10
110
  function detectLineSeparator(text) {
11
111
  if (text.includes("\r\n")) return "\r\n";
12
112
  if (text.includes("\r")) return "\r";
@@ -18,9 +118,542 @@ function textForEditor(text) {
18
118
  function textFromEditor(document, lineSeparator) {
19
119
  return document.sliceString(0, document.length, lineSeparator);
20
120
  }
121
+ var setKeyboardTooltip = state.StateEffect.define();
122
+ var keyboardTooltipField = state.StateField.define({
123
+ create: () => [],
124
+ update(value, transaction) {
125
+ let next = transaction.docChanged ? [] : value;
126
+ for (const effect of transaction.effects) {
127
+ if (effect.is(setKeyboardTooltip)) next = effect.value ? [effect.value] : [];
128
+ }
129
+ return next;
130
+ },
131
+ provide: (field) => view.showTooltip.computeN([field], (state) => state.field(field))
132
+ });
133
+
134
+ // src/editor-tooling.ts
135
+ var EditorTooling = class {
136
+ requests = new EditorRequests();
137
+ frame = null;
138
+ hoverIntent = 0;
139
+ host;
140
+ provider;
141
+ constructor(host, provider = {}) {
142
+ this.host = host;
143
+ this.provider = provider;
144
+ }
145
+ extension() {
146
+ return [
147
+ autocomplete.autocompletion({ override: [(context) => this.complete(context)] }),
148
+ view.hoverTooltip((view, offset) => this.hover(view, offset)),
149
+ this.hoverIntentExtension(),
150
+ keyboardTooltipField,
151
+ view.keymap.of([
152
+ ...autocomplete.completionKeymap,
153
+ { key: "Mod-Shift-h", run: (view) => this.keyboardHover(view) },
154
+ {
155
+ key: "Escape",
156
+ run: (view) => {
157
+ this.clear(view);
158
+ view.focus();
159
+ return true;
160
+ }
161
+ }
162
+ ])
163
+ ];
164
+ }
165
+ hoverIntentExtension() {
166
+ return view.ViewPlugin.define((view) => {
167
+ const changed = () => {
168
+ this.hoverIntent += 1;
169
+ };
170
+ const events = ["mousemove", "mouseleave", "pointerdown"];
171
+ for (const event of events) view.dom.addEventListener(event, changed);
172
+ return {
173
+ update: (update) => {
174
+ if (!update.startState.selection.eq(update.state.selection)) changed();
175
+ },
176
+ destroy: () => {
177
+ for (const event of events) view.dom.removeEventListener(event, changed);
178
+ }
179
+ };
180
+ });
181
+ }
182
+ invalidate(view = this.host.view()) {
183
+ if (this.frame !== null && view)
184
+ view.dom.ownerDocument.defaultView?.cancelAnimationFrame(this.frame);
185
+ this.frame = null;
186
+ this.requests.invalidate();
187
+ }
188
+ clear(view$1, diagnostics = false) {
189
+ this.invalidate();
190
+ autocomplete.closeCompletion(view$1);
191
+ const interactions = { effects: [setKeyboardTooltip.of(null), view.closeHoverTooltips] };
192
+ if (diagnostics) view$1.dispatch(interactions, lint.setDiagnostics(view$1.state, []));
193
+ else view$1.dispatch(interactions);
194
+ }
195
+ schedule(view) {
196
+ const diagnosticsProvider = this.provider.diagnostics?.bind(this.provider);
197
+ const window = view.dom.ownerDocument.defaultView;
198
+ if (!diagnosticsProvider || !window) return;
199
+ const ticket = this.ticket("diagnostics", view);
200
+ this.frame = window.requestAnimationFrame(() => {
201
+ this.frame = null;
202
+ if (!ticket.current()) return;
203
+ const response = diagnosticsProvider(ticket);
204
+ void this.resolve(
205
+ response,
206
+ ticket,
207
+ (result) => {
208
+ const diagnostics = result.value.filter(
209
+ (entry) => validRange(entry, ticket.snapshot.text)
210
+ );
211
+ view.dispatch(
212
+ lint.setDiagnostics(
213
+ view.state,
214
+ diagnostics.map((entry) => ({
215
+ ...this.range(view, entry),
216
+ message: entry.message,
217
+ severity: entry.severity
218
+ }))
219
+ )
220
+ );
221
+ return true;
222
+ },
223
+ false
224
+ );
225
+ });
226
+ }
227
+ format() {
228
+ const view = this.host.view();
229
+ if (!view || !this.provider.format) return false;
230
+ const ticket = this.ticket("format", view);
231
+ return this.resolve(
232
+ this.provider.format(ticket),
233
+ ticket,
234
+ (result, guarded) => this.apply(view, result.value, guarded),
235
+ false
236
+ );
237
+ }
238
+ complete(context) {
239
+ const view = this.host.view();
240
+ if (!view || view.state !== context.state || !this.provider.completion) return null;
241
+ const ticket = this.ticket("completion", view);
242
+ const offset = sourceOffset(view.state.doc, view.state.lineBreak, context.pos);
243
+ return this.resolve(
244
+ this.provider.completion(ticket, offset),
245
+ ticket,
246
+ (result, guarded) => {
247
+ const entries = result.value.filter((item) => validEdit(item.edit, ticket.snapshot.text));
248
+ const first = entries[0];
249
+ if (!first) return null;
250
+ return {
251
+ ...this.range(view, first.edit),
252
+ filter: this.host.filterCompletions ?? false,
253
+ options: entries.map((item) => this.option(view, item, guarded))
254
+ };
255
+ },
256
+ null
257
+ );
258
+ }
259
+ option(view, item, ticket) {
260
+ return {
261
+ label: item.label,
262
+ type: item.type,
263
+ detail: item.detail,
264
+ boost: item.boost,
265
+ apply: (target) => {
266
+ if (target === view) this.apply(target, item.edit, ticket);
267
+ }
268
+ };
269
+ }
270
+ apply(view, edit, ticket) {
271
+ if (!ticket.current() || !validEdit(edit, ticket.snapshot.text)) return false;
272
+ view.dispatch({
273
+ changes: { ...this.range(view, edit), insert: textForEditor(edit.text) },
274
+ annotations: state.Transaction.addToHistory.of(true)
275
+ });
276
+ return true;
277
+ }
278
+ hover(view, offset, publish) {
279
+ if (this.host.view() !== view || !this.provider.hover) return null;
280
+ const intent = this.hoverIntent;
281
+ const selection = view.state.selection;
282
+ const request = this.ticket("hover", view);
283
+ const ticket = {
284
+ ...request,
285
+ current: () => request.current() && intent === this.hoverIntent && selection.eq(view.state.selection)
286
+ };
287
+ const source = sourceOffset(view.state.doc, view.state.lineBreak, offset);
288
+ return this.resolve(
289
+ this.provider.hover(ticket, source),
290
+ ticket,
291
+ (result, guarded) => {
292
+ if (!validRange(result.value, ticket.snapshot.text)) return null;
293
+ const range = this.range(view, result.value);
294
+ const tooltip = (this.host.renderHover ?? editorTooltip)(
295
+ result.value,
296
+ range.from,
297
+ range.to
298
+ );
299
+ if (!guarded.current()) return null;
300
+ publish?.(tooltip);
301
+ return tooltip;
302
+ },
303
+ null
304
+ );
305
+ }
306
+ keyboardHover(view) {
307
+ const tooltip = this.hover(
308
+ view,
309
+ view.state.selection.main.head,
310
+ (value) => view.dispatch({ effects: setKeyboardTooltip.of(value) })
311
+ );
312
+ return tooltip instanceof Promise || tooltip !== null;
313
+ }
314
+ ticket(kind, view) {
315
+ return this.requests.start(
316
+ kind,
317
+ this.host.snapshot(),
318
+ () => this.host.snapshot(),
319
+ () => this.host.view() === view
320
+ );
321
+ }
322
+ resolve(response, ticket, publish, fallback) {
323
+ return resolveResponse(
324
+ response,
325
+ ticket,
326
+ (result) => {
327
+ const guarded = {
328
+ ...ticket,
329
+ current: () => ticket.current() && (this.host.isCurrent?.(result) ?? true)
330
+ };
331
+ return guarded.current() ? publish(result, guarded) : fallback;
332
+ },
333
+ fallback
334
+ );
335
+ }
336
+ range(view, range) {
337
+ return {
338
+ from: editorOffset(view.state.doc, view.state.lineBreak, range.from),
339
+ to: editorOffset(view.state.doc, view.state.lineBreak, range.to)
340
+ };
341
+ }
342
+ };
343
+
344
+ // src/editor-session.ts
345
+ var NeutralEditorSession = class {
346
+ extension;
347
+ snapshot;
348
+ attached = null;
349
+ disposed = false;
350
+ failed = false;
351
+ failureView = null;
352
+ notifications = [];
353
+ publishingChanges = false;
354
+ changing = 0;
355
+ separator = new state.Compartment();
356
+ tooling;
357
+ options;
358
+ bridge;
359
+ constructor(options, bridge = {}) {
360
+ this.options = options;
361
+ this.bridge = bridge;
362
+ const document = options.document;
363
+ if (typeof document.text !== "string" || typeof document.uri !== "string" || document.uri.length === 0 || !Number.isSafeInteger(document.version) || document.version < 0)
364
+ throw new TypeError("Invalid editor document");
365
+ this.snapshot = Object.freeze({ ...document, environmentGeneration: 0 });
366
+ this.tooling = new EditorTooling(
367
+ {
368
+ snapshot: () => this.snapshot,
369
+ view: () => this.disposed || this.failed ? null : this.attached,
370
+ renderHover: bridge.renderHover,
371
+ isCurrent: bridge.isCurrent,
372
+ filterCompletions: bridge.filterCompletions
373
+ },
374
+ options.provider
375
+ );
376
+ this.extension = this.createExtension();
377
+ }
378
+ getSnapshot() {
379
+ return this.snapshot;
380
+ }
381
+ refreshEnvironment() {
382
+ this.assertOpen();
383
+ if (this.snapshot.environmentGeneration >= Number.MAX_SAFE_INTEGER)
384
+ throw new RangeError("Editor environment generation exhausted");
385
+ this.snapshot = Object.freeze({
386
+ ...this.snapshot,
387
+ environmentGeneration: this.snapshot.environmentGeneration + 1
388
+ });
389
+ if (!this.attached) {
390
+ this.tooling.invalidate();
391
+ return;
392
+ }
393
+ this.tooling.clear(this.attached, true);
394
+ this.tooling.schedule(this.attached);
395
+ }
396
+ replaceDocument(text) {
397
+ this.assertOpen();
398
+ if (typeof text !== "string") throw new TypeError("Replacement text must be a string");
399
+ const view = this.attached;
400
+ if (!view) {
401
+ this.changed(text);
402
+ return;
403
+ }
404
+ const separator = detectLineSeparator(text);
405
+ view.dispatch({
406
+ changes: { from: 0, to: view.state.doc.length, insert: textForEditor(text) },
407
+ selection: state.EditorSelection.cursor(0),
408
+ annotations: state.Transaction.addToHistory.of(true),
409
+ effects: separator === view.state.lineBreak ? void 0 : this.separator.reconfigure(state.EditorState.lineSeparator.of(separator))
410
+ });
411
+ }
412
+ format() {
413
+ this.assertOpen();
414
+ return this.tooling.format();
415
+ }
416
+ dispose() {
417
+ if (this.disposed) return;
418
+ this.disposed = true;
419
+ this.tooling.invalidate(this.attached);
420
+ if (this.attached && !this.failed) this.tooling.clear(this.attached, true);
421
+ this.attached = null;
422
+ }
423
+ createExtension() {
424
+ const session = this;
425
+ return [
426
+ view.ViewPlugin.define((view) => {
427
+ this.attach(view);
428
+ return {
429
+ update: (update) => this.update(update),
430
+ destroy: () => this.detach(view)
431
+ };
432
+ }),
433
+ // Provider callbacks may dispatch; run them only after CodeMirror leaves its update phase.
434
+ view.EditorView.updateListener.of(() => {
435
+ this.publishChanges();
436
+ this.cleanupFailure();
437
+ }),
438
+ this.separator.of({
439
+ // Resolve when CodeMirror builds a state, including reuse of a cached extension.
440
+ get extension() {
441
+ return state.EditorState.lineSeparator.of(detectLineSeparator(session.snapshot.text));
442
+ }
443
+ }),
444
+ commands.history(),
445
+ this.tooling.extension(),
446
+ view.keymap.of(commands.historyKeymap),
447
+ view.EditorView.contentAttributes.of({ "aria-label": this.options.ariaLabel ?? "Source editor" }),
448
+ this.options.extensions ?? []
449
+ ];
450
+ }
451
+ attach(view) {
452
+ this.assertOpen();
453
+ if (this.attached && this.attached !== view)
454
+ throw new Error("Editor session already has a view");
455
+ if (textFromEditor(view.state.doc, view.state.lineBreak) !== this.snapshot.text)
456
+ throw new Error("CodeMirror document does not match the opened document");
457
+ this.attached = view;
458
+ this.tooling.schedule(view);
459
+ }
460
+ detach(view) {
461
+ if (this.attached !== view) return;
462
+ this.attached = null;
463
+ this.tooling.invalidate(view);
464
+ }
465
+ update(update) {
466
+ if (this.disposed || this.failed || !update.docChanged) return;
467
+ try {
468
+ this.updateTransactions(update);
469
+ } catch (error) {
470
+ this.fail(update.view);
471
+ throw error;
472
+ }
473
+ }
474
+ updateTransactions(update) {
475
+ for (const transaction of update.transactions) {
476
+ if (!transaction.docChanged) continue;
477
+ if (textFromEditor(transaction.startState.doc, transaction.startState.lineBreak) !== this.snapshot.text)
478
+ throw new Error("CodeMirror and editor snapshots diverged");
479
+ this.changed(textFromEditor(transaction.newDoc, transaction.state.lineBreak), transaction);
480
+ }
481
+ }
482
+ changed(text, transaction) {
483
+ if (this.snapshot.version >= Number.MAX_SAFE_INTEGER)
484
+ throw new RangeError("Editor document revision exhausted");
485
+ const previous = this.snapshot;
486
+ const snapshot = Object.freeze({ ...previous, version: previous.version + 1, text });
487
+ this.snapshot = snapshot;
488
+ this.notifications.push(snapshot);
489
+ this.changing += 1;
490
+ let failed = false;
491
+ let failure;
492
+ try {
493
+ this.bridge.changed?.(snapshot, previous, transaction);
494
+ } catch (error) {
495
+ this.notifications = this.notifications.filter((pending) => pending !== snapshot);
496
+ this.reconcileFailure(snapshot, previous, !!transaction);
497
+ failed = true;
498
+ failure = error;
499
+ } finally {
500
+ this.changing -= 1;
501
+ }
502
+ try {
503
+ if (!transaction) this.publishChanges();
504
+ } catch (error) {
505
+ if (!failed) throw error;
506
+ }
507
+ if (failed) throw failure;
508
+ }
509
+ publishChanges() {
510
+ if (this.changing || this.publishingChanges || !this.notifications.length) return;
511
+ this.publishingChanges = true;
512
+ try {
513
+ while (this.notifications.length) {
514
+ const notifications = this.notifications;
515
+ this.notifications = [];
516
+ this.tooling.invalidate();
517
+ for (const snapshot of notifications) this.notifyChange(snapshot);
518
+ }
519
+ } finally {
520
+ this.publishingChanges = false;
521
+ }
522
+ if (!this.disposed && !this.failed && this.attached) this.tooling.schedule(this.attached);
523
+ }
524
+ reconcileFailure(attempt, previous, attached) {
525
+ if (attached) {
526
+ this.fail(this.attached);
527
+ return;
528
+ }
529
+ try {
530
+ if (this.bridge.matches?.(this.snapshot)) return;
531
+ if (this.snapshot === attempt && this.bridge.matches?.(previous)) {
532
+ this.snapshot = previous;
533
+ this.tooling.invalidate();
534
+ return;
535
+ }
536
+ } catch {
537
+ }
538
+ this.fail(null);
539
+ this.notifications = [];
540
+ }
541
+ fail(view) {
542
+ this.failed = true;
543
+ this.failureView = view;
544
+ this.tooling.invalidate(view);
545
+ }
546
+ cleanupFailure() {
547
+ const view = this.failureView;
548
+ this.failureView = null;
549
+ if (view) this.tooling.clear(view, true);
550
+ }
551
+ notifyChange(snapshot) {
552
+ try {
553
+ this.options.onDocumentChange?.(snapshot);
554
+ } catch {
555
+ }
556
+ }
557
+ assertOpen() {
558
+ if (this.disposed) throw new Error("Editor session is disposed");
559
+ if (this.failed) throw new Error("Editor session has failed");
560
+ }
561
+ };
562
+
563
+ // src/kalada-provider.ts
564
+ var KaladaProvider = class {
565
+ service;
566
+ uri;
567
+ hovers = /* @__PURE__ */ new WeakMap();
568
+ identities = /* @__PURE__ */ new WeakMap();
569
+ constructor(service, uri) {
570
+ this.service = service;
571
+ this.uri = uri;
572
+ }
573
+ isCurrent(identity) {
574
+ const original = this.identities.get(identity);
575
+ return !!original && this.service.isCurrent(original);
576
+ }
577
+ diagnostics(request) {
578
+ const result = this.service.diagnostics(this.uri, cancellation(request));
579
+ if (result.kind !== "diagnostics" || !this.service.isCurrent(result)) return null;
580
+ return this.tagged(
581
+ request,
582
+ result,
583
+ result.diagnostics.map((item) => ({
584
+ ...item.source ? this.range(item.source.range) : { from: 0, to: 0 },
585
+ message: item.message,
586
+ severity: "error"
587
+ }))
588
+ );
589
+ }
590
+ completion(request, offset) {
591
+ const result = this.service.completion(
592
+ this.uri,
593
+ this.document().lineIndex.positionAt(offset),
594
+ cancellation(request)
595
+ );
596
+ if (result.kind !== "completion" || !this.service.isCurrent(result)) return null;
597
+ return this.tagged(
598
+ request,
599
+ result,
600
+ result.items.map((item) => ({
601
+ label: item.label,
602
+ edit: { ...this.range(item.edit.range), text: item.edit.text },
603
+ type: item.kind === "binding" ? "variable" : item.kind,
604
+ detail: `${item.support}, ${item.presence}`,
605
+ boost: item.support === "common" ? 10 : 0
606
+ }))
607
+ );
608
+ }
609
+ hover(request, offset) {
610
+ const result = this.service.hover(
611
+ this.uri,
612
+ this.document().lineIndex.positionAt(offset),
613
+ cancellation(request)
614
+ );
615
+ if (result.kind !== "hover" || !result.hover || !this.service.isCurrent(result)) return null;
616
+ const hover = { ...this.range(result.hover.range), content: "" };
617
+ this.hovers.set(hover, result.hover);
618
+ return this.tagged(request, result, hover);
619
+ }
620
+ format(request) {
621
+ const result = this.service.format(this.uri, cancellation(request));
622
+ if (result.kind !== "format" || !result.edit || !this.service.isCurrent(result)) return null;
623
+ return this.tagged(request, result, {
624
+ ...this.range(result.edit.range),
625
+ text: result.edit.text
626
+ });
627
+ }
628
+ tagged(request, original, value) {
629
+ const result = {
630
+ uri: this.uri,
631
+ version: request.snapshot.version,
632
+ environmentGeneration: request.snapshot.environmentGeneration,
633
+ value
634
+ };
635
+ this.identities.set(result, original);
636
+ return result;
637
+ }
638
+ range(source) {
639
+ const document = this.document();
640
+ return {
641
+ from: document.lineIndex.offsetAt(source.start),
642
+ to: document.lineIndex.offsetAt(source.end)
643
+ };
644
+ }
645
+ document() {
646
+ const document = this.service.getDocument(this.uri);
647
+ if (!document) throw new Error("Kalada editor document is not open");
648
+ return document;
649
+ }
650
+ };
651
+ function cancellation(request) {
652
+ return { cancellation: { isCancellationRequested: () => request.signal.aborted } };
653
+ }
21
654
 
22
655
  // src/render.ts
23
- function hoverTooltip(info, position) {
656
+ function hoverTooltip2(info, position) {
24
657
  return {
25
658
  pos: position,
26
659
  above: true,
@@ -30,15 +663,15 @@ function hoverTooltip(info, position) {
30
663
  dom.className = "cm-kalada-hover";
31
664
  dom.setAttribute("role", "tooltip");
32
665
  dom.setAttribute("aria-label", "Kalada expression information");
33
- appendSection(document, dom, "Input editor shape", info.input.map(shapeText));
34
- appendSection(document, dom, "Output Kalada semantic projection", [semanticText(info)]);
35
- appendSection(document, dom, "Presence / branch conditions", [accessText(info)]);
36
- appendSection(document, dom, "Limit/unknown evidence", info.evidence.map(evidenceText));
666
+ appendSection2(document, dom, "Input editor shape", info.input.map(shapeText));
667
+ appendSection2(document, dom, "Output Kalada semantic projection", [semanticText(info)]);
668
+ appendSection2(document, dom, "Presence / branch conditions", [accessText(info)]);
669
+ appendSection2(document, dom, "Limit/unknown evidence", info.evidence.map(evidenceText));
37
670
  return { dom };
38
671
  }
39
672
  };
40
673
  }
41
- function appendSection(document, parent, heading, values) {
674
+ function appendSection2(document, parent, heading, values) {
42
675
  const title = document.createElement("strong");
43
676
  title.textContent = heading;
44
677
  parent.append(title);
@@ -68,6 +701,17 @@ function evidenceText(evidence) {
68
701
  return `${evidence.code} at ${evidence.path.join(".") || "root"}`;
69
702
  }
70
703
 
704
+ // src/session-helpers.ts
705
+ function wholeReplacement(snapshot, text) {
706
+ return {
707
+ range: {
708
+ start: { line: 0, character: 0 },
709
+ end: snapshot.lineIndex.positionAt(snapshot.text.length)
710
+ },
711
+ text
712
+ };
713
+ }
714
+
71
715
  // src/translation.ts
72
716
  function changesToEdits(startDoc, newDoc, changes, snapshot, lineSeparator = "\n") {
73
717
  const offsets = [];
@@ -83,14 +727,6 @@ function changesToEdits(startDoc, newDoc, changes, snapshot, lineSeparator = "\n
83
727
  if (translated.every((edit) => edit !== null)) return translated;
84
728
  return [wholeDocumentEdit(snapshot, newDoc.sliceString(0, newDoc.length, lineSeparator))];
85
729
  }
86
- function rangeToOffsets(document, range) {
87
- const from = offsetAt(document, range.start.line, range.start.character);
88
- const to = offsetAt(document, range.end.line, range.end.character);
89
- return from === null || to === null || from > to ? null : { from, to };
90
- }
91
- function diagnosticOffsets(document, range) {
92
- return rangeToOffsets(document, range) ?? { from: 0, to: 0 };
93
- }
94
730
  function offsetEdit(edit, document, index) {
95
731
  const start = positionAt(document, edit.from);
96
732
  const end = positionAt(document, edit.to);
@@ -101,14 +737,6 @@ function positionAt(document, offset) {
101
737
  const line = document.lineAt(offset);
102
738
  return { line: line.number - 1, character: offset - line.from };
103
739
  }
104
- function offsetAt(document, line, character) {
105
- if (!Number.isSafeInteger(line) || !Number.isSafeInteger(character) || line < 0 || character < 0) {
106
- return null;
107
- }
108
- if (line >= document.lines) return null;
109
- const found = document.line(line + 1);
110
- return character <= found.length ? found.from + character : null;
111
- }
112
740
  function coalesceInserts(edits) {
113
741
  const output = [];
114
742
  for (const edit of edits) {
@@ -129,345 +757,76 @@ function wholeDocumentEdit(snapshot, text) {
129
757
  };
130
758
  }
131
759
 
132
- // src/session-helpers.ts
133
- function positionAt2(document, offset) {
134
- const line = document.lineAt(offset);
135
- return { line: line.number - 1, character: offset - line.from };
136
- }
137
- function translatedTooltip(result, view) {
138
- const range = result.hover ? rangeToOffsets(view.state.doc, result.hover.range) : null;
139
- if (!range || !result.hover) return null;
140
- return { ...hoverTooltip(result.hover, range.from), end: range.to };
141
- }
142
- function translateDiagnostic(diagnostic, view) {
143
- const range = diagnostic.source ? diagnosticOffsets(view.state.doc, diagnostic.source.range) : { from: 0, to: 0 };
144
- return { ...range, severity: "error", message: diagnostic.message };
145
- }
146
- function animationWindow(view) {
147
- const window = view.dom.ownerDocument.defaultView;
148
- if (!window) throw new Error("CodeMirror view has no window");
149
- return window;
150
- }
151
- function wholeReplacement(snapshot, text) {
760
+ // src/session.ts
761
+ function createKaladaEditorSession(options) {
762
+ const { service } = options;
763
+ const opened = service.openDocument(options.document);
764
+ let disposed = false;
765
+ const provider = new KaladaProvider(service, opened.uri);
766
+ const committed = /* @__PURE__ */ new WeakMap();
767
+ const core = new NeutralEditorSession(
768
+ {
769
+ document: opened,
770
+ ariaLabel: "Kalada expression editor",
771
+ provider,
772
+ onDocumentChange: (identity) => {
773
+ const snapshot = committed.get(identity);
774
+ if (snapshot) options.onDocumentChange?.(snapshot);
775
+ }
776
+ },
777
+ kaladaBridge(provider, (identity, snapshot) => committed.set(identity, snapshot))
778
+ );
152
779
  return {
153
- range: {
154
- start: { line: 0, character: 0 },
155
- end: snapshot.lineIndex.positionAt(snapshot.text.length)
780
+ extension: core.extension,
781
+ refreshEnvironment: () => core.refreshEnvironment(),
782
+ replaceDocument: (text) => core.replaceDocument(text),
783
+ format() {
784
+ const formatted = core.format();
785
+ if (typeof formatted !== "boolean") throw new Error("Kalada formatting must be synchronous");
786
+ return formatted;
156
787
  },
157
- text
158
- };
159
- }
160
- var setKeyboardTooltip = state.StateEffect.define();
161
- var keyboardTooltipField = state.StateField.define({
162
- create: () => [],
163
- update(value, transaction) {
164
- let next = value;
165
- for (const effect of transaction.effects) {
166
- if (effect.is(setKeyboardTooltip)) next = effect.value ? [effect.value] : [];
788
+ dispose() {
789
+ if (disposed) return;
790
+ disposed = true;
791
+ core.dispose();
792
+ service.closeDocument(opened.uri);
167
793
  }
168
- return next;
169
- },
170
- provide: (field) => view.showTooltip.computeN([field], (state) => state.field(field))
171
- });
172
-
173
- // src/session.ts
174
- function createKaladaEditorSession(options) {
175
- return new EditorSession(options);
794
+ };
176
795
  }
177
- var EditorSession = class {
178
- extension;
179
- service;
180
- uri;
181
- onDocumentChange;
182
- lineSeparator = new state.Compartment();
183
- view = null;
184
- expectedText;
185
- revision;
186
- disposed = false;
187
- viewEpoch = 0;
188
- completionEpoch = 0;
189
- hoverEpoch = 0;
190
- diagnosticsEpoch = 0;
191
- diagnosticsFrame = null;
192
- constructor(options) {
193
- this.service = options.service;
194
- this.uri = options.document.uri;
195
- this.onDocumentChange = options.onDocumentChange;
196
- const opened = this.service.openDocument(options.document);
197
- this.expectedText = opened.text;
198
- this.revision = opened.version;
199
- this.extension = this.createExtension();
200
- }
201
- refreshEnvironment() {
202
- this.assertOpen();
203
- this.invalidateRequests();
204
- const view = this.view;
205
- if (!view) return;
206
- this.clearEditor(view, false);
207
- this.scheduleDiagnostics(view);
208
- }
209
- replaceDocument(text) {
210
- this.assertOpen();
211
- if (typeof text !== "string") throw new TypeError("Replacement text must be a string");
212
- const view = this.view;
213
- if (view) {
214
- const separator = detectLineSeparator(text);
215
- view.dispatch({
216
- changes: { from: 0, to: view.state.doc.length, insert: textForEditor(text) },
217
- selection: state.EditorSelection.cursor(0),
218
- annotations: state.Transaction.addToHistory.of(true),
219
- effects: separator === view.state.lineBreak ? void 0 : this.lineSeparator.reconfigure(state.EditorState.lineSeparator.of(separator))
220
- });
221
- return;
222
- }
223
- const current = this.requireDocument();
224
- const snapshot = this.service.updateDocument({
225
- uri: this.uri,
226
- version: this.nextRevision(),
227
- edits: [wholeReplacement(current, text)]
228
- });
229
- this.expectedText = snapshot.text;
230
- this.invalidateRequests();
231
- this.notify(snapshot);
232
- }
233
- format() {
234
- this.assertOpen();
235
- const view = this.view;
236
- if (!view) return false;
237
- const epoch = this.completionEpoch;
238
- const result = this.service.format(this.uri, {
239
- cancellation: this.cancellation("completion", epoch)
240
- });
241
- if (result.kind !== "format" || !result.edit || !this.publishable(result, view, epoch, "completion")) {
242
- return false;
243
- }
244
- const range = rangeToOffsets(view.state.doc, result.edit.range);
245
- if (!range) return false;
246
- view.dispatch({
247
- changes: { ...range, insert: textForEditor(result.edit.text) },
248
- annotations: state.Transaction.addToHistory.of(true)
249
- });
250
- return true;
251
- }
252
- dispose() {
253
- if (this.disposed) return;
254
- this.disposed = true;
255
- this.invalidateRequests();
256
- this.cancelDiagnosticsFrame();
257
- const view = this.view;
258
- if (view) this.clearEditor(view, true);
259
- this.view = null;
260
- this.service.closeDocument(this.uri);
261
- }
262
- createExtension() {
263
- const session = this;
264
- const lifecycle = view.ViewPlugin.define((view) => {
265
- session.attach(view);
266
- return {
267
- update(update) {
268
- session.updateView(update);
269
- },
270
- destroy() {
271
- session.detach(view);
272
- }
273
- };
274
- });
275
- return [
276
- lifecycle,
277
- this.lineSeparator.of(state.EditorState.lineSeparator.of(detectLineSeparator(this.expectedText))),
278
- commands.history(),
279
- autocomplete.autocompletion({ override: [(context) => this.complete(context)] }),
280
- view.hoverTooltip((view, position) => this.pointerHover(view, position)),
281
- keyboardTooltipField,
282
- view.keymap.of([
283
- ...autocomplete.completionKeymap,
284
- ...commands.historyKeymap,
285
- { key: "Mod-Shift-h", run: (view) => this.keyboardHover(view) },
286
- { key: "Escape", run: (view) => this.clearInteractions(view) }
287
- ]),
288
- view.EditorView.contentAttributes.of({ "aria-label": "Kalada expression editor" })
289
- ];
290
- }
291
- attach(view) {
292
- this.assertOpen();
293
- if (this.view && this.view !== view)
294
- throw new Error("Kalada editor session already has a view");
295
- if (textFromEditor(view.state.doc, view.state.lineBreak) !== this.expectedText) {
296
- throw new Error("CodeMirror document does not match the opened Kalada document");
297
- }
298
- this.view = view;
299
- this.viewEpoch += 1;
300
- this.scheduleDiagnostics(view);
301
- }
302
- detach(view) {
303
- if (this.view !== view) return;
304
- this.cancelDiagnosticsFrame();
305
- this.invalidateRequests();
306
- this.view = null;
307
- this.viewEpoch += 1;
308
- }
309
- updateView(update) {
310
- if (this.disposed || !update.docChanged) return;
311
- this.invalidateRequests();
312
- for (const transaction of update.transactions) {
313
- if (transaction.docChanged) this.applyTransaction(transaction);
314
- }
315
- this.expectedText = textFromEditor(update.state.doc, update.state.lineBreak);
316
- this.scheduleDiagnostics(update.view);
317
- }
318
- applyTransaction(transaction) {
319
- const current = this.requireDocument();
320
- const startText = textFromEditor(transaction.startState.doc, transaction.startState.lineBreak);
321
- if (current.text !== startText) {
322
- throw new Error("CodeMirror and language-service snapshots diverged");
323
- }
324
- const edits = changesToEdits(
325
- transaction.startState.doc,
326
- transaction.newDoc,
327
- transaction.changes,
328
- current,
329
- transaction.state.lineBreak
330
- );
331
- const snapshot = this.service.updateDocument({
332
- uri: this.uri,
333
- version: this.nextRevision(),
334
- edits
335
- });
336
- if (snapshot.text !== textFromEditor(transaction.newDoc, transaction.state.lineBreak)) {
337
- throw new Error("CodeMirror transaction translation changed document text");
338
- }
339
- this.expectedText = snapshot.text;
340
- this.notify(snapshot);
341
- }
342
- complete(context) {
343
- const view = this.view;
344
- if (!view || view.state !== context.state || this.disposed) return null;
345
- const epoch = ++this.completionEpoch;
346
- const result = this.service.completion(this.uri, positionAt2(context.state.doc, context.pos), {
347
- cancellation: this.cancellation("completion", epoch)
348
- });
349
- if (result.kind !== "completion" || !this.publishable(result, view, epoch, "completion")) {
350
- return null;
351
- }
352
- const translated = result.items.map(
353
- (item) => this.translateCompletion(item, result, view, epoch)
354
- );
355
- const available = translated.filter((entry) => entry !== null);
356
- const first = available[0];
357
- return first ? { from: first.from, to: first.to, options: available.map(({ option }) => option) } : null;
358
- }
359
- translateCompletion(item, identity, view, epoch) {
360
- const range = rangeToOffsets(view.state.doc, item.edit.range);
361
- if (!range) return null;
362
- const option = {
363
- label: item.label,
364
- type: item.kind === "binding" ? "variable" : item.kind,
365
- detail: `${item.support}, ${item.presence}`,
366
- boost: item.support === "common" ? 10 : 0,
367
- apply: (target) => this.applyCompletion(target, item, identity, epoch)
368
- };
369
- return { ...range, option };
370
- }
371
- applyCompletion(view, item, identity, epoch) {
372
- if (!this.publishable(identity, view, epoch, "completion")) return;
373
- const range = rangeToOffsets(view.state.doc, item.edit.range);
374
- if (!range) return;
375
- view.dispatch({ changes: { ...range, insert: item.edit.text } });
376
- }
377
- pointerHover(view, offset) {
378
- return this.requestHover(view, offset);
379
- }
380
- keyboardHover(view) {
381
- const tooltip = this.requestHover(view, view.state.selection.main.head);
382
- if (!tooltip) return false;
383
- view.dispatch({ effects: setKeyboardTooltip.of(tooltip) });
384
- return true;
385
- }
386
- requestHover(view, offset) {
387
- const epoch = ++this.hoverEpoch;
388
- const result = this.service.hover(this.uri, positionAt2(view.state.doc, offset), {
389
- cancellation: this.cancellation("hover", epoch)
390
- });
391
- if (result.kind !== "hover" || !result.hover || !this.publishable(result, view, epoch, "hover")) {
392
- return null;
393
- }
394
- return translatedTooltip(result, view);
395
- }
396
- clearInteractions(view) {
397
- this.clearEditor(view, false);
398
- view.focus();
399
- return true;
400
- }
401
- clearEditor(view$1, diagnostics) {
402
- autocomplete.closeCompletion(view$1);
403
- const interactions = { effects: [setKeyboardTooltip.of(null), view.closeHoverTooltips] };
404
- if (diagnostics) view$1.dispatch(interactions, lint.setDiagnostics(view$1.state, []));
405
- else view$1.dispatch(interactions);
406
- }
407
- scheduleDiagnostics(view) {
408
- this.cancelDiagnosticsFrame();
409
- const epoch = ++this.diagnosticsEpoch;
410
- const viewEpoch = this.viewEpoch;
411
- this.diagnosticsFrame = animationWindow(view).requestAnimationFrame(() => {
412
- this.diagnosticsFrame = null;
413
- const result = this.service.diagnostics(this.uri, {
414
- cancellation: this.cancellation("diagnostics", epoch)
796
+ function kaladaBridge(provider, committed) {
797
+ return {
798
+ filterCompletions: true,
799
+ isCurrent: (identity) => provider.isCurrent(identity),
800
+ matches(snapshot) {
801
+ const current = provider.service.getDocument(provider.uri);
802
+ return !!current && current.uri === snapshot.uri && current.version === snapshot.version && current.text === snapshot.text;
803
+ },
804
+ changed(snapshot, previous, transaction) {
805
+ const current = provider.service.getDocument(provider.uri);
806
+ if (!current || current.text !== previous.text || current.version !== previous.version)
807
+ throw new Error("CodeMirror and language-service snapshots diverged");
808
+ const updated = provider.service.updateDocument({
809
+ uri: provider.uri,
810
+ version: snapshot.version,
811
+ edits: transaction ? changesToEdits(
812
+ transaction.startState.doc,
813
+ transaction.newDoc,
814
+ transaction.changes,
815
+ current,
816
+ transaction.state.lineBreak
817
+ ) : [wholeReplacement(current, snapshot.text)]
415
818
  });
416
- if (result.kind !== "diagnostics" || !this.publishable(result, view, epoch, "diagnostics"))
417
- return;
418
- if (this.viewEpoch !== viewEpoch) return;
419
- view.dispatch(
420
- lint.setDiagnostics(
421
- view.state,
422
- result.diagnostics.map((entry) => translateDiagnostic(entry, view))
423
- )
424
- );
425
- });
426
- }
427
- cancelDiagnosticsFrame() {
428
- if (this.diagnosticsFrame === null || !this.view) return;
429
- animationWindow(this.view).cancelAnimationFrame(this.diagnosticsFrame);
430
- this.diagnosticsFrame = null;
431
- }
432
- cancellation(kind, epoch) {
433
- return {
434
- isCancellationRequested: () => this.disposed || this.epoch(kind) !== epoch
435
- };
436
- }
437
- publishable(identity, view, epoch, kind) {
438
- return !this.disposed && this.view === view && this.epoch(kind) === epoch && "status" in identity && identity.status === "current" && this.service.isCurrent(identity);
439
- }
440
- epoch(kind) {
441
- if (kind === "completion") return this.completionEpoch;
442
- if (kind === "hover") return this.hoverEpoch;
443
- return this.diagnosticsEpoch;
444
- }
445
- invalidateRequests() {
446
- this.completionEpoch += 1;
447
- this.hoverEpoch += 1;
448
- this.diagnosticsEpoch += 1;
449
- }
450
- nextRevision() {
451
- if (this.revision >= Number.MAX_SAFE_INTEGER)
452
- throw new RangeError("Kalada document revision exhausted");
453
- this.revision += 1;
454
- return this.revision;
455
- }
456
- requireDocument() {
457
- const document = this.service.getDocument(this.uri);
458
- if (!document) throw new Error("Kalada editor document is not open");
459
- return document;
460
- }
461
- notify(snapshot) {
462
- try {
463
- this.onDocumentChange?.(snapshot);
464
- } catch {
819
+ if (updated.text !== snapshot.text)
820
+ throw new Error("CodeMirror transaction translation changed document text");
821
+ committed(snapshot, updated);
822
+ },
823
+ renderHover(hover, from, to) {
824
+ const info = provider.hovers.get(hover);
825
+ if (!info) throw new Error("Missing Kalada hover presentation");
826
+ return { ...hoverTooltip2(info, from), end: to };
465
827
  }
466
- }
467
- assertOpen() {
468
- if (this.disposed) throw new Error("Kalada editor session is disposed");
469
- }
470
- };
828
+ };
829
+ }
471
830
 
472
831
  exports.createKaladaEditorSession = createKaladaEditorSession;
473
832
  //# sourceMappingURL=index.cjs.map