2026-09-07 15:16:52 -04:00
import { invoke } from '@tauri-apps/api/core' ;
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api' ;
import dagre from '@dagrejs/dagre' ;
type Location = { path : string ; range : { start : { line : number ; character : number } ; end : { line : number ; character : number } } ; label : string } ;
type Node = { id : string ; label : string ; kind : string ; detail : string ; location : Location } ;
type GraphOptions = { depth : number ; excluded_namespaces : string [ ] } ;
type Graph = { title : string ; nodes : Node [ ] ; edges : { from : string ; to : string ; label : string } [ ] ; warnings : string [ ] } ;
type Options = { workspace : ( ) = > { root : string ; settings ? : { graph? : GraphOptions } } | null ; path : ( ) = > string ; epoch : ( ) = > number ; ready : ( ) = > Promise < unknown > ; open : ( path : string , hit : { path : string ; line : number ; column : number ; end_column : number } , semantic? : boolean ) = > Promise < void > } ;
const $ = < T extends HTMLElement = HTMLElement > ( id : string ) = > document . getElementById ( id ) as T ;
const ns = 'http://www.w3.org/2000/svg' ;
function svg < K extends keyof SVGElementTagNameMap > ( tag : K , attrs : Record < string , string > = { } ) { const el = document . createElementNS ( ns , tag ) ; for ( const [ k , v ] of Object . entries ( attrs ) ) el . setAttribute ( k , v ) ; return el ; }
export function installGraph ( editor : monaco.editor.IStandaloneCodeEditor , options : Options ) {
const pane = $ ( 'explanation-pane' ) ;
const chat = document . createElement ( 'div' ) ; chat . id = 'chat-panel' ; chat . setAttribute ( 'role' , 'tabpanel' ) ;
chat . append ( . . . pane . childNodes ) ; pane . append ( chat ) ;
const tabs = document . createElement ( 'div' ) ; tabs . className = 'sidebar-tabs' ;
tabs . innerHTML = '<div role="tablist" aria-label="Right sidebar"><button id="chat-tab" role="tab" aria-controls="chat-panel" aria-selected="true">Chat</button><button id="graph-tab" role="tab" aria-controls="graph-panel" aria-selected="false">Graph</button></div>' ;
const close = $ ( 'close-explanation' ) ; close . setAttribute ( 'aria-label' , 'Collapse right sidebar' ) ; close . title = 'Collapse right sidebar' ; close . classList . add ( 'icon-button' ) ; tabs . append ( close ) ; pane . prepend ( tabs ) ;
const panel = document . createElement ( 'div' ) ; panel . id = 'graph-panel' ; panel . hidden = true ; panel . setAttribute ( 'role' , 'tabpanel' ) ;
panel . innerHTML = ` <div class="graph-tools"><strong id="graph-title">Symbol graph</strong><button id="graph-refresh">Graph selected symbol</button><button id="graph-expand" title="Expand graph panel" aria-label="Expand graph panel" aria-pressed="false">⛶</button></div>
< details id = "graph-options" > < summary > Graph options < span id = "graph-options-summary" > < / span > < / summary >
< label for = "graph-depth" > Graph depth < / label > < select id = "graph-depth" > < option value = "1" > 1 · immediate neighbors < / option > < option value = "2" > 2 levels < / option > < option value = "3" > 3 levels < / option > < option value = "4" > 4 levels < / option > < / select >
< p class = "hint" > Independent of LLM context . Higher depths may take longer . < / p >
< label for = "graph-namespace" > Excluded namespaces < / label > < div id = "graph-exclusions" > < / div > < div class = "field-row" > < input id = "graph-namespace" placeholder = "library::detail" autocomplete = "off" > < button id = "graph-add" > Add < / button > < / div > < button id = "graph-apply" > Apply to graph < / button > < p id = "graph-options-status" class = "hint" role = "status" > < / p >
2026-09-07 15:52:46 -04:00
< / details > < div id = "graph-progress" hidden role = "status" aria-live = "polite" > < span class = "graph-spinner" aria-hidden = "true" > < / span > < span > Building graph … < / span > < progress aria-label = "Building graph" > < / progress > < / div > < p id = "graph-status" class = "hint" role = "status" > Select a concept , function , class , or struct name in the editor . < / p >
2026-09-07 15:16:52 -04:00
< div class = "graph-tools" > < button id = "graph-back" aria-label = "Previous graph" disabled > ← < / button > < button id = "graph-forward" aria-label = "Next graph" disabled > → < / button > < button id = "graph-fit" > Fit < / button > < button id = "graph-center" > Center symbol < / button > < button id = "graph-out" aria-label = "Zoom out graph" > − < / button > < button id = "graph-in" aria-label = "Zoom in graph" > + < / button > < span id = "graph-zoom" class = "hint" > 100 % < / span > < / div >
< div id = "graph-canvas" tabindex = "0" aria-label = "Symbol relationship graph" > < / div > < div id = "graph-detail" class = "hint" > < / div > < details id = "graph-warning-details" > < summary > Coverage and limits < / summary > < div id = "graph-warnings" class = "hint" > < / div > < / details > ` ;
pane . append ( panel ) ;
let active : 'chat' | 'graph' = 'chat' , graph : Graph | null = null , ticket = 0 , scale = 1 , needsFit = false ;
let drawing : SVGSVGElement | null = null , size = { width : 1 , height : 1 } ;
let graphOptions : GraphOptions = { depth : 1 , excluded_namespaces : [ 'std' ] } ;
let center : Location | undefined ;
const back : Location [ ] = [ ] , forward : Location [ ] = [ ] ;
let layoutPositions = new Map < string , { x : number ; y : number } > ( ) ;
const canvas = $ ( 'graph-canvas' ) ;
function historyControls() { $ < HTMLButtonElement > ( 'graph-back' ) . disabled = ! back . length ; $ < HTMLButtonElement > ( 'graph-forward' ) . disabled = ! forward . length ; }
function settingsUI() {
$ < HTMLSelectElement > ( 'graph-depth' ) . value = String ( graphOptions . depth ) ;
$ ( 'graph-options-summary' ) . textContent = ` · depth ${ graphOptions . depth } · ${ graphOptions . excluded_namespaces . length } excluded ` ;
$ ( 'graph-exclusions' ) . replaceChildren ( . . . graphOptions . excluded_namespaces . map ( name = > {
const chip = document . createElement ( 'button' ) ; chip . textContent = ` ${ name } × ` ; chip . setAttribute ( 'aria-label' , ` Remove namespace ${ name } ` ) ;
chip . onclick = ( ) = > { graphOptions . excluded_namespaces = graphOptions . excluded_namespaces . filter ( n = > n !== name ) ; settingsUI ( ) ; } ;
return chip ;
} ) ) ;
}
$ ( 'graph-depth' ) . onchange = ( ) = > { graphOptions . depth = Number ( $ < HTMLSelectElement > ( 'graph-depth' ) . value ) ; settingsUI ( ) ; } ;
$ ( 'graph-add' ) . onclick = ( ) = > {
const input = $ < HTMLInputElement > ( 'graph-namespace' ) , name = input . value . trim ( ) . replace ( /^::/ , '' ) . replace ( /::$/ , '' ) ;
if ( ! /^[A-Za-z_][A-Za-z_0-9]*(::[A-Za-z_][A-Za-z_0-9]*)*$/ . test ( name ) || name . length > 200 || graphOptions . excluded_namespaces . length >= 32 ) { $ ( 'graph-options-status' ) . textContent = 'Enter a namespace such as std or library::detail (up to 32 entries).' ; return ; }
if ( ! graphOptions . excluded_namespaces . includes ( name ) ) graphOptions . excluded_namespaces . push ( name ) ;
input . value = '' ; $ ( 'graph-options-status' ) . textContent = '' ; settingsUI ( ) ;
} ;
$ ( 'graph-namespace' ) . onkeydown = e = > { if ( e . key === 'Enter' ) { e . preventDefault ( ) ; $ ( 'graph-add' ) . click ( ) ; } } ;
$ ( 'graph-apply' ) . onclick = async ( ) = > {
const workspace = options . workspace ( ) ; if ( ! workspace ) return ;
const epoch = options . epoch ( ) , saved = structuredClone ( graphOptions ) ;
$ < HTMLButtonElement > ( 'graph-apply' ) . disabled = true ;
try {
await invoke ( 'save_graph_options' , { root : workspace.root , options : saved } ) ;
if ( options . epoch ( ) !== epoch ) return ;
if ( workspace . settings ) workspace . settings . graph = saved ;
$ ( 'graph-options-status' ) . textContent = 'Saved for this project.' ;
await refresh ( center , false ) ;
} catch ( error ) { if ( options . epoch ( ) === epoch ) $ ( 'graph-options-status' ) . textContent = String ( error ) ; }
finally { $ < HTMLButtonElement > ( 'graph-apply' ) . disabled = false ; }
} ;
$ ( 'graph-expand' ) . onclick = ( ) = > { const expanded = pane . classList . toggle ( 'graph-expanded' ) ; $ ( 'graph-expand' ) . setAttribute ( 'aria-pressed' , String ( expanded ) ) ; editor . layout ( ) ; } ;
settingsUI ( ) ;
const toggle = $ < HTMLButtonElement > ( 'toggle-sidebar' ) ;
function show ( tab : 'chat' | 'graph' ) {
active = tab ; if ( tab === 'chat' ) { pane . classList . remove ( 'graph-expanded' ) ; $ ( 'graph-expand' ) . setAttribute ( 'aria-pressed' , 'false' ) ; } pane . hidden = false ; document . querySelector ( 'main' ) ! . classList . add ( 'explaining' ) ;
chat . hidden = tab !== 'chat' ; panel . hidden = tab !== 'graph' ;
$ ( 'chat-tab' ) . setAttribute ( 'aria-selected' , String ( tab === 'chat' ) ) ; $ ( 'graph-tab' ) . setAttribute ( 'aria-selected' , String ( tab === 'graph' ) ) ;
toggle . setAttribute ( 'aria-expanded' , 'true' ) ; editor . layout ( ) ; if ( tab === 'graph' && needsFit ) fit ( ) ;
}
function collapse() { pane . classList . remove ( 'graph-expanded' ) ; $ ( 'graph-expand' ) . setAttribute ( 'aria-pressed' , 'false' ) ; pane . hidden = true ; document . querySelector ( 'main' ) ! . classList . remove ( 'explaining' ) ; toggle . setAttribute ( 'aria-expanded' , 'false' ) ; editor . layout ( ) ; }
$ ( 'chat-tab' ) . onclick = ( ) = > show ( 'chat' ) ;
$ ( 'graph-tab' ) . onclick = ( ) = > { show ( 'graph' ) ; if ( ! graph ) void refresh ( ) ; } ;
for ( const [ id , other ] of [ [ 'chat-tab' , 'graph-tab' ] , [ 'graph-tab' , 'chat-tab' ] ] ) $ ( id ) . onkeydown = event = > { if ( event . key === 'ArrowLeft' || event . key === 'ArrowRight' ) { event . preventDefault ( ) ; $ ( other ) . focus ( ) ; $ ( other ) . click ( ) ; } } ;
close . onclick = collapse ;
toggle . onclick = ( ) = > { if ( pane . hidden ) show ( active ) ; else collapse ( ) ; } ;
pane . addEventListener ( 'cex-show-chat' , ( ) = > show ( 'chat' ) ) ;
function zoom ( next : number ) {
const old = scale ; scale = Math . max ( 0.12 , Math . min ( 3 , next ) ) ;
if ( drawing ) {
const x = canvas . scrollLeft + canvas . clientWidth / 2 , y = canvas . scrollTop + canvas . clientHeight / 2 ;
drawing . setAttribute ( 'width' , String ( size . width * scale ) ) ; drawing . setAttribute ( 'height' , String ( size . height * scale ) ) ;
canvas . scrollLeft = x * scale / old - canvas . clientWidth / 2 ; canvas . scrollTop = y * scale / old - canvas . clientHeight / 2 ;
}
$ ( 'graph-zoom' ) . textContent = ` ${ Math . round ( scale * 100 ) } % ` ;
}
function fit ( readable = false ) { if ( pane . hidden || panel . hidden ) { needsFit = true ; return ; } needsFit = false ; zoom ( Math . max ( readable ? . 65 : .12 , Math . min ( 1 , Math . max ( 100 , canvas . clientWidth - 24 ) / size . width , Math . max ( 100 , canvas . clientHeight - 24 ) / size . height ) ) ) ; }
function centerSymbol() { const p = layoutPositions . get ( 'selected' ) ; if ( p ) { canvas . scrollLeft = p . x * scale - canvas . clientWidth / 2 ; canvas . scrollTop = p . y * scale - canvas . clientHeight / 2 ; } }
$ ( 'graph-fit' ) . onclick = ( ) = > fit ( ) ; $ ( 'graph-center' ) . onclick = ( ) = > { zoom ( 1 ) ; centerSymbol ( ) ; } ;
$ ( 'graph-in' ) . onclick = ( ) = > zoom ( scale * 1.25 ) ; $ ( 'graph-out' ) . onclick = ( ) = > zoom ( scale / 1.25 ) ;
canvas . addEventListener ( 'wheel' , event = > { if ( event . ctrlKey || event . metaKey ) { event . preventDefault ( ) ; zoom ( scale * ( event . deltaY < 0 ? 1.1 : 1 / 1.1 ) ) ; } } , { passive : false } ) ;
let drag : { x :number ; y :number ; left :number ; top :number } | null = null ;
canvas . onpointerdown = e = > { if ( e . button || ( e . target as Element ) . closest ( '.graph-node' ) ) return ; drag = { x :e.clientX , y :e.clientY , left :canvas.scrollLeft , top :canvas.scrollTop } ; canvas . setPointerCapture ( e . pointerId ) ; canvas . classList . add ( 'dragging' ) ; } ;
canvas . onpointermove = e = > { if ( drag ) { canvas . scrollLeft = drag . left + drag . x - e . clientX ; canvas . scrollTop = drag . top + drag . y - e . clientY ; } } ;
canvas . onpointerup = canvas . onpointercancel = ( ) = > { drag = null ; canvas . classList . remove ( 'dragging' ) ; } ;
canvas . onkeydown = e = > { if ( e . key === '+' || e . key === '=' ) { e . preventDefault ( ) ; zoom ( scale * 1.25 ) ; } else if ( e . key === '-' ) { e . preventDefault ( ) ; zoom ( scale / 1.25 ) ; } else if ( e . key === '0' ) { e . preventDefault ( ) ; fit ( ) ; } else if ( e . key === 'Home' ) { e . preventDefault ( ) ; centerSymbol ( ) ; } } ;
function select ( node : Node ) {
canvas . querySelectorAll ( '.graph-node' ) . forEach ( el = > el . classList . toggle ( 'focused' , el . getAttribute ( 'data-node' ) === node . id ) ) ;
$ ( 'graph-detail' ) . replaceChildren ( ) ;
const pre = document . createElement ( 'pre' ) ; pre . textContent = node . detail ;
const open = document . createElement ( 'button' ) ; open . textContent = 'Open source' ;
const p = node . location . range . start ;
open . onclick = ( ) = > void options . open ( node . location . path , { path : node.location.path , line :p.line + 1 , column :p.character + 1 , end_column :p.character + 1 } , true ) ;
$ ( 'graph-detail' ) . append ( pre , open ) ;
2026-09-07 15:52:46 -04:00
if ( node . kind === 'concept' || node . kind === 'selected' || node . kind === 'function' || node . kind === 'type' ) { const focus = document . createElement ( 'button' ) ; focus . textContent = node . kind === 'concept' ? 'Focus this concept' : 'Focus this symbol' ; focus . onclick = ( ) = > void refresh ( node . location ) ; $ ( 'graph-detail' ) . append ( focus ) ; }
2026-09-07 15:16:52 -04:00
}
function render ( value : Graph ) {
const layout = new dagre . graphlib . Graph ( ) . setGraph ( { rankdir : 'TB' , nodesep :32 , ranksep :54 , marginx :24 , marginy :24 } ) . setDefaultEdgeLabel ( ( ) = > ( { } ) ) ;
for ( const node of value . nodes ) layout . setNode ( node . id , { width :260 , height :92 } ) ;
for ( const edge of value . edges ) layout . setEdge ( edge . from , edge . to , { label :edge.label , width :edge.label?110 : 0 , height :16 } ) ;
dagre . layout ( layout ) ; layoutPositions = new Map ( value . nodes . map ( n = > [ n . id , layout . node ( n . id ) ] ) ) ; size = { width :layout.graph ( ) . width || 1 , height :layout.graph ( ) . height || 1 } ;
drawing = svg ( 'svg' , { viewBox : ` 0 0 ${ size . width } ${ size . height } ` , role : 'group' , 'aria-label' : value . title } ) ;
const defs = svg ( 'defs' ) , marker = svg ( 'marker' , { id : 'graph-arrow' , viewBox : '0 0 10 10' , refX : '9' , refY : '5' , markerWidth : '7' , markerHeight : '7' , orient : 'auto-start-reverse' } ) ;
marker . append ( svg ( 'path' , { d : 'M 0 0 L 10 5 L 0 10 z' , fill : 'var(--graph-edge)' } ) ) ; defs . append ( marker ) ; drawing . append ( defs ) ;
for ( const edge of value . edges ) {
const e = layout . edge ( edge . from , edge . to ) ;
drawing . append ( svg ( 'path' , { d :e.points.map ( ( p : { x :number ; y :number } , i : number ) = > ` ${ i ? 'L' : 'M' } ${ p . x } ${ p . y } ` ) . join ( ' ' ) , fill : 'none' , stroke : 'var(--graph-edge)' , 'stroke-width' : '1.5' , 'marker-end' : 'url(#graph-arrow)' } ) ) ;
if ( edge . label ) { const label = svg ( 'text' , { x :String ( e . x ) , y :String ( e . y - 4 ) , 'text-anchor' : 'middle' , class : 'graph-edge-label' } ) ; label . textContent = edge . label ; drawing . append ( label ) ; }
}
for ( const node of value . nodes ) {
const n = layout . node ( node . id ) , group = svg ( 'g' , { transform : ` translate( ${ n . x - 130 } , ${ n . y - 46 } ) ` , role : 'button' , tabindex : '0' , 'aria-label' : ` ${ node . kind } : ${ node . label } ` , 'data-node' : node . id , class : ` graph-node ${ node . kind } ` } ) ;
group . append ( svg ( 'rect' , { width : '260' , height : '92' , rx : '8' } ) ) ;
const title = svg ( 'title' ) ; title . textContent = node . detail ; group . append ( title ) ;
const kind = svg ( 'text' , { x : '12' , y : '18' , class : 'graph-kind' } ) ; kind . textContent = node . kind . toUpperCase ( ) ; group . append ( kind ) ;
const label = svg ( 'text' , { x : '12' , y : '38' } ) ;
const text = node . label . replace ( /\s+/g , ' ' ) ; const lines = text . match ( /.{1,32}(?:\s|$)|.{1,32}/g ) || [ '' ] ;
lines . slice ( 0 , 3 ) . forEach ( ( line , i ) = > { const span = svg ( 'tspan' , { x : '12' , dy :i? '17' : '0' } ) ; span . textContent = line . trim ( ) + ( i === 2 && lines . length > 3 ? '…' : '' ) ; label . append ( span ) ; } ) ; group . append ( label ) ;
2026-09-07 15:52:46 -04:00
group . onclick = ( ) = > select ( node ) ; group . ondblclick = ( ) = > { if ( [ 'concept' , 'function' , 'type' , 'selected' ] . includes ( node . kind ) ) void refresh ( node . location ) ; } ; group . onkeydown = event = > {
2026-09-07 15:16:52 -04:00
if ( event . key === 'Enter' || event . key === ' ' ) { event . preventDefault ( ) ; select ( node ) ; }
if ( event . key . startsWith ( 'Arrow' ) ) {
const direction = event . key , here = layoutPositions . get ( node . id ) ! ;
const candidates = value . nodes . filter ( other = > {
if ( other . id === node . id ) return false ;
const p = layoutPositions . get ( other . id ) ! ;
if ( direction === 'ArrowUp' ) return p . y < here . y ;
if ( direction === 'ArrowDown' ) return p . y > here . y ;
return Math . abs ( p . y - here . y ) < 50 && ( direction === 'ArrowLeft' ? p . x < here.x : p.x > here . x ) ;
} ) . sort ( ( a , b ) = > { const p = layoutPositions . get ( a . id ) ! , q = layoutPositions . get ( b . id ) ! ; return Math . hypot ( p . x - here . x , p . y - here . y ) - Math . hypot ( q . x - here . x , q . y - here . y ) ; } ) ;
if ( candidates [ 0 ] ) { event . preventDefault ( ) ; const element = [ . . . canvas . querySelectorAll < SVGGElement > ( '.graph-node' ) ] . find ( el = > el . dataset . node === candidates [ 0 ] . id ) ; element ? . focus ( ) ; element ? . scrollIntoView ( { block : 'nearest' , inline : 'nearest' } ) ; select ( candidates [ 0 ] ) ; }
}
} ;
drawing . append ( group ) ;
}
$ ( 'graph-canvas' ) . replaceChildren ( drawing ) ; fit ( true ) ; centerSymbol ( ) ;
$ ( 'graph-warnings' ) . replaceChildren ( . . . value . warnings . map ( w = > { const p = document . createElement ( 'p' ) ; p . textContent = w ; return p ; } ) ) ;
// Status and warnings affect the available canvas height after rendering.
const rendered = drawing ;
requestAnimationFrame ( ( ) = > { if ( drawing === rendered ) { fit ( true ) ; centerSymbol ( ) ; } } ) ;
}
2026-09-07 15:52:46 -04:00
function building ( value : boolean ) { $ ( 'graph-progress' ) . hidden = ! value ; canvas . setAttribute ( 'aria-busy' , String ( value ) ) ; $ < HTMLButtonElement > ( 'graph-refresh' ) . disabled = value ; }
2026-09-07 15:16:52 -04:00
async function refresh ( location? : Location , record = true ) {
const workspace = options . workspace ( ) ; if ( ! workspace ) return ;
const path = location ? . path || options . path ( ) , p = editor . getPosition ( ) ; if ( ! path || ! p ) return ;
const position = location ? . range . start || { line :p.lineNumber - 1 , character :p.column - 1 } ;
const operation = ++ ticket , epoch = options . epoch ( ) ; show ( 'graph' ) ;
2026-09-07 15:52:46 -04:00
$ ( 'graph-status' ) . textContent = 'Building relationships with clangd…' ; building ( true ) ;
2026-09-07 15:16:52 -04:00
graph = null ; $ ( 'graph-canvas' ) . replaceChildren ( ) ; $ ( 'graph-detail' ) . replaceChildren ( ) ; $ ( 'graph-warnings' ) . replaceChildren ( ) ;
try {
await options . ready ( ) ; if ( operation !== ticket || epoch !== options . epoch ( ) ) return ;
const result = await invoke < Graph > ( 'graph_request' , { root :workspace.root , path , position , options :structuredClone ( graphOptions ) } ) ;
if ( operation !== ticket || epoch !== options . epoch ( ) ) return ;
if ( record && center ) { back . push ( center ) ; if ( back . length > 30 ) back . shift ( ) ; forward . length = 0 ; }
graph = result ; center = graph . nodes . find ( n = > n . id === 'selected' ) ? . location ; historyControls ( ) ; render ( graph ) ; $ ( 'graph-title' ) . textContent = graph . title ;
2026-09-07 15:52:46 -04:00
$ ( 'graph-status' ) . textContent = 'Dependencies / constraints ↑ · members / uses / callers ↓. Select for details; double-click a symbol to refocus. Drag to pan, Ctrl/⌘+scroll to zoom.' ;
2026-09-07 15:16:52 -04:00
} catch ( error ) { if ( operation === ticket ) $ ( 'graph-status' ) . textContent = String ( error ) ; }
2026-09-07 15:52:46 -04:00
finally { if ( operation === ticket ) building ( false ) ; }
2026-09-07 15:16:52 -04:00
}
$ ( 'graph-back' ) . onclick = ( ) = > { const previous = back . pop ( ) ; if ( previous ) { if ( center ) forward . push ( center ) ; historyControls ( ) ; void refresh ( previous , false ) ; } } ;
$ ( 'graph-forward' ) . onclick = ( ) = > { const next = forward . pop ( ) ; if ( next ) { if ( center ) back . push ( center ) ; historyControls ( ) ; void refresh ( next , false ) ; } } ;
$ ( 'graph-refresh' ) . onclick = ( ) = > void refresh ( ) ; $ ( 'nav-graph' ) . onclick = ( ) = > void refresh ( ) ;
editor . addAction ( { id : 'cex.graph' , label : 'Show Symbol Graph' , contextMenuGroupId : 'navigation' , contextMenuOrder :3 , run : ( ) = > refresh ( ) } ) ;
2026-09-07 15:52:46 -04:00
return { reset ( ) { ++ ticket ; building ( false ) ; center = undefined ; back . length = 0 ; forward . length = 0 ; historyControls ( ) ; graphOptions = structuredClone ( options . workspace ( ) ? . settings ? . graph ? ? { depth :1 , excluded_namespaces : [ 'std' ] } ) ; settingsUI ( ) ; $ ( 'graph-options-status' ) . textContent = '' ; toggle . disabled = false ; $ < HTMLButtonElement > ( 'nav-graph' ) . disabled = true ; graph = null ; drawing = null ; $ ( 'graph-canvas' ) . replaceChildren ( ) ; $ ( 'graph-detail' ) . replaceChildren ( ) ; $ ( 'graph-warnings' ) . replaceChildren ( ) ; $ ( 'graph-title' ) . textContent = 'Symbol graph' ; $ ( 'graph-status' ) . textContent = 'Select a concept, function, class, or struct name in the editor.' ; $ < HTMLButtonElement > ( 'graph-refresh' ) . disabled = false ; collapse ( ) ; } , fileOpened ( ) { toggle . disabled = false ; $ < HTMLButtonElement > ( 'nav-graph' ) . disabled = editor . getModel ( ) ? . getLanguageId ( ) !== 'cpp' ; } } ;
2026-09-07 15:16:52 -04:00
}