beast-devtools 0.0.1
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/.claude/launch.json +11 -0
- package/CHANGELOG.md +37 -0
- package/README.md +87 -0
- package/devtools/CHANGELOG.md +26 -0
- package/devtools/LICENSE +15 -0
- package/devtools/README.md +164 -0
- package/devtools/client/BeastDevtools.btsx +185 -0
- package/devtools/client/CodeView.btsx +61 -0
- package/devtools/client/ComponentsPanel.btsx +212 -0
- package/devtools/client/FileList.btsx +35 -0
- package/devtools/client/InspectorPanel.btsx +131 -0
- package/devtools/client/RefactorPanel.btsx +304 -0
- package/devtools/client/api.ts +69 -0
- package/devtools/client/compile.test.ts +22 -0
- package/devtools/client/devtools.css +1279 -0
- package/devtools/client/highlight.ts +210 -0
- package/devtools/client/mount.ts +16 -0
- package/devtools/client/runtime.ts +168 -0
- package/devtools/client/util.ts +136 -0
- package/devtools/package.json +73 -0
- package/devtools/server/analyze.test.ts +148 -0
- package/devtools/server/analyze.ts +737 -0
- package/devtools/server/diff.ts +82 -0
- package/devtools/server/line-map.ts +63 -0
- package/devtools/server/octane-bundler.d.ts +17 -0
- package/devtools/server/project.ts +369 -0
- package/devtools/server/refactor.test.ts +224 -0
- package/devtools/server/refactor.ts +224 -0
- package/devtools/server/source-scan.ts +377 -0
- package/devtools/shared/types.ts +208 -0
- package/devtools/test/fixtures/App.btsx +193 -0
- package/devtools/tsconfig.build.json +17 -0
- package/devtools/vite.ts +159 -0
- package/favicon.ico +0 -0
- package/index.html +14 -0
- package/package.json +34 -0
- package/public/beast.svg +1 -0
- package/src/App.btsx +144 -0
- package/src/AppHeader.btsx +24 -0
- package/src/LeftArticle.btsx +22 -0
- package/src/RightArticle.btsx +19 -0
- package/src/env.d.ts +8 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.ts +8 -0
- package/src/style.css +44 -0
- package/tsconfig.json +39 -0
- package/vite.config.ts +19 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { useMemo, useState, useSyncExternalStore } from 'octane'
|
|
2
|
+
import type { ComponentLocation } from '../shared/types.ts'
|
|
3
|
+
import { openInEditor } from './api.ts'
|
|
4
|
+
import { getRuntimeSnapshot, selectRuntimeNode, subscribeRuntime, type HookCell, type RuntimeNode } from './runtime.ts'
|
|
5
|
+
import { cx, isExpandable, preview, pretty, valueTone } from './util.ts'
|
|
6
|
+
|
|
7
|
+
module
|
|
8
|
+
interface ComponentsPanelProps {
|
|
9
|
+
components: readonly ComponentLocation[]
|
|
10
|
+
showControlFlow: boolean
|
|
11
|
+
onShowControlFlowChange: (value: boolean) => void
|
|
12
|
+
onViewSource: (path: string, line: number) => void
|
|
13
|
+
onAnalyze: (path: string) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface TreeRow {
|
|
17
|
+
node: RuntimeNode
|
|
18
|
+
depth: number
|
|
19
|
+
expandable: boolean
|
|
20
|
+
open: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ValueRow {
|
|
24
|
+
key: string
|
|
25
|
+
label: string
|
|
26
|
+
kind: string
|
|
27
|
+
value: unknown
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const KIND_LABEL: Record<HookCell['kind'], string> = {
|
|
31
|
+
state: 'state',
|
|
32
|
+
reducer: 'reducer',
|
|
33
|
+
ref: 'ref',
|
|
34
|
+
'memo-or-callback': 'memo',
|
|
35
|
+
other: 'hook',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Setup hooks whose runtime cell `inspect()` reports, keyed to the cell kind. */
|
|
39
|
+
const VALUE_HOOKS: Record<string, HookCell['kind']> = {
|
|
40
|
+
useState: 'state',
|
|
41
|
+
useLinkedState: 'state',
|
|
42
|
+
useReducer: 'reducer',
|
|
43
|
+
useRef: 'ref',
|
|
44
|
+
useMemo: 'memo-or-callback',
|
|
45
|
+
useCallback: 'memo-or-callback',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function childrenOf(node: RuntimeNode, showControlFlow: boolean): RuntimeNode[] {
|
|
49
|
+
if (showControlFlow) return node.children
|
|
50
|
+
return node.children.flatMap((child) => (child.controlFlow ? childrenOf(child, false) : [child]))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function matches(node: RuntimeNode, query: string, showControlFlow: boolean): boolean {
|
|
54
|
+
return node.label.toLowerCase().includes(query) || childrenOf(node, showControlFlow).some((child) => matches(child, query, showControlFlow))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function treeRows(roots: readonly RuntimeNode[], collapsed: ReadonlySet<number>, showControlFlow: boolean, query: string): TreeRow[] {
|
|
58
|
+
const rows: TreeRow[] = []
|
|
59
|
+
const visit = (node: RuntimeNode, depth: number) => {
|
|
60
|
+
if (query !== '' && !matches(node, query, showControlFlow)) return
|
|
61
|
+
const children = childrenOf(node, showControlFlow)
|
|
62
|
+
const open = query !== '' || !collapsed.has(node.id)
|
|
63
|
+
rows.push({ node, depth, expandable: children.length > 0, open })
|
|
64
|
+
if (open) children.forEach((child) => visit(child, depth + 1))
|
|
65
|
+
}
|
|
66
|
+
roots.forEach((root) => visit(root, 0))
|
|
67
|
+
return rows
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Name runtime hook cells after the setup declarations that created them.
|
|
72
|
+
* Labels are applied only when the value hooks line up one-to-one by kind,
|
|
73
|
+
* so a mismatch (custom hooks, conditional scopes) falls back to indices.
|
|
74
|
+
*/
|
|
75
|
+
function hookRows(cells: readonly HookCell[], location: ComponentLocation | undefined): ValueRow[] {
|
|
76
|
+
const bindings = (location?.hooks ?? []).filter((binding) => binding.hook in VALUE_HOOKS)
|
|
77
|
+
const valueCells = cells.filter((cell) => cell.kind !== 'other')
|
|
78
|
+
const aligned = bindings.length === valueCells.length && bindings.every((binding, index) => VALUE_HOOKS[binding.hook] === valueCells[index]!.kind)
|
|
79
|
+
let next = 0
|
|
80
|
+
return cells.map((cell, index) => {
|
|
81
|
+
const binding = aligned && cell.kind !== 'other' ? bindings[next++] : undefined
|
|
82
|
+
return {
|
|
83
|
+
key: `hook-${index}`,
|
|
84
|
+
label: binding?.names[0] ?? `#${index}`,
|
|
85
|
+
kind: binding?.hook ?? KIND_LABEL[cell.kind],
|
|
86
|
+
value: cell.value,
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
props { components, showControlFlow, onShowControlFlowChange, onViewSource, onAnalyze }: ComponentsPanelProps
|
|
92
|
+
setup
|
|
93
|
+
const runtime = useSyncExternalStore(subscribeRuntime, getRuntimeSnapshot, getRuntimeSnapshot);
|
|
94
|
+
const [query, setQuery] = useState('');
|
|
95
|
+
const [collapsed, setCollapsed] = useState<ReadonlySet<number>>(new Set());
|
|
96
|
+
const [expanded, setExpanded] = useState<ReadonlySet<string>>(new Set());
|
|
97
|
+
const rows = useMemo(() => treeRows(runtime.roots, collapsed, showControlFlow, query.trim().toLowerCase()), [runtime.roots, collapsed, showControlFlow, query]);
|
|
98
|
+
const detail = runtime.detail;
|
|
99
|
+
const location = detail === null ? undefined : components.find((component) => component.name === detail.name);
|
|
100
|
+
const hooks = detail === null ? [] : hookRows(detail.hooks, location);
|
|
101
|
+
const context = detail === null ? [] : detail.context.map((entry, index) => ({ key: `context-${index}`, label: entry.name, kind: 'context', value: entry.value }));
|
|
102
|
+
const values = [...hooks, ...context];
|
|
103
|
+
const toggleNode = (id: number) => setCollapsed((current) => {
|
|
104
|
+
const next = new Set(current);
|
|
105
|
+
if (next.has(id)) next.delete(id);
|
|
106
|
+
else next.add(id);
|
|
107
|
+
return next;
|
|
108
|
+
});
|
|
109
|
+
const toggleValue = (key: string) => setExpanded((current) => {
|
|
110
|
+
const next = new Set(current);
|
|
111
|
+
if (next.has(key)) next.delete(key);
|
|
112
|
+
else next.add(key);
|
|
113
|
+
return next;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if runtime.status === 'unavailable'
|
|
117
|
+
.bdt-empty
|
|
118
|
+
.bdt-empty-card
|
|
119
|
+
p.bdt-empty-title Runtime inspection is off
|
|
120
|
+
p Octane exposes its component tree only when profiling metadata is compiled in. Enable it for dev builds in vite.config.ts:
|
|
121
|
+
pre.bdt-pre #{"beastOctane({ octane: { profile: 'auto' } })"}
|
|
122
|
+
p.bdt-dim BTSX → TSRX and Refactor keep working without it.
|
|
123
|
+
elseif runtime.status === 'connecting'
|
|
124
|
+
.bdt-empty
|
|
125
|
+
p Connecting to the Octane runtime…
|
|
126
|
+
else
|
|
127
|
+
fragment
|
|
128
|
+
.bdt-pane
|
|
129
|
+
.bdt-toolbar
|
|
130
|
+
input.bdt-input(
|
|
131
|
+
~ type="search"
|
|
132
|
+
~ placeholder="Filter components"
|
|
133
|
+
~ aria-label="Filter components"
|
|
134
|
+
~ value={query}
|
|
135
|
+
~ onInput={(event) => setQuery(event.currentTarget.value)}
|
|
136
|
+
~ )
|
|
137
|
+
label.bdt-check
|
|
138
|
+
input(type="checkbox" checked={showControlFlow} onChange={(event) => onShowControlFlowChange(event.currentTarget.checked)})
|
|
139
|
+
| Control flow
|
|
140
|
+
.bdt-spacer
|
|
141
|
+
span.bdt-dim #{runtime.componentCount} component#{runtime.componentCount === 1 ? '' : 's'}
|
|
142
|
+
.bdt-scroll
|
|
143
|
+
if rows.length === 0
|
|
144
|
+
p.bdt-empty #{query === '' ? 'No Octane roots are mounted.' : 'Nothing matches that filter.'}
|
|
145
|
+
else
|
|
146
|
+
div.bdt-tree(role="tree" aria-label="Component tree")
|
|
147
|
+
each row in rows key row.node.id
|
|
148
|
+
button(
|
|
149
|
+
~ type="button"
|
|
150
|
+
~ role="treeitem"
|
|
151
|
+
~ aria-selected={runtime.selectedId === row.node.id}
|
|
152
|
+
~ aria-expanded={row.expandable ? row.open : undefined}
|
|
153
|
+
~ className={cx('bdt-tree-row', runtime.selectedId === row.node.id && 'is-selected')}
|
|
154
|
+
~ style={{ paddingLeft: `${6 + row.depth * 14}px` }}
|
|
155
|
+
~ onClick={() => selectRuntimeNode(row.node.id)}
|
|
156
|
+
~ onDoubleClick={() => row.expandable && toggleNode(row.node.id)}
|
|
157
|
+
~ )
|
|
158
|
+
span(
|
|
159
|
+
~ className={cx('bdt-caret', row.open && 'is-open')}
|
|
160
|
+
~ onClick={(event) => { event.stopPropagation(); if (row.expandable) toggleNode(row.node.id); }}
|
|
161
|
+
~ )
|
|
162
|
+
| #{row.expandable ? '▸' : ''}
|
|
163
|
+
span(className={cx('bdt-tree-name', row.node.controlFlow && 'is-flow')}) #{row.node.label}
|
|
164
|
+
if row.node.kind === 'root' || row.node.kind === 'portal'
|
|
165
|
+
span.bdt-badge(style={{ marginLeft: '8px' }}) #{row.node.kind}
|
|
166
|
+
span.bdt-tree-id ##{row.node.id}
|
|
167
|
+
aside.bdt-detail(aria-label="Selected component")
|
|
168
|
+
if detail === null
|
|
169
|
+
.bdt-empty
|
|
170
|
+
p Select a component to inspect its state, context, and source.
|
|
171
|
+
else
|
|
172
|
+
fragment
|
|
173
|
+
.bdt-detail-head
|
|
174
|
+
p.bdt-detail-name
|
|
175
|
+
| #{detail.name}
|
|
176
|
+
span.bdt-badge ##{detail.id}
|
|
177
|
+
if location === undefined
|
|
178
|
+
p.bdt-dim No .btsx declaration found for this name.
|
|
179
|
+
else
|
|
180
|
+
fragment
|
|
181
|
+
button.bdt-link(type="button" title="Open in editor" onClick={() => openInEditor(location.absolutePath, location.line, location.column)}) #{location.path}:#{location.line}
|
|
182
|
+
.bdt-detail-actions
|
|
183
|
+
button.bdt-button(type="button" onClick={() => onViewSource(location.path, location.line)}) BTSX → TSRX
|
|
184
|
+
button.bdt-button(type="button" onClick={() => onAnalyze(location.path)}) Analyze file
|
|
185
|
+
button.bdt-button(type="button" onClick={() => openInEditor(location.absolutePath, location.line, location.column)}) Open in editor
|
|
186
|
+
.bdt-scroll
|
|
187
|
+
p.bdt-section-label State & hooks
|
|
188
|
+
if values.length === 0
|
|
189
|
+
p.bdt-dim(style={{ padding: '0 14px' }}) No hook state or context.
|
|
190
|
+
else
|
|
191
|
+
.bdt-props
|
|
192
|
+
each item in values key item.key
|
|
193
|
+
div.bdt-prop
|
|
194
|
+
span.bdt-prop-key
|
|
195
|
+
| #{item.label}
|
|
196
|
+
span.bdt-prop-kind #{item.kind}
|
|
197
|
+
if isExpandable(item.value)
|
|
198
|
+
span(
|
|
199
|
+
~ role="button"
|
|
200
|
+
~ tabIndex={0}
|
|
201
|
+
~ aria-expanded={expanded.has(item.key)}
|
|
202
|
+
~ className={cx('bdt-prop-value', 'is-toggle', valueTone(item.value))}
|
|
203
|
+
~ onClick={() => toggleValue(item.key)}
|
|
204
|
+
~ onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); toggleValue(item.key); } }}
|
|
205
|
+
~ )
|
|
206
|
+
| #{expanded.has(item.key) ? '▾ ' : '▸ '}#{preview(item.value)}
|
|
207
|
+
else
|
|
208
|
+
span(className={cx('bdt-prop-value', valueTone(item.value))}) #{preview(item.value)}
|
|
209
|
+
if expanded.has(item.key)
|
|
210
|
+
pre.bdt-pre #{pretty(item.value)}
|
|
211
|
+
p.bdt-section-label Effects
|
|
212
|
+
p.bdt-dim(style={{ padding: '0 14px 14px' }}) #{detail.effectCount} effect slot#{detail.effectCount === 1 ? '' : 's'}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { FileSummary } from '../shared/types.ts'
|
|
2
|
+
import { cx } from './util.ts'
|
|
3
|
+
|
|
4
|
+
module
|
|
5
|
+
interface FileListProps {
|
|
6
|
+
files: readonly FileSummary[]
|
|
7
|
+
active: string | null
|
|
8
|
+
/** Show refactor suggestion counts instead of line counts. */
|
|
9
|
+
metrics: boolean
|
|
10
|
+
onSelect: (path: string) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
props { files, active, metrics, onSelect }: FileListProps
|
|
14
|
+
|
|
15
|
+
nav.bdt-sidebar(aria-label="Beast source files")
|
|
16
|
+
p.bdt-section-label Files · #{files.length}
|
|
17
|
+
.bdt-scroll
|
|
18
|
+
each file in files key file.path
|
|
19
|
+
button(
|
|
20
|
+
~ type="button"
|
|
21
|
+
~ title={file.path}
|
|
22
|
+
~ aria-current={file.path === active ? 'true' : undefined}
|
|
23
|
+
~ className={cx('bdt-file', file.path === active && 'is-active')}
|
|
24
|
+
~ onClick={() => onSelect(file.path)}
|
|
25
|
+
~ )
|
|
26
|
+
span.bdt-file-name #{file.path}
|
|
27
|
+
if file.error !== null
|
|
28
|
+
span.bdt-badge.is-bad(title={file.error}) error
|
|
29
|
+
elseif !metrics
|
|
30
|
+
span.bdt-dim.bdt-mono #{file.lines}
|
|
31
|
+
elseif file.suggestions > 0
|
|
32
|
+
span.bdt-badge.is-warn(title="Refactor suggestions") #{file.suggestions}
|
|
33
|
+
else
|
|
34
|
+
span.bdt-badge.is-ok(title={`Max depth ${file.maxDepth}`}) ok
|
|
35
|
+
div.infinite-lines
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'octane'
|
|
2
|
+
import type { FileReport, FileSummary } from '../shared/types.ts'
|
|
3
|
+
import CodeView from './CodeView.btsx'
|
|
4
|
+
import FileList from './FileList.btsx'
|
|
5
|
+
import { openInEditor } from './api.ts'
|
|
6
|
+
import { scrollTarget, type ScrollTarget } from './util.ts'
|
|
7
|
+
|
|
8
|
+
module
|
|
9
|
+
interface InspectorPanelProps {
|
|
10
|
+
files: readonly FileSummary[]
|
|
11
|
+
file: string | null
|
|
12
|
+
report: FileReport | null
|
|
13
|
+
loadError: string | null
|
|
14
|
+
/** A BTSX line another panel asked to reveal. */
|
|
15
|
+
focus: ScrollTarget | null
|
|
16
|
+
onSelectFile: (path: string) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Link {
|
|
20
|
+
side: 'btsx' | 'tsrx'
|
|
21
|
+
line: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
props { files, file, report, loadError, focus, onSelectFile }: InspectorPanelProps
|
|
25
|
+
setup
|
|
26
|
+
const [hover, setHover] = useState<Link | null>(null);
|
|
27
|
+
const [pinned, setPinned] = useState<Link | null>(null);
|
|
28
|
+
const [btsxScroll, setBtsxScroll] = useState<ScrollTarget | null>(null);
|
|
29
|
+
const [tsrxScroll, setTsrxScroll] = useState<ScrollTarget | null>(null);
|
|
30
|
+
const appliedFocus = useRef<number | null>(null);
|
|
31
|
+
const shownFile = useRef(file);
|
|
32
|
+
const current = report !== null && report.path === file ? report : null;
|
|
33
|
+
const compiled = current?.compiled ?? null;
|
|
34
|
+
const btsxFor = (line: number) => (compiled?.ok ? (compiled.tsrxToBtsx[line - 1] ?? null) : null);
|
|
35
|
+
const tsrxFor = (line: number) => (compiled?.ok ? (compiled.btsxToTsrx[line - 1] ?? []) : []);
|
|
36
|
+
const link = hover ?? pinned;
|
|
37
|
+
const btsxLinked = link === null ? [] : link.side === 'btsx' ? [link.line] : [btsxFor(link.line)].filter((line): line is number => line !== null);
|
|
38
|
+
const tsrxLinked = link === null ? [] : link.side === 'tsrx' ? [link.line] : tsrxFor(link.line);
|
|
39
|
+
const editorLine = pinned === null ? 1 : pinned.side === 'btsx' ? pinned.line : (btsxFor(pinned.line) ?? 1);
|
|
40
|
+
const pinBtsx = (line: number) => {
|
|
41
|
+
setPinned({ side: 'btsx', line });
|
|
42
|
+
const target = tsrxFor(line)[0];
|
|
43
|
+
if (target !== undefined) setTsrxScroll(scrollTarget(target));
|
|
44
|
+
};
|
|
45
|
+
const pinTsrx = (line: number) => {
|
|
46
|
+
setPinned({ side: 'tsrx', line });
|
|
47
|
+
const target = btsxFor(line);
|
|
48
|
+
if (target !== null) setBtsxScroll(scrollTarget(target));
|
|
49
|
+
};
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (shownFile.current === file) return;
|
|
52
|
+
shownFile.current = file;
|
|
53
|
+
setPinned(null);
|
|
54
|
+
setHover(null);
|
|
55
|
+
}, [file]);
|
|
56
|
+
// Reveal a line requested by another panel once its file report arrives.
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (focus === null || current === null || appliedFocus.current === focus.nonce) return;
|
|
59
|
+
appliedFocus.current = focus.nonce;
|
|
60
|
+
setBtsxScroll(focus);
|
|
61
|
+
pinBtsx(focus.line);
|
|
62
|
+
}, [focus, current]);
|
|
63
|
+
|
|
64
|
+
FileList(files={files} active={file} metrics={false} onSelect={onSelectFile})
|
|
65
|
+
.bdt-pane
|
|
66
|
+
if loadError !== null && current === null
|
|
67
|
+
.bdt-empty
|
|
68
|
+
p Could not load #{file ?? 'the file'}: #{loadError}
|
|
69
|
+
elseif current === null
|
|
70
|
+
.bdt-empty
|
|
71
|
+
p #{files.length === 0 ? 'No .btsx files found under the configured include directories.' : 'Loading…'}
|
|
72
|
+
else
|
|
73
|
+
fragment
|
|
74
|
+
.bdt-toolbar
|
|
75
|
+
span.bdt-toolbar-title.bdt-mono #{current.path}
|
|
76
|
+
if compiled?.ok
|
|
77
|
+
span.bdt-badge.is-ok compiled
|
|
78
|
+
else
|
|
79
|
+
span.bdt-badge.is-bad compile error
|
|
80
|
+
.bdt-spacer
|
|
81
|
+
span.bdt-dim.bdt-hint Hover to link lines · click to pin
|
|
82
|
+
button.bdt-button(type="button" onClick={() => openInEditor(current.absolutePath, editorLine)}) Open in editor
|
|
83
|
+
if compiled !== null && !compiled.ok
|
|
84
|
+
.bdt-diagnostic
|
|
85
|
+
p
|
|
86
|
+
span.bdt-badge.is-bad #{compiled.error.code}
|
|
87
|
+
| #{' ' + compiled.error.message}
|
|
88
|
+
pre.bdt-pre #{compiled.error.formatted}
|
|
89
|
+
if compiled.error.hint !== undefined
|
|
90
|
+
p.bdt-muted Hint: #{compiled.error.hint}
|
|
91
|
+
elseif compiled !== null && compiled.diagnostics.length > 0
|
|
92
|
+
each diagnostic, index in compiled.diagnostics key index
|
|
93
|
+
div(className={diagnostic.severity === 'warning' ? 'bdt-diagnostic is-warning' : 'bdt-diagnostic'})
|
|
94
|
+
pre.bdt-pre #{diagnostic.formatted}
|
|
95
|
+
.bdt-split
|
|
96
|
+
section.bdt-pane(aria-label="BTSX source")
|
|
97
|
+
.bdt-pane-head
|
|
98
|
+
span BTSX
|
|
99
|
+
span.bdt-dim #{current.source.split('\n').length} lines
|
|
100
|
+
CodeView(
|
|
101
|
+
~ source={current.source}
|
|
102
|
+
~ language="btsx"
|
|
103
|
+
~ label="BTSX source"
|
|
104
|
+
~ linked={btsxLinked}
|
|
105
|
+
~ activeLine={pinned?.side === 'btsx' ? pinned.line : null}
|
|
106
|
+
~ errorLine={compiled !== null && !compiled.ok ? compiled.error.line : null}
|
|
107
|
+
~ scrollTo={btsxScroll}
|
|
108
|
+
~ onLineEnter={(line) => setHover({ side: 'btsx', line })}
|
|
109
|
+
~ onLineClick={pinBtsx}
|
|
110
|
+
~ onLeave={() => setHover(null)}
|
|
111
|
+
~ )
|
|
112
|
+
section.bdt-pane(aria-label="Generated TSRX")
|
|
113
|
+
.bdt-pane-head
|
|
114
|
+
span Generated TSRX
|
|
115
|
+
if compiled?.ok
|
|
116
|
+
span.bdt-dim #{compiled.tsrx.split('\n').length} lines
|
|
117
|
+
if compiled?.ok
|
|
118
|
+
CodeView(
|
|
119
|
+
~ source={compiled.tsrx}
|
|
120
|
+
~ language="tsrx"
|
|
121
|
+
~ label="Generated TSRX"
|
|
122
|
+
~ linked={tsrxLinked}
|
|
123
|
+
~ activeLine={pinned?.side === 'tsrx' ? pinned.line : null}
|
|
124
|
+
~ scrollTo={tsrxScroll}
|
|
125
|
+
~ onLineEnter={(line) => setHover({ side: 'tsrx', line })}
|
|
126
|
+
~ onLineClick={pinTsrx}
|
|
127
|
+
~ onLeave={() => setHover(null)}
|
|
128
|
+
~ )
|
|
129
|
+
else
|
|
130
|
+
.bdt-empty
|
|
131
|
+
p Fix the BTSX error to see generated TSRX.
|