@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.js CHANGED
@@ -1,20 +1,95 @@
1
- import { autocompletion, completionKeymap, closeCompletion } from '@codemirror/autocomplete';
2
- import { history, historyKeymap } from '@codemirror/commands';
3
- import { setDiagnostics } from '@codemirror/lint';
4
- import { StateEffect, StateField, Compartment, EditorState, Transaction, EditorSelection, Text } from '@codemirror/state';
5
- import { showTooltip, ViewPlugin, hoverTooltip as hoverTooltip$1, keymap, EditorView, closeHoverTooltips } from '@codemirror/view';
1
+ import { NeutralEditorSession } from './chunk-GCHU6EL7.js';
6
2
 
7
- // src/session.ts
8
- function detectLineSeparator(text) {
9
- if (text.includes("\r\n")) return "\r\n";
10
- if (text.includes("\r")) return "\r";
11
- return "\n";
12
- }
13
- function textForEditor(text) {
14
- return Text.of(text.split(/\r\n|\r|\n/u));
15
- }
16
- function textFromEditor(document, lineSeparator) {
17
- return document.sliceString(0, document.length, lineSeparator);
3
+ // src/kalada-provider.ts
4
+ var KaladaProvider = class {
5
+ service;
6
+ uri;
7
+ hovers = /* @__PURE__ */ new WeakMap();
8
+ identities = /* @__PURE__ */ new WeakMap();
9
+ constructor(service, uri) {
10
+ this.service = service;
11
+ this.uri = uri;
12
+ }
13
+ isCurrent(identity) {
14
+ const original = this.identities.get(identity);
15
+ return !!original && this.service.isCurrent(original);
16
+ }
17
+ diagnostics(request) {
18
+ const result = this.service.diagnostics(this.uri, cancellation(request));
19
+ if (result.kind !== "diagnostics" || !this.service.isCurrent(result)) return null;
20
+ return this.tagged(
21
+ request,
22
+ result,
23
+ result.diagnostics.map((item) => ({
24
+ ...item.source ? this.range(item.source.range) : { from: 0, to: 0 },
25
+ message: item.message,
26
+ severity: "error"
27
+ }))
28
+ );
29
+ }
30
+ completion(request, offset) {
31
+ const result = this.service.completion(
32
+ this.uri,
33
+ this.document().lineIndex.positionAt(offset),
34
+ cancellation(request)
35
+ );
36
+ if (result.kind !== "completion" || !this.service.isCurrent(result)) return null;
37
+ return this.tagged(
38
+ request,
39
+ result,
40
+ result.items.map((item) => ({
41
+ label: item.label,
42
+ edit: { ...this.range(item.edit.range), text: item.edit.text },
43
+ type: item.kind === "binding" ? "variable" : item.kind,
44
+ detail: `${item.support}, ${item.presence}`,
45
+ boost: item.support === "common" ? 10 : 0
46
+ }))
47
+ );
48
+ }
49
+ hover(request, offset) {
50
+ const result = this.service.hover(
51
+ this.uri,
52
+ this.document().lineIndex.positionAt(offset),
53
+ cancellation(request)
54
+ );
55
+ if (result.kind !== "hover" || !result.hover || !this.service.isCurrent(result)) return null;
56
+ const hover = { ...this.range(result.hover.range), content: "" };
57
+ this.hovers.set(hover, result.hover);
58
+ return this.tagged(request, result, hover);
59
+ }
60
+ format(request) {
61
+ const result = this.service.format(this.uri, cancellation(request));
62
+ if (result.kind !== "format" || !result.edit || !this.service.isCurrent(result)) return null;
63
+ return this.tagged(request, result, {
64
+ ...this.range(result.edit.range),
65
+ text: result.edit.text
66
+ });
67
+ }
68
+ tagged(request, original, value) {
69
+ const result = {
70
+ uri: this.uri,
71
+ version: request.snapshot.version,
72
+ environmentGeneration: request.snapshot.environmentGeneration,
73
+ value
74
+ };
75
+ this.identities.set(result, original);
76
+ return result;
77
+ }
78
+ range(source) {
79
+ const document = this.document();
80
+ return {
81
+ from: document.lineIndex.offsetAt(source.start),
82
+ to: document.lineIndex.offsetAt(source.end)
83
+ };
84
+ }
85
+ document() {
86
+ const document = this.service.getDocument(this.uri);
87
+ if (!document) throw new Error("Kalada editor document is not open");
88
+ return document;
89
+ }
90
+ };
91
+ function cancellation(request) {
92
+ return { cancellation: { isCancellationRequested: () => request.signal.aborted } };
18
93
  }
19
94
 
20
95
  // src/render.ts
@@ -66,6 +141,17 @@ function evidenceText(evidence) {
66
141
  return `${evidence.code} at ${evidence.path.join(".") || "root"}`;
67
142
  }
68
143
 
144
+ // src/session-helpers.ts
145
+ function wholeReplacement(snapshot, text) {
146
+ return {
147
+ range: {
148
+ start: { line: 0, character: 0 },
149
+ end: snapshot.lineIndex.positionAt(snapshot.text.length)
150
+ },
151
+ text
152
+ };
153
+ }
154
+
69
155
  // src/translation.ts
70
156
  function changesToEdits(startDoc, newDoc, changes, snapshot, lineSeparator = "\n") {
71
157
  const offsets = [];
@@ -81,14 +167,6 @@ function changesToEdits(startDoc, newDoc, changes, snapshot, lineSeparator = "\n
81
167
  if (translated.every((edit) => edit !== null)) return translated;
82
168
  return [wholeDocumentEdit(snapshot, newDoc.sliceString(0, newDoc.length, lineSeparator))];
83
169
  }
84
- function rangeToOffsets(document, range) {
85
- const from = offsetAt(document, range.start.line, range.start.character);
86
- const to = offsetAt(document, range.end.line, range.end.character);
87
- return from === null || to === null || from > to ? null : { from, to };
88
- }
89
- function diagnosticOffsets(document, range) {
90
- return rangeToOffsets(document, range) ?? { from: 0, to: 0 };
91
- }
92
170
  function offsetEdit(edit, document, index) {
93
171
  const start = positionAt(document, edit.from);
94
172
  const end = positionAt(document, edit.to);
@@ -99,14 +177,6 @@ function positionAt(document, offset) {
99
177
  const line = document.lineAt(offset);
100
178
  return { line: line.number - 1, character: offset - line.from };
101
179
  }
102
- function offsetAt(document, line, character) {
103
- if (!Number.isSafeInteger(line) || !Number.isSafeInteger(character) || line < 0 || character < 0) {
104
- return null;
105
- }
106
- if (line >= document.lines) return null;
107
- const found = document.line(line + 1);
108
- return character <= found.length ? found.from + character : null;
109
- }
110
180
  function coalesceInserts(edits) {
111
181
  const output = [];
112
182
  for (const edit of edits) {
@@ -127,345 +197,76 @@ function wholeDocumentEdit(snapshot, text) {
127
197
  };
128
198
  }
129
199
 
130
- // src/session-helpers.ts
131
- function positionAt2(document, offset) {
132
- const line = document.lineAt(offset);
133
- return { line: line.number - 1, character: offset - line.from };
134
- }
135
- function translatedTooltip(result, view) {
136
- const range = result.hover ? rangeToOffsets(view.state.doc, result.hover.range) : null;
137
- if (!range || !result.hover) return null;
138
- return { ...hoverTooltip(result.hover, range.from), end: range.to };
139
- }
140
- function translateDiagnostic(diagnostic, view) {
141
- const range = diagnostic.source ? diagnosticOffsets(view.state.doc, diagnostic.source.range) : { from: 0, to: 0 };
142
- return { ...range, severity: "error", message: diagnostic.message };
143
- }
144
- function animationWindow(view) {
145
- const window = view.dom.ownerDocument.defaultView;
146
- if (!window) throw new Error("CodeMirror view has no window");
147
- return window;
148
- }
149
- function wholeReplacement(snapshot, text) {
200
+ // src/session.ts
201
+ function createKaladaEditorSession(options) {
202
+ const { service } = options;
203
+ const opened = service.openDocument(options.document);
204
+ let disposed = false;
205
+ const provider = new KaladaProvider(service, opened.uri);
206
+ const committed = /* @__PURE__ */ new WeakMap();
207
+ const core = new NeutralEditorSession(
208
+ {
209
+ document: opened,
210
+ ariaLabel: "Kalada expression editor",
211
+ provider,
212
+ onDocumentChange: (identity) => {
213
+ const snapshot = committed.get(identity);
214
+ if (snapshot) options.onDocumentChange?.(snapshot);
215
+ }
216
+ },
217
+ kaladaBridge(provider, (identity, snapshot) => committed.set(identity, snapshot))
218
+ );
150
219
  return {
151
- range: {
152
- start: { line: 0, character: 0 },
153
- end: snapshot.lineIndex.positionAt(snapshot.text.length)
220
+ extension: core.extension,
221
+ refreshEnvironment: () => core.refreshEnvironment(),
222
+ replaceDocument: (text) => core.replaceDocument(text),
223
+ format() {
224
+ const formatted = core.format();
225
+ if (typeof formatted !== "boolean") throw new Error("Kalada formatting must be synchronous");
226
+ return formatted;
154
227
  },
155
- text
156
- };
157
- }
158
- var setKeyboardTooltip = StateEffect.define();
159
- var keyboardTooltipField = StateField.define({
160
- create: () => [],
161
- update(value, transaction) {
162
- let next = value;
163
- for (const effect of transaction.effects) {
164
- if (effect.is(setKeyboardTooltip)) next = effect.value ? [effect.value] : [];
228
+ dispose() {
229
+ if (disposed) return;
230
+ disposed = true;
231
+ core.dispose();
232
+ service.closeDocument(opened.uri);
165
233
  }
166
- return next;
167
- },
168
- provide: (field) => showTooltip.computeN([field], (state) => state.field(field))
169
- });
170
-
171
- // src/session.ts
172
- function createKaladaEditorSession(options) {
173
- return new EditorSession(options);
234
+ };
174
235
  }
175
- var EditorSession = class {
176
- extension;
177
- service;
178
- uri;
179
- onDocumentChange;
180
- lineSeparator = new Compartment();
181
- view = null;
182
- expectedText;
183
- revision;
184
- disposed = false;
185
- viewEpoch = 0;
186
- completionEpoch = 0;
187
- hoverEpoch = 0;
188
- diagnosticsEpoch = 0;
189
- diagnosticsFrame = null;
190
- constructor(options) {
191
- this.service = options.service;
192
- this.uri = options.document.uri;
193
- this.onDocumentChange = options.onDocumentChange;
194
- const opened = this.service.openDocument(options.document);
195
- this.expectedText = opened.text;
196
- this.revision = opened.version;
197
- this.extension = this.createExtension();
198
- }
199
- refreshEnvironment() {
200
- this.assertOpen();
201
- this.invalidateRequests();
202
- const view = this.view;
203
- if (!view) return;
204
- this.clearEditor(view, false);
205
- this.scheduleDiagnostics(view);
206
- }
207
- replaceDocument(text) {
208
- this.assertOpen();
209
- if (typeof text !== "string") throw new TypeError("Replacement text must be a string");
210
- const view = this.view;
211
- if (view) {
212
- const separator = detectLineSeparator(text);
213
- view.dispatch({
214
- changes: { from: 0, to: view.state.doc.length, insert: textForEditor(text) },
215
- selection: EditorSelection.cursor(0),
216
- annotations: Transaction.addToHistory.of(true),
217
- effects: separator === view.state.lineBreak ? void 0 : this.lineSeparator.reconfigure(EditorState.lineSeparator.of(separator))
218
- });
219
- return;
220
- }
221
- const current = this.requireDocument();
222
- const snapshot = this.service.updateDocument({
223
- uri: this.uri,
224
- version: this.nextRevision(),
225
- edits: [wholeReplacement(current, text)]
226
- });
227
- this.expectedText = snapshot.text;
228
- this.invalidateRequests();
229
- this.notify(snapshot);
230
- }
231
- format() {
232
- this.assertOpen();
233
- const view = this.view;
234
- if (!view) return false;
235
- const epoch = this.completionEpoch;
236
- const result = this.service.format(this.uri, {
237
- cancellation: this.cancellation("completion", epoch)
238
- });
239
- if (result.kind !== "format" || !result.edit || !this.publishable(result, view, epoch, "completion")) {
240
- return false;
241
- }
242
- const range = rangeToOffsets(view.state.doc, result.edit.range);
243
- if (!range) return false;
244
- view.dispatch({
245
- changes: { ...range, insert: textForEditor(result.edit.text) },
246
- annotations: Transaction.addToHistory.of(true)
247
- });
248
- return true;
249
- }
250
- dispose() {
251
- if (this.disposed) return;
252
- this.disposed = true;
253
- this.invalidateRequests();
254
- this.cancelDiagnosticsFrame();
255
- const view = this.view;
256
- if (view) this.clearEditor(view, true);
257
- this.view = null;
258
- this.service.closeDocument(this.uri);
259
- }
260
- createExtension() {
261
- const session = this;
262
- const lifecycle = ViewPlugin.define((view) => {
263
- session.attach(view);
264
- return {
265
- update(update) {
266
- session.updateView(update);
267
- },
268
- destroy() {
269
- session.detach(view);
270
- }
271
- };
272
- });
273
- return [
274
- lifecycle,
275
- this.lineSeparator.of(EditorState.lineSeparator.of(detectLineSeparator(this.expectedText))),
276
- history(),
277
- autocompletion({ override: [(context) => this.complete(context)] }),
278
- hoverTooltip$1((view, position) => this.pointerHover(view, position)),
279
- keyboardTooltipField,
280
- keymap.of([
281
- ...completionKeymap,
282
- ...historyKeymap,
283
- { key: "Mod-Shift-h", run: (view) => this.keyboardHover(view) },
284
- { key: "Escape", run: (view) => this.clearInteractions(view) }
285
- ]),
286
- EditorView.contentAttributes.of({ "aria-label": "Kalada expression editor" })
287
- ];
288
- }
289
- attach(view) {
290
- this.assertOpen();
291
- if (this.view && this.view !== view)
292
- throw new Error("Kalada editor session already has a view");
293
- if (textFromEditor(view.state.doc, view.state.lineBreak) !== this.expectedText) {
294
- throw new Error("CodeMirror document does not match the opened Kalada document");
295
- }
296
- this.view = view;
297
- this.viewEpoch += 1;
298
- this.scheduleDiagnostics(view);
299
- }
300
- detach(view) {
301
- if (this.view !== view) return;
302
- this.cancelDiagnosticsFrame();
303
- this.invalidateRequests();
304
- this.view = null;
305
- this.viewEpoch += 1;
306
- }
307
- updateView(update) {
308
- if (this.disposed || !update.docChanged) return;
309
- this.invalidateRequests();
310
- for (const transaction of update.transactions) {
311
- if (transaction.docChanged) this.applyTransaction(transaction);
312
- }
313
- this.expectedText = textFromEditor(update.state.doc, update.state.lineBreak);
314
- this.scheduleDiagnostics(update.view);
315
- }
316
- applyTransaction(transaction) {
317
- const current = this.requireDocument();
318
- const startText = textFromEditor(transaction.startState.doc, transaction.startState.lineBreak);
319
- if (current.text !== startText) {
320
- throw new Error("CodeMirror and language-service snapshots diverged");
321
- }
322
- const edits = changesToEdits(
323
- transaction.startState.doc,
324
- transaction.newDoc,
325
- transaction.changes,
326
- current,
327
- transaction.state.lineBreak
328
- );
329
- const snapshot = this.service.updateDocument({
330
- uri: this.uri,
331
- version: this.nextRevision(),
332
- edits
333
- });
334
- if (snapshot.text !== textFromEditor(transaction.newDoc, transaction.state.lineBreak)) {
335
- throw new Error("CodeMirror transaction translation changed document text");
336
- }
337
- this.expectedText = snapshot.text;
338
- this.notify(snapshot);
339
- }
340
- complete(context) {
341
- const view = this.view;
342
- if (!view || view.state !== context.state || this.disposed) return null;
343
- const epoch = ++this.completionEpoch;
344
- const result = this.service.completion(this.uri, positionAt2(context.state.doc, context.pos), {
345
- cancellation: this.cancellation("completion", epoch)
346
- });
347
- if (result.kind !== "completion" || !this.publishable(result, view, epoch, "completion")) {
348
- return null;
349
- }
350
- const translated = result.items.map(
351
- (item) => this.translateCompletion(item, result, view, epoch)
352
- );
353
- const available = translated.filter((entry) => entry !== null);
354
- const first = available[0];
355
- return first ? { from: first.from, to: first.to, options: available.map(({ option }) => option) } : null;
356
- }
357
- translateCompletion(item, identity, view, epoch) {
358
- const range = rangeToOffsets(view.state.doc, item.edit.range);
359
- if (!range) return null;
360
- const option = {
361
- label: item.label,
362
- type: item.kind === "binding" ? "variable" : item.kind,
363
- detail: `${item.support}, ${item.presence}`,
364
- boost: item.support === "common" ? 10 : 0,
365
- apply: (target) => this.applyCompletion(target, item, identity, epoch)
366
- };
367
- return { ...range, option };
368
- }
369
- applyCompletion(view, item, identity, epoch) {
370
- if (!this.publishable(identity, view, epoch, "completion")) return;
371
- const range = rangeToOffsets(view.state.doc, item.edit.range);
372
- if (!range) return;
373
- view.dispatch({ changes: { ...range, insert: item.edit.text } });
374
- }
375
- pointerHover(view, offset) {
376
- return this.requestHover(view, offset);
377
- }
378
- keyboardHover(view) {
379
- const tooltip = this.requestHover(view, view.state.selection.main.head);
380
- if (!tooltip) return false;
381
- view.dispatch({ effects: setKeyboardTooltip.of(tooltip) });
382
- return true;
383
- }
384
- requestHover(view, offset) {
385
- const epoch = ++this.hoverEpoch;
386
- const result = this.service.hover(this.uri, positionAt2(view.state.doc, offset), {
387
- cancellation: this.cancellation("hover", epoch)
388
- });
389
- if (result.kind !== "hover" || !result.hover || !this.publishable(result, view, epoch, "hover")) {
390
- return null;
391
- }
392
- return translatedTooltip(result, view);
393
- }
394
- clearInteractions(view) {
395
- this.clearEditor(view, false);
396
- view.focus();
397
- return true;
398
- }
399
- clearEditor(view, diagnostics) {
400
- closeCompletion(view);
401
- const interactions = { effects: [setKeyboardTooltip.of(null), closeHoverTooltips] };
402
- if (diagnostics) view.dispatch(interactions, setDiagnostics(view.state, []));
403
- else view.dispatch(interactions);
404
- }
405
- scheduleDiagnostics(view) {
406
- this.cancelDiagnosticsFrame();
407
- const epoch = ++this.diagnosticsEpoch;
408
- const viewEpoch = this.viewEpoch;
409
- this.diagnosticsFrame = animationWindow(view).requestAnimationFrame(() => {
410
- this.diagnosticsFrame = null;
411
- const result = this.service.diagnostics(this.uri, {
412
- cancellation: this.cancellation("diagnostics", epoch)
236
+ function kaladaBridge(provider, committed) {
237
+ return {
238
+ filterCompletions: true,
239
+ isCurrent: (identity) => provider.isCurrent(identity),
240
+ matches(snapshot) {
241
+ const current = provider.service.getDocument(provider.uri);
242
+ return !!current && current.uri === snapshot.uri && current.version === snapshot.version && current.text === snapshot.text;
243
+ },
244
+ changed(snapshot, previous, transaction) {
245
+ const current = provider.service.getDocument(provider.uri);
246
+ if (!current || current.text !== previous.text || current.version !== previous.version)
247
+ throw new Error("CodeMirror and language-service snapshots diverged");
248
+ const updated = provider.service.updateDocument({
249
+ uri: provider.uri,
250
+ version: snapshot.version,
251
+ edits: transaction ? changesToEdits(
252
+ transaction.startState.doc,
253
+ transaction.newDoc,
254
+ transaction.changes,
255
+ current,
256
+ transaction.state.lineBreak
257
+ ) : [wholeReplacement(current, snapshot.text)]
413
258
  });
414
- if (result.kind !== "diagnostics" || !this.publishable(result, view, epoch, "diagnostics"))
415
- return;
416
- if (this.viewEpoch !== viewEpoch) return;
417
- view.dispatch(
418
- setDiagnostics(
419
- view.state,
420
- result.diagnostics.map((entry) => translateDiagnostic(entry, view))
421
- )
422
- );
423
- });
424
- }
425
- cancelDiagnosticsFrame() {
426
- if (this.diagnosticsFrame === null || !this.view) return;
427
- animationWindow(this.view).cancelAnimationFrame(this.diagnosticsFrame);
428
- this.diagnosticsFrame = null;
429
- }
430
- cancellation(kind, epoch) {
431
- return {
432
- isCancellationRequested: () => this.disposed || this.epoch(kind) !== epoch
433
- };
434
- }
435
- publishable(identity, view, epoch, kind) {
436
- return !this.disposed && this.view === view && this.epoch(kind) === epoch && "status" in identity && identity.status === "current" && this.service.isCurrent(identity);
437
- }
438
- epoch(kind) {
439
- if (kind === "completion") return this.completionEpoch;
440
- if (kind === "hover") return this.hoverEpoch;
441
- return this.diagnosticsEpoch;
442
- }
443
- invalidateRequests() {
444
- this.completionEpoch += 1;
445
- this.hoverEpoch += 1;
446
- this.diagnosticsEpoch += 1;
447
- }
448
- nextRevision() {
449
- if (this.revision >= Number.MAX_SAFE_INTEGER)
450
- throw new RangeError("Kalada document revision exhausted");
451
- this.revision += 1;
452
- return this.revision;
453
- }
454
- requireDocument() {
455
- const document = this.service.getDocument(this.uri);
456
- if (!document) throw new Error("Kalada editor document is not open");
457
- return document;
458
- }
459
- notify(snapshot) {
460
- try {
461
- this.onDocumentChange?.(snapshot);
462
- } catch {
259
+ if (updated.text !== snapshot.text)
260
+ throw new Error("CodeMirror transaction translation changed document text");
261
+ committed(snapshot, updated);
262
+ },
263
+ renderHover(hover, from, to) {
264
+ const info = provider.hovers.get(hover);
265
+ if (!info) throw new Error("Missing Kalada hover presentation");
266
+ return { ...hoverTooltip(info, from), end: to };
463
267
  }
464
- }
465
- assertOpen() {
466
- if (this.disposed) throw new Error("Kalada editor session is disposed");
467
- }
468
- };
268
+ };
269
+ }
469
270
 
470
271
  export { createKaladaEditorSession };
471
272
  //# sourceMappingURL=index.js.map