@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 +126 -1
- package/dist/chunk-GCHU6EL7.js +566 -0
- package/dist/chunk-GCHU6EL7.js.map +1 -0
- package/dist/editor.cjs +568 -0
- package/dist/editor.cjs.map +1 -0
- package/dist/editor.d.cts +74 -0
- package/dist/editor.d.ts +74 -0
- package/dist/editor.js +3 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.cjs +717 -358
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +166 -365
- package/dist/index.js.map +1 -1
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @kalada/codemirror
|
|
2
2
|
|
|
3
|
-
|
|
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
|