@jxsuite/studio 0.13.0 → 0.14.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.
@@ -1,8 +1,7 @@
1
1
  /**
2
- * File Operations — open, load, save documents.
2
+ * File Operations — open, save, export documents.
3
3
  *
4
- * Each function that mutates state accepts a `commit(newState)` callback so the caller (studio.js)
5
- * can assign S and trigger render().
4
+ * All functions read/write directly from/to `activeTab.value` (the reactive tab).
6
5
  */
7
6
 
8
7
  import { unified } from "unified";
@@ -10,17 +9,13 @@ import remarkStringify from "remark-stringify";
10
9
  import remarkDirective from "remark-directive";
11
10
  import { stringify as stringifyYaml } from "yaml";
12
11
  import { jxToMd, jxDocToMd } from "../markdown/md-convert.js";
13
- import { createState } from "../store.js";
14
12
  import { locateDocument } from "../services/code-services.js";
15
13
  import { statusMessage } from "../panels/statusbar.js";
16
14
  import { getPlatform } from "../platform.js";
15
+ import { activeTab, openTab } from "../workspace/workspace.js";
17
16
 
18
- /**
19
- * Open a file via the File System Access API (or fallback input).
20
- *
21
- * @param {{ S: any; commit: (s: any) => void; renderToolbar: () => void }} ctx
22
- */
23
- export async function openFile({ S: _S, commit, renderToolbar: _renderToolbar }) {
17
+ /** Open a file via the File System Access API (or fallback input). */
18
+ export async function openFile() {
24
19
  try {
25
20
  if ("showOpenFilePicker" in window) {
26
21
  const [handle] = await /** @type {any} */ (window).showOpenFilePicker({
@@ -31,18 +26,21 @@ export async function openFile({ S: _S, commit, renderToolbar: _renderToolbar })
31
26
  });
32
27
  const file = await handle.getFile();
33
28
  const text = await file.text();
29
+ const documentPath = await locateDocument(handle.name);
34
30
 
35
31
  if (handle.name.endsWith(".md")) {
36
- const newState = await loadMarkdown(text, handle);
37
- commit(newState);
32
+ const { document, frontmatter } = await loadMarkdown(text);
33
+ openTab({
34
+ id: handle.name,
35
+ documentPath,
36
+ fileHandle: handle,
37
+ document,
38
+ frontmatter,
39
+ sourceFormat: "md",
40
+ });
38
41
  } else {
39
- const doc = JSON.parse(text);
40
- const newState = createState(doc);
41
- newState.fileHandle = handle;
42
- newState.dirty = false;
43
- newState.documentPath = await locateDocument(handle.name);
44
- await loadCompanionJS(handle, newState);
45
- commit(newState);
42
+ const document = JSON.parse(text);
43
+ openTab({ id: handle.name, documentPath, fileHandle: handle, document });
46
44
  }
47
45
 
48
46
  statusMessage(`Opened ${handle.name}`);
@@ -57,13 +55,11 @@ export async function openFile({ S: _S, commit, renderToolbar: _renderToolbar })
57
55
  const text = await file.text();
58
56
 
59
57
  if (file.name.endsWith(".md")) {
60
- const newState = await loadMarkdown(text, null);
61
- commit(newState);
58
+ const { document, frontmatter } = await loadMarkdown(text);
59
+ openTab({ id: file.name, document, frontmatter, sourceFormat: "md" });
62
60
  } else {
63
- const doc = JSON.parse(text);
64
- const newState = createState(doc);
65
- newState.dirty = false;
66
- commit(newState);
61
+ const document = JSON.parse(text);
62
+ openTab({ id: file.name, document });
67
63
  }
68
64
 
69
65
  statusMessage(`Opened ${file.name}`);
@@ -76,93 +72,50 @@ export async function openFile({ S: _S, commit, renderToolbar: _renderToolbar })
76
72
  }
77
73
 
78
74
  /**
79
- * Parse a markdown string into a Jx state object (pure — no side effects).
75
+ * Parse a markdown string into document + frontmatter (pure — no side effects).
80
76
  *
81
- * All markdown goes through `transpileJxMarkdown()`. Content documents (no hyphenated `tagName`)
82
- * are wrapped in a `{ tagName: "div", $id: "content" }` root to match the studio's content
83
- * contract.
84
- *
85
- * @param {any} source Markdown text
86
- * @param {any} fileHandle File handle (or null)
87
- * @returns {Promise<any>} A new state object ready for commit()
77
+ * @param {string} source Markdown text
78
+ * @returns {Promise<{ document: any; frontmatter: Record<string, any> }>}
88
79
  */
89
- export async function loadMarkdown(source, fileHandle) {
80
+ export async function loadMarkdown(source) {
90
81
  const { transpileJxMarkdown } = await import("@jxsuite/parser/transpile");
91
82
  const doc = /** @type {any} */ (transpileJxMarkdown(source));
92
83
 
93
84
  const isComponent = doc.tagName && String(doc.tagName).includes("-");
94
85
 
95
86
  if (isComponent) {
96
- const newState = /** @type {any} */ (createState(doc));
97
- newState.sourceFormat = "md";
98
- newState.rawMarkdown = source;
99
- newState.fileHandle = fileHandle;
100
- newState.dirty = false;
101
- return newState;
87
+ return { document: doc, frontmatter: {} };
102
88
  }
103
89
 
104
90
  // Content markdown — children form the root-level document body
105
- const contentDoc = {
106
- children: doc.children ?? [],
107
- };
91
+ const contentDoc = { children: doc.children ?? [] };
108
92
 
109
- // Extract frontmatter keys (everything except children) as content metadata
110
93
  /** @type {Record<string, any>} */
111
94
  const frontmatter = {};
112
95
  for (const [key, value] of Object.entries(doc)) {
113
96
  if (key !== "children") frontmatter[key] = value;
114
97
  }
115
98
 
116
- const newState = /** @type {any} */ (createState(contentDoc));
117
- newState.sourceFormat = "md";
118
- newState.mode = "content";
119
- newState.content = { frontmatter };
120
- newState.rawMarkdown = source;
121
- newState.fileHandle = fileHandle;
122
- newState.dirty = false;
123
- return newState;
124
- }
125
-
126
- /**
127
- * Load companion JS file metadata into state.
128
- *
129
- * @param {any} handle
130
- * @param {any} state State object to mutate in-place
131
- */
132
- async function loadCompanionJS(handle, state) {
133
- try {
134
- if (handle.getParent) {
135
- // Not yet available in any browser; skip for now
136
- }
137
- if (state.document.$handlers) {
138
- state.handlersSource = `// Companion file: ${state.document.$handlers}\n// (Read-only in builder — edit the JS file directly)`;
139
- }
140
- } catch {}
99
+ return { document: contentDoc, frontmatter };
141
100
  }
142
101
 
143
- /**
144
- * Save the current document back to its source location.
145
- *
146
- * @param {{ S: any; commit: (s: any) => void; renderToolbar: () => void }} ctx
147
- */
148
- export async function saveFile({ S, commit, renderToolbar }) {
102
+ /** Save the current document back to its source location. */
103
+ export async function saveFile() {
104
+ const tab = activeTab.value;
105
+ if (!tab) return;
149
106
  try {
150
- const output = serializeDocument(S);
107
+ const output = serializeDocument(tab);
151
108
 
152
- if (S.documentPath) {
153
- // Project file — save via platform
109
+ if (tab.documentPath) {
154
110
  const platform = getPlatform();
155
- await platform.writeFile(S.documentPath, output);
156
- commit({ ...S, dirty: false });
157
- renderToolbar();
111
+ await platform.writeFile(tab.documentPath, output);
112
+ tab.doc.dirty = false;
158
113
  statusMessage("Saved");
159
- } else if (S.fileHandle && "createWritable" in S.fileHandle) {
160
- // Standalone file opened via FS Access API
161
- const writable = await S.fileHandle.createWritable();
114
+ } else if (tab.fileHandle && "createWritable" in tab.fileHandle) {
115
+ const writable = await /** @type {any} */ (tab.fileHandle).createWritable();
162
116
  await writable.write(output);
163
117
  await writable.close();
164
- commit({ ...S, dirty: false });
165
- renderToolbar();
118
+ tab.doc.dirty = false;
166
119
  statusMessage("Saved");
167
120
  } else {
168
121
  statusMessage("No save target — use Export");
@@ -172,22 +125,20 @@ export async function saveFile({ S, commit, renderToolbar }) {
172
125
  }
173
126
  }
174
127
 
175
- /**
176
- * Export the current document to a new location (Save As / download).
177
- *
178
- * @param {{ S: any; commit: (s: any) => void; renderToolbar: () => void }} ctx
179
- */
180
- export async function exportFile({ S, commit, renderToolbar }) {
128
+ /** Export the current document to a new location (Save As / download). */
129
+ export async function exportFile() {
130
+ const tab = activeTab.value;
131
+ if (!tab) return;
181
132
  try {
182
- const isContent = S.mode === "content";
183
- const output = serializeDocument(S);
133
+ const isContent = tab.doc.mode === "content";
134
+ const output = serializeDocument(tab);
184
135
  const mimeType = isContent ? "text/markdown" : "application/json";
185
136
  const ext = isContent ? ".md" : ".json";
186
137
  const description = isContent ? "Markdown Content" : "Jx Component";
187
138
 
188
139
  if ("showSaveFilePicker" in window) {
189
- const suggestedName = S.documentPath
190
- ? S.documentPath.split("/").pop()
140
+ const suggestedName = tab.documentPath
141
+ ? tab.documentPath.split("/").pop()
191
142
  : isContent
192
143
  ? "content.md"
193
144
  : "component.json";
@@ -198,8 +149,7 @@ export async function exportFile({ S, commit, renderToolbar }) {
198
149
  const writable = await handle.createWritable();
199
150
  await writable.write(output);
200
151
  await writable.close();
201
- commit({ ...S, dirty: false });
202
- renderToolbar();
152
+ tab.doc.dirty = false;
203
153
  statusMessage(`Exported as ${handle.name}`);
204
154
  } else {
205
155
  // Fallback: download
@@ -210,8 +160,7 @@ export async function exportFile({ S, commit, renderToolbar }) {
210
160
  a.download = isContent ? "content.md" : "component.json";
211
161
  a.click();
212
162
  URL.revokeObjectURL(url);
213
- commit({ ...S, dirty: false });
214
- renderToolbar();
163
+ tab.doc.dirty = false;
215
164
  statusMessage("Downloaded");
216
165
  }
217
166
  } catch (/** @type {any} */ e) {
@@ -222,22 +171,22 @@ export async function exportFile({ S, commit, renderToolbar }) {
222
171
  /**
223
172
  * Serialize the current document to its output format (JSON or Markdown).
224
173
  *
225
- * @param {any} S
174
+ * @param {import("../tabs/tab.js").Tab} tab
226
175
  * @returns {string}
227
176
  */
228
- function serializeDocument(S) {
229
- if (S.sourceFormat === "md") {
230
- return jxDocToMd(S.document);
177
+ function serializeDocument(tab) {
178
+ if (tab.doc.sourceFormat === "md") {
179
+ return jxDocToMd(tab.doc.document);
231
180
  }
232
- if (S.mode === "content") {
233
- const mdast = jxToMd(S.document);
181
+ if (tab.doc.mode === "content") {
182
+ const mdast = jxToMd(tab.doc.document);
234
183
  const md = unified()
235
184
  .use(remarkDirective)
236
185
  .use(remarkStringify, { bullet: "-", emphasis: "*", strong: "*" })
237
186
  .stringify(mdast);
238
- const fm = S.content?.frontmatter;
187
+ const fm = tab.doc.content?.frontmatter;
239
188
  const hasFrontmatter = fm && Object.keys(fm).length > 0;
240
189
  return hasFrontmatter ? `---\n${stringifyYaml(fm).trim()}\n---\n\n${md}` : md;
241
190
  }
242
- return JSON.stringify(S.document, null, 2);
191
+ return JSON.stringify(tab.doc.document, null, 2);
243
192
  }
@@ -17,6 +17,7 @@ import { statusMessage } from "../panels/statusbar.js";
17
17
  import { loadComponentRegistry } from "./components.js";
18
18
  import { workspace, openTab, activateTab } from "../workspace/workspace.js";
19
19
  import { loadMarkdown } from "./file-ops.js";
20
+ import { view } from "../view.js";
20
21
 
21
22
  // ─── File icon map ────────────────────────────────────────────────────────────
22
23
 
@@ -79,13 +80,11 @@ export async function loadProject() {
79
80
  * Open a project via the platform adapter.
80
81
  *
81
82
  * @param {{
82
- * S: any;
83
- * commit: (s: any) => void;
84
83
  * renderActivityBar: () => void;
85
84
  * renderLeftPanel: () => void;
86
85
  * }} ctx
87
86
  */
88
- export async function openProject({ S, commit, renderActivityBar, renderLeftPanel }) {
87
+ export async function openProject({ renderActivityBar, renderLeftPanel }) {
89
88
  try {
90
89
  const platform = getPlatform();
91
90
  const result = await platform.openProject();
@@ -129,7 +128,7 @@ export async function openProject({ S, commit, renderActivityBar, renderLeftPane
129
128
  }
130
129
  projectState.projectDirs = foundDirs;
131
130
 
132
- commit({ ...S, ui: { ...S.ui, leftTab: "files" } });
131
+ view.leftTab = "files";
133
132
  renderActivityBar();
134
133
  renderLeftPanel();
135
134
  statusMessage(`Opened project: ${projectState.name}`);
@@ -616,15 +615,21 @@ export async function openFileInTab(path) {
616
615
 
617
616
  let document, frontmatter;
618
617
  if (path.endsWith(".md")) {
619
- const state = await loadMarkdown(content, null);
620
- document = state.document;
621
- frontmatter = state.content?.frontmatter;
618
+ const result = await loadMarkdown(content);
619
+ document = result.document;
620
+ frontmatter = result.frontmatter;
622
621
  } else {
623
622
  document = JSON.parse(content);
624
623
  }
625
624
 
626
625
  const id = path;
627
- openTab({ id, documentPath: path, document, frontmatter });
626
+ openTab({
627
+ id,
628
+ documentPath: path,
629
+ document,
630
+ frontmatter,
631
+ sourceFormat: path.endsWith(".md") ? "md" : null,
632
+ });
628
633
  projectState.selectedPath = path;
629
634
  statusMessage(`Opened ${path.split("/").pop()}`);
630
635
  } catch (/** @type {any} */ e) {
@@ -1,9 +1,10 @@
1
1
  /** Activity bar — tab icons for switching left panel views. */
2
2
 
3
3
  import { html, render as litRender, nothing } from "lit-html";
4
- import { activityBar } from "../store.js";
4
+ import { activityBar, renderOnly } from "../store.js";
5
5
  import { effect, effectScope } from "../reactivity.js";
6
6
  import { activeTab } from "../workspace/workspace.js";
7
+ import { view } from "../view.js";
7
8
 
8
9
  /** @type {import("@vue/reactivity").EffectScope | null} */
9
10
  let _scope = null;
@@ -14,7 +15,6 @@ export function mount() {
14
15
  effect(() => {
15
16
  const tab = activeTab.value;
16
17
  if (!tab) return;
17
- void tab.session.ui.leftTab;
18
18
  renderActivityBar();
19
19
  });
20
20
  });
@@ -83,7 +83,7 @@ export function tabIcon(tag, size) {
83
83
  export function renderActivityBar() {
84
84
  const tab = activeTab.value;
85
85
  if (!tab) return;
86
- const leftTab = tab.session.ui.leftTab;
86
+ const leftTab = view.leftTab;
87
87
  const tabs = [
88
88
  { value: "files", icon: "sp-icon-folder", label: "Files" },
89
89
  { value: "layers", icon: "sp-icon-layers", label: "Layers" },
@@ -100,7 +100,9 @@ export function renderActivityBar() {
100
100
  direction="vertical"
101
101
  quiet
102
102
  @change=${(/** @type {any} */ e) => {
103
- activeTab.value.session.ui.leftTab = e.target.selected;
103
+ view.leftTab = e.target.selected;
104
+ renderOnly("leftPanel");
105
+ renderActivityBar();
104
106
  }}
105
107
  >
106
108
  ${tabs.map(
@@ -8,14 +8,8 @@ import {
8
8
  monitorForElements,
9
9
  } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
10
10
 
11
- import {
12
- getState,
13
- elToPath,
14
- canvasPanels,
15
- getNodeAtPath,
16
- VOID_ELEMENTS,
17
- isAncestor,
18
- } from "../store.js";
11
+ import { elToPath, canvasPanels, getNodeAtPath, VOID_ELEMENTS, isAncestor } from "../store.js";
12
+ import { activeTab } from "../workspace/workspace.js";
19
13
  import { view } from "../view.js";
20
14
  import { applyDropInstruction } from "../panels/dnd.js";
21
15
  import { effectiveZoom } from "../canvas/canvas-helpers.js";
@@ -72,12 +66,12 @@ export function registerPanelDnD(panel) {
72
66
  });
73
67
  view.canvasDndCleanups.push(monitorCleanup);
74
68
 
75
- const S = getState();
69
+ const document = activeTab.value?.doc.document;
76
70
  for (const el of allEls) {
77
71
  const elPath = elToPath.get(el);
78
72
  if (!elPath) continue;
79
73
 
80
- const node = getNodeAtPath(S.document, elPath);
74
+ const node = getNodeAtPath(document, elPath);
81
75
  const tag = (node?.tagName || "div").toLowerCase();
82
76
  const hasElementChildren = node?.children?.some(
83
77
  (/** @type {unknown} */ c) => c != null && typeof c === "object",
package/src/panels/dnd.js CHANGED
@@ -15,12 +15,12 @@ import {
15
15
  } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
16
16
 
17
17
  import {
18
- getState,
19
18
  leftPanel,
20
19
  getNodeAtPath,
21
20
  parentElementPath,
22
21
  childIndex,
23
22
  isAncestor,
23
+ renderOnly,
24
24
  } from "../store.js";
25
25
  import { transact, transactDoc, mutateMoveNode, mutateInsertNode } from "../tabs/transact.js";
26
26
  import { activeTab } from "../workspace/workspace.js";
@@ -42,6 +42,7 @@ export function registerLayersDnD() {
42
42
  .map((/** @type {any} */ s) => (/^\d+$/.test(s) ? parseInt(s) : s));
43
43
  const rowDepth = parseInt(/** @type {string} */ (row.dataset.dndDepth)) || 0;
44
44
  const isVoid = row.hasAttribute("data-dnd-void");
45
+ const isExpanded = row.hasAttribute("data-dnd-expanded");
45
46
 
46
47
  const cleanup = combine(
47
48
  draggable({
@@ -59,9 +60,15 @@ export function registerLayersDnD() {
59
60
  onDragStart() {
60
61
  row.classList.add("dragging");
61
62
  view.layerDragSourceHeight = row.offsetHeight;
63
+ if (isExpanded) {
64
+ hideDescendantRows(row, container);
65
+ }
62
66
  },
63
67
  onDrop() {
64
68
  row.classList.remove("dragging");
69
+ if (isExpanded) {
70
+ renderOnly("leftPanel");
71
+ }
65
72
  },
66
73
  }),
67
74
  dropTargetForElements({
@@ -79,6 +86,7 @@ export function registerLayersDnD() {
79
86
  element,
80
87
  currentLevel: rowDepth,
81
88
  indentPerLevel: 16,
89
+ mode: isExpanded ? "expanded" : "standard",
82
90
  block: isVoid ? ["make-child"] : [],
83
91
  }),
84
92
  );
@@ -111,7 +119,21 @@ export function registerLayersDnD() {
111
119
  if (!instruction || instruction.type === "instruction-blocked") return;
112
120
  const srcData = source.data;
113
121
  const targetPath = target.data.path;
122
+
123
+ // If the source had children, persist collapse at the new location
124
+ const srcRow = srcData.type === "tree-node" && source.element;
125
+ const wasExpanded = srcRow && srcRow.hasAttribute("data-dnd-expanded");
126
+
114
127
  applyDropInstruction(instruction, srcData, targetPath);
128
+
129
+ if (wasExpanded) {
130
+ const tab = activeTab.value;
131
+ const newPath = tab?.session.selection;
132
+ if (newPath) {
133
+ const collapsed = view._layersCollapsed || (view._layersCollapsed = new Set());
134
+ collapsed.add(newPath.join("/"));
135
+ }
136
+ }
115
137
  },
116
138
  });
117
139
  view.dndCleanups.push(monitorCleanup);
@@ -190,6 +212,22 @@ export function registerElementsDnD() {
190
212
  });
191
213
  }
192
214
 
215
+ /**
216
+ * Hide descendant rows of the dragged item so it appears collapsed during drag.
217
+ *
218
+ * @param {any} parentRow
219
+ * @param {any} container
220
+ */
221
+ function hideDescendantRows(parentRow, container) {
222
+ const prefix = parentRow.dataset.path + "/";
223
+ const rows = container.querySelectorAll(".layers-tree .layer-row");
224
+ for (const r of rows) {
225
+ if (/** @type {any} */ (r).dataset.path?.startsWith(prefix)) {
226
+ /** @type {any} */ (r).style.display = "none";
227
+ }
228
+ }
229
+ }
230
+
193
231
  /**
194
232
  * @param {any} rowEl
195
233
  * @param {any} data
@@ -251,7 +289,7 @@ export function clearLayerDropGap(container) {
251
289
  * @param {any} targetPath
252
290
  */
253
291
  export function applyDropInstruction(instruction, srcData, targetPath) {
254
- const S = getState();
292
+ const document = activeTab.value?.doc.document;
255
293
  if (srcData.type === "tree-node") {
256
294
  const fromPath = srcData.path;
257
295
  const targetParent = /** @type {any} */ (parentElementPath(targetPath));
@@ -267,7 +305,7 @@ export function applyDropInstruction(instruction, srcData, targetPath) {
267
305
  );
268
306
  break;
269
307
  case "make-child": {
270
- const target = getNodeAtPath(S.document, targetPath);
308
+ const target = getNodeAtPath(document, targetPath);
271
309
  const len = target?.children?.length || 0;
272
310
  transactDoc(activeTab.value, (t) => mutateMoveNode(t, fromPath, targetPath, len));
273
311
  break;
@@ -289,7 +327,7 @@ export function applyDropInstruction(instruction, srcData, targetPath) {
289
327
  );
290
328
  break;
291
329
  case "make-child": {
292
- const target = getNodeAtPath(S.document, targetPath);
330
+ const target = getNodeAtPath(document, targetPath);
293
331
  const len = target?.children?.length || 0;
294
332
  transactDoc(activeTab.value, (t) =>
295
333
  mutateInsertNode(t, targetPath, len, structuredClone(srcData.fragment)),
@@ -303,8 +341,8 @@ export function applyDropInstruction(instruction, srcData, targetPath) {
303
341
  if (tag && tag.includes("-")) {
304
342
  const comp = componentRegistry.find((/** @type {any} */ c) => c.tagName === tag);
305
343
  if (comp) {
306
- const currentS = getState();
307
- const elements = currentS.document.$elements || [];
344
+ const tab = activeTab.value;
345
+ const elements = tab?.doc.document?.$elements || [];
308
346
  if (comp.source === "npm") {
309
347
  const specifier = comp.modulePath ? `${comp.package}/${comp.modulePath}` : comp.package;
310
348
  const alreadyImported = elements.some(
@@ -323,7 +361,7 @@ export function applyDropInstruction(instruction, srcData, targetPath) {
323
361
  (e.$ref === `./${comp.path}` || e.$ref.endsWith(comp.path.split("/").pop())),
324
362
  );
325
363
  if (!alreadyImported) {
326
- const relPath = computeRelativePath(currentS.documentPath, comp.path);
364
+ const relPath = computeRelativePath(tab?.documentPath, comp.path);
327
365
  transact(activeTab.value, (/** @type {any} */ doc) => {
328
366
  if (!doc.$elements) doc.$elements = [];
329
367
  doc.$elements.push({ $ref: relPath });
@@ -7,26 +7,25 @@ import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
7
7
  import { html, render as litRender, nothing } from "lit-html";
8
8
  import { ref } from "lit-html/directives/ref.js";
9
9
 
10
- import { getState, renderOnly, canvasWrap, canvasPanels, getNodeAtPath } from "../store.js";
10
+ import { renderOnly, canvasWrap, canvasPanels, getNodeAtPath } from "../store.js";
11
11
  import { activeTab } from "../workspace/workspace.js";
12
12
  import { transactDoc, mutateUpdateDef, mutateUpdateProperty } from "../tabs/transact.js";
13
13
  import { view } from "../view.js";
14
14
  import { codeService, setLintMarkers, getFunctionArgs } from "../services/code-services.js";
15
15
 
16
16
  function getFunctionBody(/** @type {any} */ editing) {
17
- const S = getState();
17
+ const document = activeTab.value?.doc.document;
18
18
  if (editing.type === "def") {
19
- return S.document.state?.[editing.defName]?.body || "";
19
+ return document?.state?.[editing.defName]?.body || "";
20
20
  } else if (editing.type === "event") {
21
- const node = getNodeAtPath(S.document, editing.path);
21
+ const node = getNodeAtPath(document, editing.path);
22
22
  return node?.[editing.eventKey]?.body || "";
23
23
  }
24
24
  return "";
25
25
  }
26
26
 
27
27
  export function renderFunctionEditor() {
28
- const S = getState();
29
- const editing = S.ui.editingFunction;
28
+ const editing = activeTab.value?.session.ui.editingFunction;
30
29
 
31
30
  // If editor already exists and matches current target, just sync value
32
31
  if (view.functionEditor && view.functionEditor._editingTarget === JSON.stringify(editing)) {
@@ -76,7 +75,7 @@ export function renderFunctionEditor() {
76
75
  );
77
76
 
78
77
  const body = getFunctionBody(editing);
79
- const args = getFunctionArgs(editing, S);
78
+ const args = getFunctionArgs(editing, activeTab.value?.doc.document);
80
79
 
81
80
  view.functionEditor = monaco.editor.create(/** @type {any} */ (editorContainer), {
82
81
  value: body,
@@ -120,14 +119,14 @@ export function renderFunctionEditor() {
120
119
  clearTimeout(syncDebounce);
121
120
  syncDebounce = setTimeout(() => {
122
121
  const newBody = view.functionEditor.getValue();
123
- if (editing.type === "def") {
124
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, editing.defName, { body: newBody }));
125
- } else if (editing.type === "event") {
126
- const S = getState();
127
- const node = getNodeAtPath(S.document, editing.path);
128
- const current = node?.[editing.eventKey] || {};
122
+ const ed = /** @type {any} */ (editing);
123
+ if (ed.type === "def") {
124
+ transactDoc(activeTab.value, (t) => mutateUpdateDef(t, ed.defName, { body: newBody }));
125
+ } else if (ed.type === "event") {
126
+ const node = getNodeAtPath(activeTab.value?.doc.document, ed.path);
127
+ const current = node?.[ed.eventKey] || {};
129
128
  transactDoc(activeTab.value, (t) =>
130
- mutateUpdateProperty(t, editing.path, editing.eventKey, {
129
+ mutateUpdateProperty(t, ed.path, ed.eventKey, {
131
130
  ...current,
132
131
  $prototype: "Function",
133
132
  body: newBody,
@@ -157,8 +156,7 @@ export function registerFunctionCompletions() {
157
156
  monaco.languages.registerCompletionItemProvider("javascript", {
158
157
  triggerCharacters: ["."],
159
158
  provideCompletionItems(model, position) {
160
- const S = getState();
161
- const defs = S?.document?.state || {};
159
+ const defs = activeTab.value?.doc.document?.state || {};
162
160
  const word = model.getWordUntilPosition(position);
163
161
  const range = {
164
162
  startLineNumber: position.lineNumber,