@nbt-dev/components 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +177 -0
  2. package/README.md +10 -0
  3. package/TRADEMARKS.md +49 -0
  4. package/dist/chunk-3ZM6YOA4.js +704 -0
  5. package/dist/chunk-3ZM6YOA4.js.map +7 -0
  6. package/dist/chunk-7B2T5ZNG.js +467 -0
  7. package/dist/chunk-7B2T5ZNG.js.map +7 -0
  8. package/dist/chunk-S7VBQE6Y.js +636 -0
  9. package/dist/chunk-S7VBQE6Y.js.map +7 -0
  10. package/dist/chunk-UPEOXMLZ.js +625 -0
  11. package/dist/chunk-UPEOXMLZ.js.map +7 -0
  12. package/dist/core/auth.d.ts +13 -0
  13. package/dist/core/bulk-decoder.d.ts +13 -0
  14. package/dist/core/config.d.ts +10 -0
  15. package/dist/core/data-store.d.ts +20 -0
  16. package/dist/core/index.d.ts +9 -0
  17. package/dist/core/use-bulk-stream.d.ts +24 -0
  18. package/dist/core/use-cartridge-info.d.ts +14 -0
  19. package/dist/core/utils.d.ts +2 -0
  20. package/dist/editor/index.d.ts +7 -0
  21. package/dist/editor/index.js +16 -0
  22. package/dist/editor/index.js.map +7 -0
  23. package/dist/editor/lsp-client.d.ts +57 -0
  24. package/dist/editor/lsp-extensions.d.ts +4 -0
  25. package/dist/editor/nbt-editor.d.ts +13 -0
  26. package/dist/editor/nbt-language.d.ts +7 -0
  27. package/dist/generated/bulk-protocol.d.ts +36 -0
  28. package/dist/graph/diagram.d.ts +5 -0
  29. package/dist/graph/entity-graph-utils.d.ts +92 -0
  30. package/dist/graph/entity-node.d.ts +9 -0
  31. package/dist/graph/index.d.ts +5 -0
  32. package/dist/graph/index.js +19 -0
  33. package/dist/graph/index.js.map +7 -0
  34. package/dist/index.d.ts +4 -0
  35. package/dist/index.js +134 -0
  36. package/dist/index.js.map +7 -0
  37. package/dist/styles.css +2 -0
  38. package/dist/table/data-table.d.ts +9 -0
  39. package/dist/table/index.d.ts +3 -0
  40. package/dist/table/index.js +11 -0
  41. package/dist/table/index.js.map +7 -0
  42. package/dist/table/value-popover.d.ts +18 -0
  43. package/package.json +77 -0
  44. package/src/core/auth.ts +100 -0
  45. package/src/core/bulk-decoder.ts +178 -0
  46. package/src/core/config.tsx +39 -0
  47. package/src/core/data-store.ts +113 -0
  48. package/src/core/index.ts +34 -0
  49. package/src/core/use-bulk-stream.ts +412 -0
  50. package/src/core/use-cartridge-info.ts +100 -0
  51. package/src/core/utils.ts +6 -0
  52. package/src/editor/index.ts +13 -0
  53. package/src/editor/lsp-client.ts +227 -0
  54. package/src/editor/lsp-extensions.ts +191 -0
  55. package/src/editor/nbt-editor.tsx +142 -0
  56. package/src/editor/nbt-language.ts +151 -0
  57. package/src/generated/bulk-protocol.ts +63 -0
  58. package/src/graph/diagram.tsx +296 -0
  59. package/src/graph/entity-graph-utils.ts +423 -0
  60. package/src/graph/entity-node.tsx +122 -0
  61. package/src/graph/index.ts +19 -0
  62. package/src/index.ts +7 -0
  63. package/src/styles.css +94 -0
  64. package/src/table/data-table.tsx +274 -0
  65. package/src/table/index.ts +5 -0
  66. package/src/table/value-popover.tsx +230 -0
@@ -0,0 +1,13 @@
1
+ export { NbtEditor } from "./nbt-editor";
2
+ export type { NbtEditorProps } from "./nbt-editor";
3
+ export { nbtLanguage, nbtLanguageSupport } from "./nbt-language";
4
+ export { NbtLspClient } from "./lsp-client";
5
+ export type {
6
+ LspPosition,
7
+ LspRange,
8
+ LspDiagnostic,
9
+ LspCompletionItem,
10
+ LspLocation,
11
+ } from "./lsp-client";
12
+ export { lspExtensions } from "./lsp-extensions";
13
+ export type { GotoDefHandler } from "./lsp-extensions";
@@ -0,0 +1,227 @@
1
+ // Minimal LSP client over WebSocket for the console's dev-mode nbt_lsp bridge
2
+ // (/_console/dev/lsp). One JSON-RPC message per WS text frame — no
3
+ // Content-Length framing (the bridge's write_proc emits bare JSON).
4
+ //
5
+ // Hand-rolled on purpose: the server speaks ~10 methods with full-text sync,
6
+ // and a library client (codemirror-languageserver) would drag transitive
7
+ // runtime deps into every host app.
8
+
9
+ export type LspPosition = { line: number; character: number };
10
+ export type LspRange = { start: LspPosition; end: LspPosition };
11
+ export type LspDiagnostic = {
12
+ range: LspRange;
13
+ severity?: number; // 1=error 2=warning 3=info 4=hint
14
+ message: string;
15
+ source?: string;
16
+ };
17
+ export type LspCompletionItem = {
18
+ label: string;
19
+ kind?: number;
20
+ detail?: string;
21
+ };
22
+ export type LspLocation = { uri: string; range: LspRange };
23
+
24
+ type Pending = {
25
+ resolve: (v: unknown) => void;
26
+ reject: (e: Error) => void;
27
+ };
28
+
29
+ export class NbtLspClient {
30
+ private url: string;
31
+ private rootUri: string | null = null;
32
+ private ws: WebSocket | null = null;
33
+ private nextId = 1;
34
+ private pending = new Map<number, Pending>();
35
+ private diagHandlers = new Map<string, (d: LspDiagnostic[]) => void>();
36
+ // Open buffers, kept for replay across reconnects.
37
+ private docs = new Map<string, { text: string; version: number }>();
38
+ private ready = false;
39
+ private queue: string[] = [];
40
+ private closed = false;
41
+ private retryMs = 500;
42
+
43
+ constructor(url: string) {
44
+ this.url = url;
45
+ this.connect();
46
+ }
47
+
48
+ dispose() {
49
+ this.closed = true;
50
+ this.ws?.close();
51
+ this.pending.forEach((p) => p.reject(new Error("lsp client disposed")));
52
+ this.pending.clear();
53
+ }
54
+
55
+ private connect() {
56
+ if (this.closed) return;
57
+ const ws = new WebSocket(this.url);
58
+ this.ws = ws;
59
+ ws.onopen = () => {
60
+ this.retryMs = 500;
61
+ if (this.rootUri) this.sendInitialize(this.rootUri);
62
+ };
63
+ ws.onmessage = (ev) => {
64
+ if (typeof ev.data !== "string") return;
65
+ this.onMessage(ev.data);
66
+ };
67
+ ws.onclose = () => {
68
+ this.ready = false;
69
+ this.ws = null;
70
+ for (const p of this.pending.values())
71
+ p.reject(new Error("lsp connection closed"));
72
+ this.pending.clear();
73
+ if (!this.closed) {
74
+ setTimeout(() => this.connect(), this.retryMs);
75
+ this.retryMs = Math.min(this.retryMs * 2, 10_000);
76
+ }
77
+ };
78
+ }
79
+
80
+ // Called once by the host with file://<projectRoot>; also re-sent after every
81
+ // reconnect, followed by didOpen replays for tracked buffers.
82
+ initialize(rootUri: string) {
83
+ this.rootUri = rootUri;
84
+ if (this.ws?.readyState === WebSocket.OPEN) this.sendInitialize(rootUri);
85
+ }
86
+
87
+ private sendInitialize(rootUri: string) {
88
+ const id = this.nextId++;
89
+ this.pending.set(id, {
90
+ resolve: () => {
91
+ this.sendRaw({ jsonrpc: "2.0", method: "initialized", params: {} });
92
+ this.ready = true;
93
+ for (const [uri, d] of this.docs) {
94
+ this.sendRaw({
95
+ jsonrpc: "2.0",
96
+ method: "textDocument/didOpen",
97
+ params: {
98
+ textDocument: {
99
+ uri,
100
+ languageId: "nbt",
101
+ version: d.version,
102
+ text: d.text,
103
+ },
104
+ },
105
+ });
106
+ }
107
+ const q = this.queue;
108
+ this.queue = [];
109
+ for (const m of q) this.ws?.send(m);
110
+ },
111
+ reject: () => {},
112
+ });
113
+ this.ws?.send(
114
+ JSON.stringify({
115
+ jsonrpc: "2.0",
116
+ id,
117
+ method: "initialize",
118
+ params: { rootUri },
119
+ }),
120
+ );
121
+ }
122
+
123
+ private sendRaw(msg: object) {
124
+ const s = JSON.stringify(msg);
125
+ if (this.ready && this.ws?.readyState === WebSocket.OPEN) this.ws.send(s);
126
+ else this.queue.push(s);
127
+ }
128
+
129
+ private onMessage(data: string) {
130
+ let msg: any;
131
+ try {
132
+ msg = JSON.parse(data);
133
+ } catch {
134
+ return;
135
+ }
136
+ if (msg.id != null && (msg.result !== undefined || msg.error)) {
137
+ const p = this.pending.get(msg.id);
138
+ if (!p) return;
139
+ this.pending.delete(msg.id);
140
+ if (msg.error) p.reject(new Error(msg.error.message ?? "lsp error"));
141
+ else p.resolve(msg.result);
142
+ return;
143
+ }
144
+ if (msg.method === "textDocument/publishDiagnostics") {
145
+ const uri = msg.params?.uri as string;
146
+ const handler = this.diagHandlers.get(uri);
147
+ if (handler) handler((msg.params?.diagnostics ?? []) as LspDiagnostic[]);
148
+ }
149
+ }
150
+
151
+ request<T>(method: string, params: object): Promise<T> {
152
+ const id = this.nextId++;
153
+ const p = new Promise<T>((resolve, reject) => {
154
+ this.pending.set(id, { resolve: resolve as Pending["resolve"], reject });
155
+ });
156
+ this.sendRaw({ jsonrpc: "2.0", id, method, params });
157
+ return p;
158
+ }
159
+
160
+ onDiagnostics(uri: string, handler: (d: LspDiagnostic[]) => void): () => void {
161
+ this.diagHandlers.set(uri, handler);
162
+ return () => {
163
+ if (this.diagHandlers.get(uri) === handler) this.diagHandlers.delete(uri);
164
+ };
165
+ }
166
+
167
+ didOpen(uri: string, text: string) {
168
+ const existing = this.docs.get(uri);
169
+ if (existing) {
170
+ // Re-opening an already-tracked buffer (tab switch) — just resync.
171
+ this.didChange(uri, text);
172
+ return;
173
+ }
174
+ this.docs.set(uri, { text, version: 1 });
175
+ this.sendRaw({
176
+ jsonrpc: "2.0",
177
+ method: "textDocument/didOpen",
178
+ params: { textDocument: { uri, languageId: "nbt", version: 1, text } },
179
+ });
180
+ }
181
+
182
+ didChange(uri: string, text: string) {
183
+ const d = this.docs.get(uri);
184
+ if (!d) return this.didOpen(uri, text);
185
+ if (d.text === text) return;
186
+ d.text = text;
187
+ d.version += 1;
188
+ this.sendRaw({
189
+ jsonrpc: "2.0",
190
+ method: "textDocument/didChange",
191
+ params: {
192
+ textDocument: { uri, version: d.version },
193
+ contentChanges: [{ text }],
194
+ },
195
+ });
196
+ }
197
+
198
+ didClose(uri: string) {
199
+ if (!this.docs.delete(uri)) return;
200
+ this.sendRaw({
201
+ jsonrpc: "2.0",
202
+ method: "textDocument/didClose",
203
+ params: { textDocument: { uri } },
204
+ });
205
+ }
206
+
207
+ completion(uri: string, pos: LspPosition): Promise<{ items: LspCompletionItem[] } | LspCompletionItem[] | null> {
208
+ return this.request("textDocument/completion", {
209
+ textDocument: { uri },
210
+ position: pos,
211
+ });
212
+ }
213
+
214
+ hover(uri: string, pos: LspPosition): Promise<{ contents?: { value?: string } } | null> {
215
+ return this.request("textDocument/hover", {
216
+ textDocument: { uri },
217
+ position: pos,
218
+ });
219
+ }
220
+
221
+ definition(uri: string, pos: LspPosition): Promise<LspLocation | LspLocation[] | null> {
222
+ return this.request("textDocument/definition", {
223
+ textDocument: { uri },
224
+ position: pos,
225
+ });
226
+ }
227
+ }
@@ -0,0 +1,191 @@
1
+ // CodeMirror extensions wiring an editor buffer to the NbtLspClient:
2
+ // full-text didChange sync (debounced), pushed publishDiagnostics → lint,
3
+ // completion, hover, and F12 go-to-definition.
4
+
5
+ import { type Extension, type Text } from "@codemirror/state";
6
+ import {
7
+ EditorView,
8
+ ViewPlugin,
9
+ type ViewUpdate,
10
+ hoverTooltip,
11
+ keymap,
12
+ } from "@codemirror/view";
13
+ import {
14
+ autocompletion,
15
+ type CompletionContext,
16
+ type CompletionResult,
17
+ } from "@codemirror/autocomplete";
18
+ import { setDiagnostics, type Diagnostic } from "@codemirror/lint";
19
+ import {
20
+ NbtLspClient,
21
+ type LspDiagnostic,
22
+ type LspPosition,
23
+ type LspCompletionItem,
24
+ type LspLocation,
25
+ } from "./lsp-client";
26
+
27
+ function toLspPos(doc: Text, offset: number): LspPosition {
28
+ const line = doc.lineAt(offset);
29
+ return { line: line.number - 1, character: offset - line.from };
30
+ }
31
+
32
+ function fromLspPos(doc: Text, pos: LspPosition): number {
33
+ const lineNo = Math.min(Math.max(pos.line + 1, 1), doc.lines);
34
+ const line = doc.line(lineNo);
35
+ return Math.min(line.from + Math.max(pos.character, 0), line.to);
36
+ }
37
+
38
+ function toCmDiagnostics(doc: Text, diags: LspDiagnostic[]): Diagnostic[] {
39
+ return diags.map((d) => {
40
+ const from = fromLspPos(doc, d.range.start);
41
+ let to = fromLspPos(doc, d.range.end);
42
+ if (to <= from) to = Math.min(from + 1, doc.length);
43
+ const severity =
44
+ d.severity === 2 ? "warning" : d.severity === 3 || d.severity === 4 ? "info" : "error";
45
+ return { from, to, severity, message: d.message, source: d.source ?? "nbt" };
46
+ });
47
+ }
48
+
49
+ // LSP CompletionItemKind → CM completion type (drives the icon).
50
+ function completionType(kind?: number): string {
51
+ switch (kind) {
52
+ case 3: return "function";
53
+ case 5: return "property";
54
+ case 13: return "enum";
55
+ case 14: return "keyword";
56
+ case 22: return "class";
57
+ default: return "variable";
58
+ }
59
+ }
60
+
61
+ export type GotoDefHandler = (uri: string, line: number) => void;
62
+
63
+ export function lspExtensions(
64
+ client: NbtLspClient,
65
+ uri: string,
66
+ onGotoDefinition?: GotoDefHandler,
67
+ ): Extension[] {
68
+ // Sync + diagnostics plugin. didOpen on create, debounced didChange,
69
+ // diagnostics pushed by the server are dispatched into the lint state.
70
+ const syncPlugin = ViewPlugin.fromClass(
71
+ class {
72
+ private timer: ReturnType<typeof setTimeout> | null = null;
73
+ private unsubscribe: () => void;
74
+
75
+ constructor(readonly view: EditorView) {
76
+ client.didOpen(uri, view.state.doc.toString());
77
+ this.unsubscribe = client.onDiagnostics(uri, (diags) => {
78
+ const cm = toCmDiagnostics(this.view.state.doc, diags);
79
+ this.view.dispatch(setDiagnostics(this.view.state, cm));
80
+ });
81
+ }
82
+
83
+ update(u: ViewUpdate) {
84
+ if (!u.docChanged) return;
85
+ if (this.timer) clearTimeout(this.timer);
86
+ this.timer = setTimeout(() => {
87
+ this.timer = null;
88
+ client.didChange(uri, this.view.state.doc.toString());
89
+ }, 200);
90
+ }
91
+
92
+ destroy() {
93
+ if (this.timer) {
94
+ clearTimeout(this.timer);
95
+ client.didChange(uri, this.view.state.doc.toString());
96
+ }
97
+ this.unsubscribe();
98
+ }
99
+ },
100
+ );
101
+
102
+ const completionSource = async (
103
+ ctx: CompletionContext,
104
+ ): Promise<CompletionResult | null> => {
105
+ const word = ctx.matchBefore(/\w*/);
106
+ if (!word) return null;
107
+ if (word.from === word.to && !ctx.explicit) {
108
+ // Only fire automatically right after a trigger character.
109
+ const before = ctx.state.doc.sliceString(Math.max(0, ctx.pos - 1), ctx.pos);
110
+ if (before !== "." && before !== ":" && before !== '"') return null;
111
+ }
112
+ // Flush any pending edit so the server completes against the live buffer.
113
+ client.didChange(uri, ctx.state.doc.toString());
114
+ let result;
115
+ try {
116
+ result = await client.completion(uri, toLspPos(ctx.state.doc, ctx.pos));
117
+ } catch {
118
+ return null;
119
+ }
120
+ const items: LspCompletionItem[] = Array.isArray(result)
121
+ ? result
122
+ : (result?.items ?? []);
123
+ if (items.length === 0) return null;
124
+ return {
125
+ from: word.from,
126
+ options: items.map((it) => ({
127
+ label: it.label,
128
+ type: completionType(it.kind),
129
+ detail: it.detail,
130
+ })),
131
+ };
132
+ };
133
+
134
+ const hover = hoverTooltip(async (view, pos) => {
135
+ let result;
136
+ try {
137
+ result = await client.hover(uri, toLspPos(view.state.doc, pos));
138
+ } catch {
139
+ return null;
140
+ }
141
+ const value = result?.contents?.value;
142
+ if (!value) return null;
143
+ return {
144
+ pos,
145
+ create: () => {
146
+ const dom = document.createElement("pre");
147
+ dom.className = "nbt-hover-tooltip";
148
+ dom.style.cssText =
149
+ "margin:0;padding:6px 8px;max-width:480px;white-space:pre-wrap;font-size:11px;";
150
+ // Server emits fenced markdown code blocks; strip the fences, show text.
151
+ dom.textContent = value.replace(/```\w*\n?/g, "");
152
+ return { dom };
153
+ },
154
+ };
155
+ });
156
+
157
+ const gotoDef = keymap.of([
158
+ {
159
+ key: "F12",
160
+ run: (view) => {
161
+ const pos = toLspPos(view.state.doc, view.state.selection.main.head);
162
+ client
163
+ .definition(uri, pos)
164
+ .then((result) => {
165
+ const loc: LspLocation | null = Array.isArray(result)
166
+ ? (result[0] ?? null)
167
+ : result;
168
+ if (!loc?.uri) return;
169
+ if (loc.uri === uri) {
170
+ const off = fromLspPos(view.state.doc, loc.range.start);
171
+ view.dispatch({
172
+ selection: { anchor: off },
173
+ scrollIntoView: true,
174
+ });
175
+ } else {
176
+ onGotoDefinition?.(loc.uri, loc.range.start.line);
177
+ }
178
+ })
179
+ .catch(() => {});
180
+ return true;
181
+ },
182
+ },
183
+ ]);
184
+
185
+ return [
186
+ syncPlugin,
187
+ autocompletion({ override: [completionSource] }),
188
+ hover,
189
+ gotoDef,
190
+ ];
191
+ }
@@ -0,0 +1,142 @@
1
+ // CodeMirror 6 React wrapper for one NBT buffer. Recreated per uri (keyed by
2
+ // the host); owns the EditorView lifecycle, Ctrl+S save, and the LSP wiring.
3
+
4
+ import React from "react";
5
+ import { EditorState } from "@codemirror/state";
6
+ import {
7
+ EditorView,
8
+ keymap,
9
+ lineNumbers,
10
+ drawSelection,
11
+ highlightActiveLine,
12
+ } from "@codemirror/view";
13
+ import {
14
+ defaultKeymap,
15
+ history,
16
+ historyKeymap,
17
+ indentWithTab,
18
+ } from "@codemirror/commands";
19
+ import {
20
+ syntaxHighlighting,
21
+ defaultHighlightStyle,
22
+ bracketMatching,
23
+ } from "@codemirror/language";
24
+ import { lintGutter } from "@codemirror/lint";
25
+ import { searchKeymap } from "@codemirror/search";
26
+ import { completionKeymap, closeBrackets } from "@codemirror/autocomplete";
27
+ import { nbtLanguageSupport } from "./nbt-language";
28
+ import { lspExtensions, type GotoDefHandler } from "./lsp-extensions";
29
+ import { NbtLspClient } from "./lsp-client";
30
+
31
+ // Match the devtools panel chrome via its CSS variables (we render inside the
32
+ // .nimbit-devtools subtree, so these resolve to the scoped theme).
33
+ const nbtTheme = EditorView.theme(
34
+ {
35
+ "&": {
36
+ height: "100%",
37
+ fontSize: "12px",
38
+ backgroundColor: "var(--background)",
39
+ color: "var(--foreground)",
40
+ },
41
+ ".cm-content": { fontFamily: "ui-monospace, monospace" },
42
+ ".cm-gutters": {
43
+ backgroundColor: "var(--background)",
44
+ color: "var(--muted-foreground)",
45
+ borderRight: "1px solid var(--border)",
46
+ },
47
+ ".cm-activeLine": { backgroundColor: "color-mix(in srgb, var(--accent) 35%, transparent)" },
48
+ "&.cm-focused": { outline: "none" },
49
+ ".cm-tooltip": {
50
+ backgroundColor: "var(--popover)",
51
+ color: "var(--popover-foreground)",
52
+ border: "1px solid var(--border)",
53
+ },
54
+ },
55
+ { dark: true },
56
+ );
57
+
58
+ export type NbtEditorProps = {
59
+ uri: string;
60
+ value: string;
61
+ readOnly?: boolean;
62
+ lsp?: NbtLspClient | null;
63
+ onChange?: (text: string) => void;
64
+ onSave?: (text: string) => void;
65
+ onGotoDefinition?: GotoDefHandler;
66
+ };
67
+
68
+ export const NbtEditor: React.FC<NbtEditorProps> = ({
69
+ uri,
70
+ value,
71
+ readOnly = false,
72
+ lsp,
73
+ onChange,
74
+ onSave,
75
+ onGotoDefinition,
76
+ }) => {
77
+ const hostRef = React.useRef<HTMLDivElement | null>(null);
78
+ const viewRef = React.useRef<EditorView | null>(null);
79
+
80
+ // Latest callbacks without recreating the view.
81
+ const onChangeRef = React.useRef(onChange);
82
+ onChangeRef.current = onChange;
83
+ const onSaveRef = React.useRef(onSave);
84
+ onSaveRef.current = onSave;
85
+ const onGotoRef = React.useRef(onGotoDefinition);
86
+ onGotoRef.current = onGotoDefinition;
87
+
88
+ React.useEffect(() => {
89
+ const host = hostRef.current;
90
+ if (!host) return;
91
+
92
+ const extensions = [
93
+ lineNumbers(),
94
+ history(),
95
+ drawSelection(),
96
+ highlightActiveLine(),
97
+ bracketMatching(),
98
+ closeBrackets(),
99
+ lintGutter(),
100
+ syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
101
+ nbtLanguageSupport(),
102
+ nbtTheme,
103
+ keymap.of([
104
+ {
105
+ key: "Mod-s",
106
+ run: (view) => {
107
+ onSaveRef.current?.(view.state.doc.toString());
108
+ return true;
109
+ },
110
+ },
111
+ ...defaultKeymap,
112
+ ...historyKeymap,
113
+ ...searchKeymap,
114
+ ...completionKeymap,
115
+ indentWithTab,
116
+ ]),
117
+ EditorView.updateListener.of((u) => {
118
+ if (u.docChanged) onChangeRef.current?.(u.state.doc.toString());
119
+ }),
120
+ EditorState.readOnly.of(readOnly),
121
+ ];
122
+ if (lsp && !readOnly) {
123
+ extensions.push(
124
+ lspExtensions(lsp, uri, (defUri, line) => onGotoRef.current?.(defUri, line)),
125
+ );
126
+ }
127
+
128
+ const view = new EditorView({
129
+ state: EditorState.create({ doc: value, extensions }),
130
+ parent: host,
131
+ });
132
+ viewRef.current = view;
133
+ return () => {
134
+ view.destroy();
135
+ viewRef.current = null;
136
+ };
137
+ // Recreate per buffer identity / mode; `value` is only the initial doc.
138
+ // eslint-disable-next-line react-hooks/exhaustive-deps
139
+ }, [uri, readOnly, lsp]);
140
+
141
+ return <div ref={hostRef} className="h-full min-h-0 overflow-hidden" />;
142
+ };