@@ -19,16 +19,18 @@ interface ResultPanelProps {
19
19
diagnostics : Diagnostic [ ] ;
20
20
ast ?: string ;
21
21
astTree ?: ASTNode ;
22
+ tsAstTree ?: ASTNode ;
22
23
initialized ?: boolean ;
23
24
error ?: string ;
24
25
fixedCode ?: string ;
25
26
typeInfo ?: string ;
26
27
loading ?: boolean ;
27
28
onAstNodeSelect ?: ( start : number , end : number ) => void ;
28
29
selectedAstNodeRange ?: { start : number ; end : number } ;
30
+ onRequestTsAst ?: ( ) => void ;
29
31
}
30
32
31
- type TabType = 'lint' | 'fixed' | 'ast' | 'type' ;
33
+ type TabType = 'lint' | 'fixed' | 'ast' | 'ast_ts' | ' type';
32
34
33
35
interface ASTNode {
34
36
type : string ;
@@ -44,13 +46,15 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => {
44
46
diagnostics,
45
47
ast,
46
48
astTree,
49
+ tsAstTree,
47
50
error,
48
51
initialized,
49
52
fixedCode,
50
53
typeInfo,
51
54
loading,
52
55
onAstNodeSelect,
53
56
selectedAstNodeRange,
57
+ onRequestTsAst,
54
58
} = props ;
55
59
const [ activeTab , setActiveTab ] = useState < TabType > ( ( ) => {
56
60
if ( typeof window === 'undefined' ) return 'lint' ;
@@ -60,7 +64,13 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => {
60
64
const hashParams = new URLSearchParams ( window . location . hash . slice ( 1 ) ) ;
61
65
tab = hashParams . get ( 'tab' ) ;
62
66
}
63
- if ( tab === 'lint' || tab === 'ast' || tab === 'fixed' || tab === 'type' ) {
67
+ if (
68
+ tab === 'lint' ||
69
+ tab === 'ast' ||
70
+ tab === 'ast_ts' ||
71
+ tab === 'fixed' ||
72
+ tab === 'type'
73
+ ) {
64
74
return tab as TabType ;
65
75
}
66
76
return 'lint' ;
@@ -81,6 +91,13 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => {
81
91
}
82
92
} , [ activeTab ] ) ;
83
93
94
+ // Notify parent when TS AST tab is opened (including initial state)
95
+ useEffect ( ( ) => {
96
+ if ( activeTab === 'ast_ts' ) {
97
+ onRequestTsAst ?.( ) ;
98
+ }
99
+ } , [ activeTab ] ) ;
100
+
84
101
// Respond to browser navigation updating the tab
85
102
useEffect ( ( ) => {
86
103
if ( typeof window === 'undefined' ) return ;
@@ -95,6 +112,7 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => {
95
112
if (
96
113
tab === 'lint' ||
97
114
tab === 'ast' ||
115
+ tab === 'ast_ts' ||
98
116
tab === 'fixed' ||
99
117
tab === 'type'
100
118
) {
@@ -108,9 +126,12 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => {
108
126
return ( ) => window . removeEventListener ( 'popstate' , handler ) ;
109
127
} , [ ] ) ;
110
128
111
- // AST tree view state
129
+ // AST tree view state (tsgo)
112
130
const [ expanded , setExpanded ] = useState < Set < string > > ( ( ) => new Set ( ) ) ;
113
131
const [ selectedId , setSelectedId ] = useState < string | null > ( null ) ;
132
+ // AST tree view state (TypeScript)
133
+ const [ tsExpanded , setTsExpanded ] = useState < Set < string > > ( ( ) => new Set ( ) ) ;
134
+ const [ tsSelectedId , setTsSelectedId ] = useState < string | null > ( null ) ;
114
135
115
136
function nodeId ( n : ASTNode ) {
116
137
return `${ n . type } :${ n . start } -${ n . end } ` ;
@@ -171,6 +192,54 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => {
171
192
) ;
172
193
} ;
173
194
195
+ // TypeScript AST rendering (separate selection/expansion state)
196
+ function tsNodeId ( n : ASTNode ) {
197
+ return `ts:${ n . type } :${ n . start } -${ n . end } ` ;
198
+ }
199
+ function renderTsNode ( n : ASTNode , depth = 0 ) {
200
+ const id = tsNodeId ( n ) ;
201
+ const open = tsExpanded . has ( id ) ;
202
+ const hasKids = isExpandable ( n ) ;
203
+ const preview = n . text ? n . text . replace ( / \s + / g, ' ' ) . slice ( 0 , 40 ) : '' ;
204
+ return (
205
+ < div key = { id } className = "ast-node" style = { { paddingLeft : depth * 2 } } >
206
+ < div
207
+ className = { `ast-node-row ${ tsSelectedId === id ? 'selected' : '' } ` }
208
+ onClick = { ( ) => {
209
+ setTsSelectedId ( id ) ;
210
+ onAstNodeSelect ?.( n . start , n . end ) ;
211
+ } }
212
+ >
213
+ { hasKids && (
214
+ < button
215
+ className = { `twisty ${ open ? 'open' : '' } ` }
216
+ onClick = { e => {
217
+ e . stopPropagation ( ) ;
218
+ setTsExpanded ( prev => {
219
+ const next = new Set ( prev ) ;
220
+ if ( next . has ( id ) ) next . delete ( id ) ;
221
+ else next . add ( id ) ;
222
+ return next ;
223
+ } ) ;
224
+ } }
225
+ aria-label = { open ? 'Collapse' : 'Expand' }
226
+ />
227
+ ) }
228
+ < span className = "node-type" > { n . type } </ span >
229
+ < span className = "node-range" >
230
+ [{ n . start } , { n . end } ]
231
+ </ span >
232
+ { preview && < span className = "node-preview" > “{ preview } ”</ span > }
233
+ </ div >
234
+ { open && hasKids && (
235
+ < div className = "ast-children" >
236
+ { n . children ! . map ( child => renderTsNode ( child , depth + 1 ) ) }
237
+ </ div >
238
+ ) }
239
+ </ div >
240
+ ) ;
241
+ }
242
+
174
243
// When selection in editor changes, select smallest covering AST node and expand its ancestors
175
244
useEffect ( ( ) => {
176
245
if ( ! astTree || ! selectedAstNodeRange ) return ;
@@ -200,17 +269,52 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => {
200
269
}
201
270
} , [ selectedAstNodeRange , astTree ] ) ;
202
271
203
- // Auto-expand root when a new tree arrives
272
+ // Auto-expand roots when new trees arrive
204
273
useEffect ( ( ) => {
205
- if ( ! astTree ) return ;
206
- const id = nodeId ( astTree ) ;
207
- setExpanded ( prev => {
208
- if ( prev . has ( id ) ) return prev ;
209
- const next = new Set ( prev ) ;
210
- next . add ( id ) ;
211
- return next ;
212
- } ) ;
213
- } , [ astTree ] ) ;
274
+ if ( astTree ) {
275
+ const id = nodeId ( astTree ) ;
276
+ setExpanded ( prev => {
277
+ if ( prev . has ( id ) ) return prev ;
278
+ const next = new Set ( prev ) ;
279
+ next . add ( id ) ;
280
+ return next ;
281
+ } ) ;
282
+ }
283
+ if ( tsAstTree ) {
284
+ const id = tsNodeId ( tsAstTree ) ;
285
+ setTsExpanded ( prev => {
286
+ if ( prev . has ( id ) ) return prev ;
287
+ const next = new Set ( prev ) ;
288
+ next . add ( id ) ;
289
+ return next ;
290
+ } ) ;
291
+ }
292
+ } , [ astTree , tsAstTree ] ) ;
293
+
294
+ // Selection sync for TS AST
295
+ useEffect ( ( ) => {
296
+ if ( ! tsAstTree || ! selectedAstNodeRange ) return ;
297
+ const { start, end } = selectedAstNodeRange ;
298
+ let best : { node : ASTNode ; depth : number ; path : ASTNode [ ] } | null = null ;
299
+ function visit ( node : ASTNode , depth : number , path : ASTNode [ ] ) {
300
+ if ( node . start <= start && node . end >= end ) {
301
+ if ( ! best || depth > best . depth )
302
+ best = { node, depth, path : [ ...path , node ] } ;
303
+ if ( node . children )
304
+ for ( const c of node . children ) visit ( c , depth + 1 , [ ...path , node ] ) ;
305
+ }
306
+ }
307
+ visit ( tsAstTree , 0 , [ ] ) ;
308
+ if ( best ) {
309
+ const id = tsNodeId ( best . node ) ;
310
+ setTsSelectedId ( id ) ;
311
+ setTsExpanded ( prev => {
312
+ const next = new Set ( prev ) ;
313
+ for ( const p of best ! . path ) next . add ( tsNodeId ( p ) ) ;
314
+ return next ;
315
+ } ) ;
316
+ }
317
+ } , [ selectedAstNodeRange , tsAstTree ] ) ;
214
318
215
319
// Share button state and handler
216
320
const [ shareCopied , setShareCopied ] = useState ( false ) ;
@@ -245,7 +349,19 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => {
245
349
onClick = { ( ) => setActiveTab ( 'ast' ) }
246
350
aria-pressed = { activeTab === 'ast' }
247
351
>
248
- AST
352
+ AST (tsgo)
353
+ </ Button >
354
+ < Button
355
+ type = "button"
356
+ variant = { activeTab === 'ast_ts' ? 'default' : 'outline' }
357
+ size = "sm"
358
+ onClick = { ( ) => {
359
+ setActiveTab ( 'ast_ts' ) ;
360
+ onRequestTsAst ?.( ) ;
361
+ } }
362
+ aria-pressed = { activeTab === 'ast_ts' }
363
+ >
364
+ AST (TypeScript)
249
365
</ Button >
250
366
</ div >
251
367
< div className = "result-actions" >
@@ -320,6 +436,22 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => {
320
436
) }
321
437
</ div >
322
438
) }
439
+
440
+ { ! error && activeTab === 'ast_ts' && (
441
+ < div className = "ast-view" >
442
+ { tsAstTree ? (
443
+ < div className = "ast-tree" role = "tree" >
444
+ { renderTsNode ( tsAstTree ) }
445
+ </ div >
446
+ ) : (
447
+ < div className = "empty-state" >
448
+ < div className = "empty-text" >
449
+ TypeScript AST will be displayed here
450
+ </ div >
451
+ </ div >
452
+ ) }
453
+ </ div >
454
+ ) }
323
455
</ div >
324
456
) : (
325
457
< div className = "result-content" >
0 commit comments