2026-09-07 15:16:52 -04:00
import { invoke , isTauri } from '@tauri-apps/api/core' ;
import { open , confirm } from '@tauri-apps/plugin-dialog' ;
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api' ;
import 'monaco-editor/esm/vs/basic-languages/cpp/cpp.contribution' ;
import 'monaco-editor/esm/vs/editor/contrib/folding/browser/folding' ;
import 'monaco-editor/esm/vs/editor/contrib/find/browser/findController' ;
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker' ;
import './style.css' ;
2026-09-07 15:52:46 -04:00
import { installSidebarResize } from './resize' ;
2026-09-07 15:16:52 -04:00
import { installTheme } from './theme' ;
import { installNavigation } from './navigation' ;
import { installGraph } from './graph' ;
import { installVim } from './vim' ;
import { installExplanations } from './explanations' ;
self . MonacoEnvironment = { getWorker : ( ) = > new EditorWorker ( ) } ;
type Settings = { graph ? : { depth : number ; excluded_namespaces : string [ ] } ; version : number ; build_directory : string | null ; last_file : string | null ; search_exclusions : string [ ] ; clangd_path : string } ;
type Workspace = { root : string ; settings : Settings } ;
type Inspection = { root : string ; settings : Settings | null ; warning : string | null ; can_reset : boolean } ;
type Entry = { path : string ; name : string ; directory : boolean } ;
type Hit = { path : string ; line : number ; column : number ; end_column : number ; preview : string } ;
type SearchResults = { hits : Hit [ ] ; truncated : boolean ; cancelled : boolean ; skipped_files : number } ;
const $ = < T extends HTMLElement = HTMLElement > ( id : string ) = > document . getElementById ( id ) as T ;
document . querySelector < HTMLDivElement > ( '#app' ) ! . innerHTML = `
2026-09-07 15:52:46 -04:00
< header > < details id = "file-menu" > < summary > File < / summary > < div class = "file-dropdown" > < button id = "file-open" > Open Project … < / button > < details id = "file-recents" > < summary > Open Recent < / summary > < div id = "file-recent-list" > < span class = "hint" > Loading … < / span > < / div > < / details > < button id = "file-welcome" > Return to Welcome < / button > < / div > < / details > < div class = "brand" > cex < span > CODE EXPLORER < / span > < / div > < div id = "project-name" > No project open < / div > < select id = "theme-select" aria-label = "Color theme" title = "Color theme" > < option value = "system" > System < / option > < option value = "light" > Light < / option > < option value = "dark" > Dark < / option > < / select > < button id = "toggle-sidebar" class = "icon-button" aria-label = "Toggle right sidebar" title = "Toggle right sidebar" aria-expanded = "false" aria-controls = "explanation-pane" disabled > < svg viewBox = "0 0 20 20" width = "18" height = "18" fill = "none" stroke = "currentColor" stroke-width = "1.5" aria-hidden = "true" > < rect x = "2.5" y = "3.5" width = "15" height = "13" rx = "2" / > < path d = "M12.5 4v12" / > < / svg > < / button > < button id = "workspace-button" > Open project … < / button > < / header >
2026-09-07 15:16:52 -04:00
< main class = "landing" >
< section id = "landing" aria-label = "Open Recent" > < div class = "landing-intro" > < div class = "welcome-mark" > { cex } < / div > < h1 > Open Recent < / h1 > < p > Pick up where you left off , or explore a new project . < / p > < button id = "recent-open" class = "primary" > Open a project < / button > < / div > < div class = "recent-heading" > < h2 > Recent projects < / h2 > < button id = "refresh-recents" aria-label = "Refresh recent projects" > Refresh < / button > < / div > < p id = "recent-status" class = "hint" role = "status" > Loading recent projects … < / p > < div id = "recent-projects" > < / div > < small class = "hint" > History is stored in ~ / . c e x . P r o j e c t s e t t i n g s s t a y w i t h e a c h p r o j e c t . < / s m a l l > < / s e c t i o n >
< aside > < nav > < button id = "files-tab" class = "active" > Files < / button > < button id = "search-tab" > Search < / button > < button id = "navigation-tab" > Navigation < / button > < / nav >
< section id = "files-panel" > < div class = "panel-heading" > PROJECT FILES < button id = "refresh" title = "Refresh file tree" aria-label = "Refresh file tree" > ↻ < / button > < / div > < div id = "tree" > < p class = "hint" > Open a directory to explore its source . < / p > < / div > < / section >
< section id = "search-panel" hidden > < label for = "query" > Search project text < / label > < input id = "query" type = "search" placeholder = "Literal, case-sensitive search" autocomplete = "off" > < label for = "exclusions" class = "exclusions-label" > Exclude paths ( one glob per line ) < / label > < textarea id = "exclusions" rows = "2" placeholder = "docs/ **/generated/**" > < / textarea > < button id = "apply-exclusions" > Apply exclusions < / button > < p id = "exclusion-status" class = "hint" role = "status" > < / p > < div class = "search-actions" > < small > Respects . gitignore < / small > < button id = "cancel-search" hidden > Cancel < / button > < / div > < p id = "search-summary" class = "hint" > Results appear as you type . < / p > < div id = "results" > < / div > < / section >
< section id = "navigation-panel" hidden > < div class = "panel-heading" > CLANGD < button id = "restart-analysis" title = "Restart clangd and reload compilation commands" > Restart analysis < / button > < / div > < p id = "analysis-detail" class = "hint" > Open a project with a build directory to enable navigation . < / p > < p id = "diagnostic-count" class = "hint" > < / p > < h3 id = "navigation-title" > Code navigation < / h3 > < p id = "navigation-summary" class = "hint" > Select a symbol , then choose Definition , References , or Callers . < / p > < div id = "navigation-results" > < / div > < p id = "navigation-evidence" class = "hint" > < / p > < / section >
< / aside >
< section class = "code-pane" > < div id = "file-bar" > < span id = "file-path" > Welcome to CEX < / span > < span class = "badge" > READ ONLY < / span > < button id = "find-file" disabled > Find in file < / button > < / div > < div id = "navigation-bar" > < button id = "nav-back" disabled title = "Navigate back (Alt+Left)" > ← Back < / button > < button id = "nav-definition" disabled title = "Go to definition (F12 or Ctrl/Cmd+click)" > Definition < / button > < button id = "nav-references" disabled title = "Find references (Shift+F12)" > References < / button > < button id = "nav-callers" disabled > Callers < / button > < button id = "nav-hover" disabled > Type info < / button > < button id = "nav-graph" disabled > Graph < / button > < button id = "nav-explain" disabled > Explain … < / button > < button id = "analysis-state" title = "Show analysis status" > Navigation unavailable < / button > < / div > < div id = "editor" > < / div > < div id = "welcome" > < div class = "welcome-mark" > { cex } < / div > < h1 > Explore the code . < / h1 > < p > Browse C and C ++ source , fold sections , < br > and find text across your project . < / p > < button id = "welcome-open" class = "primary" > Open a project < / button > < small > Select a Meson or CMake build to enable definitions , references , and callers . < / small > < / div > < / section >
< / main >
< footer > < span id = "status" role = "status" > Ready < / span > < span id = "cursor" > C / C ++ · Read only < / span > < / footer >
< dialog id = "setup" > < form id = "setup-form" > < h2 id = "setup-title" > Open project < / h2 > < p class = "hint" > Choose the source directory . CEX stores its settings and caches in that directory ’ s . cex folder . < / p >
< label for = "source" > Source directory < / label > < div class = "field-row" > < input id = "source" placeholder = "/path/to/project" required > < button id = "browse-source" type = "button" > Browse … < / button > < / div >
< p id = "saved-state" class = "hint" role = "status" > < / p >
< label for = "build" > Meson or CMake build directory < span class = "optional" > optional for browsing < / span > < / label > < div class = "field-row" > < input id = "build" placeholder = "/path/to/project/build" > < button id = "browse-build" type = "button" > Browse … < / button > < / div >
< p class = "hint" > Select an existing configured build with compile_commands . json . For CMake , enable CMAKE_EXPORT_COMPILE_COMMANDS with a Ninja or Makefile generator ; for presets , choose the preset ’ s binary directory . CEX does not configure or build your project . < / p >
< label for = "clangd-path" > clangd executable < / label > < input id = "clangd-path" placeholder = "clangd" value = "clangd" > < p class = "hint" > Executable name on PATH or an absolute path . Changing it restarts analysis . < / p >
< label class = "reset-label" > < input id = "reset" type = "checkbox" > Force reset — discard this project ’ s CEX settings and caches < / label >
< p id = "setup-error" class = "error" role = "alert" > < / p > < div class = "dialog-actions" > < button id = "setup-cancel" type = "button" > Cancel < / button > < button id = "setup-submit" class = "primary" type = "submit" > Open project < / button > < / div >
< / form > < / dialog > ` ;
const editor = monaco . editor . create ( $ ( 'editor' ) , {
contextmenu : true ,
theme : 'vs-dark' , readOnly : true , domReadOnly : true , automaticLayout : true ,
fontSize : 14 , fontFamily : "'SFMono-Regular', Consolas, 'Liberation Mono', monospace" ,
lineHeight : 22 , minimap : { enabled : false } , scrollBeyondLastLine : false ,
folding : true , foldingStrategy : 'auto' , showFoldingControls : 'always' ,
renderLineHighlight : 'all' , padding : { top : 14 } , wordWrap : 'off' ,
model : null ,
} ) ;
installTheme ( ) ;
let workspace : Workspace | null = null ;
let epoch = 0 ;
let fileRequest = 0 ;
let searchRequest = 0 ;
let treeRequest = 0 ;
let searchTimer : ReturnType < typeof setTimeout > ;
let inspecting = 0 ;
let inspectedRoot = '' ;
let inspection : Inspection | null = null ;
let currentPath = '' ;
const viewStates = new Map < string , monaco.editor.ICodeEditorViewState > ( ) ;
let rememberQueue : Promise < unknown > = Promise . resolve ( ) ;
const setup = $ < HTMLDialogElement > ( 'setup' ) ;
const source = $ < HTMLInputElement > ( 'source' ) ;
const build = $ < HTMLInputElement > ( 'build' ) ;
const reset = $ < HTMLInputElement > ( 'reset' ) ;
const query = $ < HTMLInputElement > ( 'query' ) ;
const exclusions = $ < HTMLTextAreaElement > ( 'exclusions' ) ;
const clangdPath = $ < HTMLInputElement > ( 'clangd-path' ) ;
const navigation = installNavigation ( editor , { workspace : ( ) = > workspace , path : ( ) = > currentPath , epoch : ( ) = > epoch , showPanel : ( ) = > switchPanel ( 'navigation' ) , open : openFile } ) ;
const explanations = installExplanations ( editor , { workspace : ( ) = > workspace , path : ( ) = > currentPath , epoch : ( ) = > epoch , ready : navigation.whenReady , open : openFile } ) ;
const graph = installGraph ( editor , { workspace : ( ) = > workspace , path : ( ) = > currentPath , epoch : ( ) = > epoch , ready : navigation.whenReady , open : openFile } ) ;
const vim = installVim ( editor , navigation ) ;
2026-09-07 15:52:46 -04:00
installSidebarResize ( editor ) ;
2026-09-07 15:16:52 -04:00
function status ( message : string , error = false ) { $ ( 'status' ) . textContent = message ; $ ( 'status' ) . classList . toggle ( 'error' , error ) ; }
function errorText ( error : unknown ) { return error instanceof Error ? error.message : String ( error ) ; }
function button ( text : string , className = '' ) { const el = document . createElement ( 'button' ) ; el . textContent = text ; el . className = className ; return el ; }
2026-09-07 15:52:46 -04:00
let closingWorkspace = false ;
2026-09-07 15:16:52 -04:00
function showSetup() {
2026-09-07 15:52:46 -04:00
if ( closingWorkspace ) return ;
2026-09-07 15:16:52 -04:00
source . value = workspace ? . root ? ? '' ;
build . value = workspace ? . settings . build_directory ? ? '' ;
clangdPath . value = workspace ? . settings . clangd_path ? ? 'clangd' ;
reset . checked = false ; reset . disabled = true ;
inspectedRoot = '' ; inspection = null ;
$ ( 'saved-state' ) . textContent = '' ; $ ( 'setup-error' ) . textContent = '' ;
$ ( 'setup-title' ) . textContent = workspace ? 'Workspace settings' : 'Open project' ;
setup . showModal ( ) ;
if ( source . value ) void inspectSource ( ) ;
}
async function inspectSource() {
const request = ++ inspecting ;
const root = source . value . trim ( ) ;
inspectedRoot = '' ; inspection = null ; reset . checked = false ; reset . disabled = true ;
if ( ! root ) { $ ( 'saved-state' ) . textContent = '' ; return ; }
$ ( 'saved-state' ) . textContent = 'Checking for saved workspace…' ;
try {
const result = await invoke < Inspection > ( 'inspect_workspace' , { root } ) ;
if ( request !== inspecting || source . value . trim ( ) !== root ) return ;
inspection = result ; inspectedRoot = root ;
build . value = result . settings ? . build_directory ? ? '' ;
clangdPath . value = result . settings ? . clangd_path ? ? 'clangd' ;
reset . disabled = ! result . can_reset ;
$ ( 'saved-state' ) . textContent = result . warning ? ? ( result . settings ? 'Saved workspace found. Build selection and last file will be restored.' : 'New workspace. A .cex folder will be created on opening.' ) ;
} catch ( error ) { if ( request === inspecting ) $ ( 'saved-state' ) . textContent = errorText ( error ) ; }
}
source . addEventListener ( 'input' , ( ) = > { ++ inspecting ; inspectedRoot = '' ; inspection = null ; reset . checked = false ; reset . disabled = true ; build . value = '' ; clangdPath . value = 'clangd' ; $ ( 'saved-state' ) . textContent = '' ; } ) ;
source . addEventListener ( 'change' , ( ) = > void inspectSource ( ) ) ;
$ ( 'browse-source' ) . onclick = async ( ) = > {
try { const path = await open ( { directory : true , multiple : false , title : 'Select source directory' } ) ; if ( typeof path === 'string' ) { source . value = path ; await inspectSource ( ) ; } }
catch ( error ) { $ ( 'setup-error' ) . textContent = errorText ( error ) ; }
} ;
$ ( 'browse-build' ) . onclick = async ( ) = > {
try { const path = await open ( { directory : true , multiple : false , title : 'Select configured Meson or CMake build directory' } ) ; if ( typeof path === 'string' ) build . value = path ; }
catch ( error ) { $ ( 'setup-error' ) . textContent = errorText ( error ) ; }
} ;
$ ( 'workspace-button' ) . onclick = showSetup ;
$ ( 'welcome-open' ) . onclick = showSetup ;
$ ( 'setup-cancel' ) . onclick = ( ) = > setup . close ( ) ;
$ ( 'setup-form' ) . onsubmit = async ( event ) = > {
event . preventDefault ( ) ;
const controls = [ . . . setup . querySelectorAll < HTMLInputElement | HTMLButtonElement > ( 'input, button' ) ] ;
controls . forEach ( c = > c . disabled = true ) ;
$ ( 'setup-error' ) . textContent = '' ;
try {
if ( inspectedRoot !== source . value . trim ( ) ) await inspectSource ( ) ;
if ( ! inspection ) throw new Error ( 'Choose an accessible source directory first.' ) ;
if ( inspection . warning && ! reset . checked ) throw new Error ( inspection . warning ) ;
if ( reset . checked && ! await confirm ( 'Delete CEX settings and caches in this project’ s .cex folder and start fresh? Source and build files will be preserved.' , { title : 'Force reset CEX workspace' , kind : 'warning' } ) ) return ;
++ fileRequest ;
await rememberQueue ;
const next = await invoke < Workspace > ( 'open_workspace' , { root : source.value.trim ( ) , build : build.value.trim ( ) || null , reset : reset.checked , clangdPath : clangdPath.value.trim ( ) || "clangd" } ) ;
workspace = next ; $ ( 'landing' ) . hidden = true ; document . querySelector ( 'main' ) ! . classList . remove ( 'landing' ) ; ++ epoch ; ++ fileRequest ; ++ searchRequest ;
navigation . reset ( ) ;
vim . reset ( ) ;
explanations . reset ( ) ;
graph . reset ( ) ;
exclusions . value = ( next . settings . search_exclusions ? ? [ ] ) . join ( '\n' ) ;
$ ( 'exclusion-status' ) . textContent = '' ;
void navigation . start ( ) ;
clearTimeout ( searchTimer ) ; viewStates . clear ( ) ; currentPath = '' ;
const model = editor . getModel ( ) ; editor . setModel ( null ) ; model ? . dispose ( ) ;
$ ( 'welcome' ) . hidden = false ; $ ( 'file-path' ) . textContent = 'Choose a source file' ;
$ < HTMLButtonElement > ( 'find-file' ) . disabled = true ;
query . value = '' ; $ ( 'results' ) . replaceChildren ( ) ; $ ( 'search-summary' ) . textContent = 'Results appear as you type.' ;
$ ( 'cancel-search' ) . hidden = true ;
$ ( 'project-name' ) . textContent = next . root . split ( '/' ) . pop ( ) || next . root ;
$ ( 'project-name' ) . title = next . root ;
$ ( 'workspace-button' ) . textContent = 'Workspace…' ;
setup . close ( ) ; switchPanel ( 'files' ) ;
status ( next . settings . build_directory ? ` Build: ${ next . settings . build_directory } ` : 'Browsing without a build directory' ) ;
await refreshTree ( ) ;
if ( next . settings . last_file ) await openFile ( next . settings . last_file ) ;
try { await invoke ( 'remember_recent' , { root : next.root } ) ; }
catch ( error ) { status ( ` Project opened, but recent history could not be saved: ${ errorText ( error ) } ` , true ) ; }
} catch ( error ) { $ ( 'setup-error' ) . textContent = errorText ( error ) ; status ( errorText ( error ) , true ) ; }
finally { controls . forEach ( c = > c . disabled = false ) ; reset . disabled = ! inspection ? . can_reset ; }
} ;
function switchPanel ( panel : 'files' | 'search' | 'navigation' ) {
for ( const name of [ 'files' , 'search' , 'navigation' ] ) {
$ ( name + '-panel' ) . hidden = name !== panel ;
$ ( name + '-tab' ) . classList . toggle ( 'active' , name === panel ) ;
}
if ( panel === 'search' ) query . focus ( ) ;
}
$ ( 'files-tab' ) . onclick = ( ) = > switchPanel ( 'files' ) ;
$ ( 'search-tab' ) . onclick = ( ) = > switchPanel ( 'search' ) ;
$ ( 'navigation-tab' ) . onclick = ( ) = > switchPanel ( 'navigation' ) ;
async function directoryNodes ( path : string , level : number , activeEpoch : number ) : Promise < DocumentFragment > {
const entries = await invoke < Entry [ ] > ( 'list_directory' , { path } ) ;
const fragment = document . createDocumentFragment ( ) ;
if ( epoch !== activeEpoch ) return fragment ;
for ( const entry of entries ) {
const row = button ( ` ${ entry . directory ? '▸' : '·' } ${ entry . name } ` , 'tree-row' ) ;
row . style . paddingLeft = ` ${ 12 + level * 15 } px ` ; row . title = entry . path ;
if ( entry . directory ) {
row . setAttribute ( 'aria-expanded' , 'false' ) ;
const children = document . createElement ( 'div' ) ; children . hidden = true ;
let loaded = false ;
row . onclick = async ( ) = > {
if ( epoch !== activeEpoch ) return ;
if ( ! loaded ) {
row . disabled = true ;
try { children . replaceChildren ( await directoryNodes ( entry . path , level + 1 , activeEpoch ) ) ; loaded = true ; }
catch ( error ) { status ( errorText ( error ) , true ) ; return ; }
finally { row . disabled = false ; }
}
children . hidden = ! children . hidden ;
row . textContent = ` ${ children . hidden ? '▸' : '▾' } ${ entry . name } ` ;
row . setAttribute ( 'aria-expanded' , String ( ! children . hidden ) ) ;
} ;
fragment . append ( row , children ) ;
} else {
row . dataset . path = entry . path ;
row . classList . toggle ( 'selected' , entry . path === currentPath ) ;
row . onclick = ( ) = > { if ( epoch === activeEpoch ) void openFile ( entry . path ) ; } ;
fragment . append ( row ) ;
}
}
return fragment ;
}
async function refreshTree() {
if ( ! workspace ) return ;
const request = ++ treeRequest , activeEpoch = epoch ;
$ ( 'tree' ) . textContent = 'Loading files…' ;
try {
const nodes = await directoryNodes ( '' , 0 , activeEpoch ) ;
if ( request === treeRequest && activeEpoch === epoch ) {
$ ( 'tree' ) . replaceChildren ( nodes ) ;
if ( ! $ ( 'tree' ) . childElementCount ) $ ( 'tree' ) . textContent = 'This directory is empty.' ;
}
} catch ( error ) { if ( request === treeRequest ) { $ ( 'tree' ) . textContent = 'Unable to list files.' ; status ( errorText ( error ) , true ) ; } }
}
$ ( 'refresh' ) . onclick = ( ) = > void refreshTree ( ) ;
async function openFile ( path : string , hit? : Omit < Hit , 'preview' > , semantic = false ) {
if ( ! workspace ) return ;
const request = ++ fileRequest , activeEpoch = epoch , root = workspace . root ;
status ( ` Loading ${ path } … ` ) ;
try {
const doc = await invoke < { path : string ; content : string } > ( 'read_file' , { path } ) ;
if ( request !== fileRequest || activeEpoch !== epoch ) return ;
const previous = editor . getModel ( ) , view = editor . saveViewState ( ) ;
if ( view && currentPath ) viewStates . set ( currentPath , view ) ;
const language = semantic || /\.(c|h|cc|hh|cpp|hpp|cxx|hxx|ipp|tpp|inl|ixx|cppm)$/i . test ( path ) ? 'cpp' : 'plaintext' ;
const model = monaco . editor . createModel ( doc . content , language ) ;
editor . setModel ( model ) ; previous ? . dispose ( ) ; currentPath = path ;
if ( viewStates . has ( path ) ) editor . restoreViewState ( viewStates . get ( path ) ! ) ;
if ( hit ) { editor . setSelection ( { startLineNumber : hit.line , endLineNumber : hit.line , startColumn : hit.column , endColumn : hit.end_column } ) ; editor . revealLineInCenter ( hit . line ) ; }
$ ( 'welcome' ) . hidden = true ; $ ( 'file-path' ) . textContent = path ; $ ( 'file-path' ) . title = path ;
$ < HTMLButtonElement > ( 'find-file' ) . disabled = false ;
document . querySelectorAll < HTMLElement > ( '[data-path]' ) . forEach ( row = > row . classList . toggle ( 'selected' , row . dataset . path === path ) ) ;
status ( ` ${ model . getLineCount ( ) . toLocaleString ( ) } lines · ${ language === 'cpp' ? 'C / C++' : 'Plain text' } · Read only ` ) ;
editor . focus ( ) ;
navigation . fileOpened ( ) ;
explanations . fileOpened ( ) ;
graph . fileOpened ( ) ;
if ( path . startsWith ( '/' ) || ( workspace . settings . build_directory && ` ${ root } / ${ path } ` . startsWith ( workspace . settings . build_directory + '/' ) ) ) return ;
workspace . settings . last_file = path ;
rememberQueue = rememberQueue . then ( ( ) = > invoke ( 'remember_file' , { path , root } ) ) . catch ( error = > { if ( epoch === activeEpoch ) status ( ` File opened, but settings could not be saved: ${ errorText ( error ) } ` , true ) ; } ) ;
} catch ( error ) { if ( request === fileRequest && activeEpoch === epoch ) status ( errorText ( error ) , true ) ; }
}
editor . onDidChangeCursorPosition ( event = > { $ ( 'cursor' ) . textContent = ` Ln ${ event . position . lineNumber } , Col ${ event . position . column } · Read only ` ; } ) ;
$ ( 'find-file' ) . onclick = ( ) = > { editor . focus ( ) ; void editor . getAction ( 'actions.find' ) ? . run ( ) ; } ;
function cancelSearch() {
++ searchRequest ; clearTimeout ( searchTimer ) ;
void invoke ( 'cancel_search' ) . catch ( error = > status ( errorText ( error ) , true ) ) ;
$ ( 'cancel-search' ) . hidden = true ;
}
$ ( 'cancel-search' ) . onclick = ( ) = > { cancelSearch ( ) ; $ ( 'search-summary' ) . textContent = 'Search cancelled.' ; } ;
$ ( 'apply-exclusions' ) . onclick = async ( ) = > {
if ( ! workspace ) { $ ( 'exclusion-status' ) . textContent = 'Open a project first.' ; return ; }
const patterns = exclusions . value . split ( '\n' ) . map ( s = > s . trim ( ) ) . filter ( Boolean ) ;
const root = workspace . root , activeEpoch = epoch ;
$ < HTMLButtonElement > ( 'apply-exclusions' ) . disabled = true ;
cancelSearch ( ) ;
try {
await invoke ( 'save_search_exclusions' , { root , exclusions : patterns } ) ;
if ( epoch !== activeEpoch ) return ;
workspace . settings . search_exclusions = patterns ;
$ ( 'exclusion-status' ) . textContent = 'Exclusions saved for this project.' ;
query . dispatchEvent ( new Event ( 'input' ) ) ;
} catch ( error ) { if ( epoch === activeEpoch ) $ ( 'exclusion-status' ) . textContent = errorText ( error ) ; }
finally { $ < HTMLButtonElement > ( 'apply-exclusions' ) . disabled = false ; }
} ;
query . oninput = ( ) = > {
cancelSearch ( ) ; $ ( 'results' ) . replaceChildren ( ) ;
if ( ! workspace ) { $ ( 'search-summary' ) . textContent = 'Open a project first.' ; return ; }
if ( ! query . value ) { $ ( 'search-summary' ) . textContent = 'Results appear as you type.' ; return ; }
const request = searchRequest , activeEpoch = epoch , term = query . value ;
$ ( 'search-summary' ) . textContent = 'Searching…' ; $ ( 'cancel-search' ) . hidden = false ;
searchTimer = setTimeout ( async ( ) = > {
try {
const result = await invoke < SearchResults > ( 'search' , { query : term } ) ;
if ( request !== searchRequest || epoch !== activeEpoch ) return ;
const fragment = document . createDocumentFragment ( ) ;
for ( const hit of result . hits ) {
const row = button ( '' , 'result' ) ;
const location = document . createElement ( 'strong' ) ; location . textContent = ` ${ hit . path } : ${ hit . line } ` ;
const preview = document . createElement ( 'span' ) ; preview . textContent = hit . preview ;
row . append ( location , preview ) ; row . title = ` ${ hit . path } : ${ hit . line } : ${ hit . column } ` ;
row . onclick = ( ) = > { if ( epoch === activeEpoch ) void openFile ( hit . path , hit ) ; } ;
fragment . append ( row ) ;
}
$ ( 'results' ) . replaceChildren ( fragment ) ;
$ ( 'search-summary' ) . textContent = ` ${ result . hits . length } ${ result . truncated ? '+' : '' } matches ${ result . truncated ? ' — limit reached; narrow your search' : '' } ${ result . cancelled ? ' (cancelled)' : '' } ${ result . skipped_files ? ` · ${ result . skipped_files } unreadable, binary, oversized or unsupported files skipped ` : '' } ` ;
} catch ( error ) { if ( request === searchRequest ) $ ( 'search-summary' ) . textContent = errorText ( error ) ; }
finally { if ( request === searchRequest ) $ ( 'cancel-search' ) . hidden = true ; }
} , 250 ) ;
} ;
document . addEventListener ( 'keydown' , event = > {
if ( ( event . ctrlKey || event . metaKey ) && event . shiftKey && event . key . toLowerCase ( ) === 'f' ) { event . preventDefault ( ) ; switchPanel ( 'search' ) ; }
} ) ;
if ( ! isTauri ( ) ) {
status ( 'Browser preview only. Run npm run desktop to access local projects.' , true ) ;
$ ( 'workspace-button' ) . onclick = $ ( 'welcome-open' ) . onclick = ( ) = > status ( 'Folder access requires the desktop app: npm run desktop' , true ) ;
}
async function loadRecents() {
if ( ! isTauri ( ) ) { $ ( 'recent-status' ) . textContent = 'Open the desktop app to see recent projects.' ; return ; }
$ ( 'recent-status' ) . textContent = 'Loading recent projects…' ;
try {
const projects = await invoke < { root : string ; last_opened : number } [ ] > ( 'recent_projects' ) ;
$ ( 'recent-projects' ) . replaceChildren ( ) ;
for ( const project of projects ) {
const row = document . createElement ( 'div' ) ; row . className = 'recent-row' ;
const open = button ( '' , 'recent-project' ) ;
const name = document . createElement ( 'strong' ) ; name . textContent = project . root . split ( '/' ) . pop ( ) || project . root ;
const path = document . createElement ( 'span' ) ; path . textContent = project . root ;
const date = document . createElement ( 'small' ) ; date . textContent = ` Last opened ${ new Date ( project . last_opened * 1000 ) . toLocaleString ( ) } ` ;
open . append ( name , path , date ) ; open . title = project . root ;
2026-09-07 15:52:46 -04:00
open . onclick = ( ) = > void openRecent ( project . root ) ;
2026-09-07 15:16:52 -04:00
const remove = button ( '× ' , 'recent-remove' ) ; remove . setAttribute ( 'aria-label' , ` Remove ${ name . textContent } from recent projects ` ) ;
remove . title = 'Remove from history; project files are preserved' ;
remove . onclick = async ( ) = > {
remove . disabled = true ;
try { await invoke ( 'remove_recent' , { root : project.root } ) ; await loadRecents ( ) ; }
catch ( error ) { $ ( 'recent-status' ) . textContent = errorText ( error ) ; remove . disabled = false ; }
} ;
row . append ( open , remove ) ; $ ( 'recent-projects' ) . append ( row ) ;
}
$ ( 'recent-status' ) . textContent = projects . length ? 'Select a project to restore its saved build and last viewed file.' : 'No recent projects yet. Open a project to get started.' ;
} catch ( error ) { $ ( 'recent-status' ) . textContent = ` Could not load recent projects: ${ errorText ( error ) } ` ; }
}
$ ( 'recent-open' ) . onclick = ( ) = > $ ( 'workspace-button' ) . click ( ) ;
$ ( 'refresh-recents' ) . onclick = ( ) = > void loadRecents ( ) ;
void loadRecents ( ) ;
2026-09-07 15:52:46 -04:00
function closeFileMenu() { $ < HTMLDetailsElement > ( 'file-menu' ) . open = false ; }
async function openRecent ( root : string ) {
if ( closingWorkspace ) return ;
closeFileMenu ( ) ; showSetup ( ) ; source . value = root ; await inspectSource ( ) ;
if ( setup . open && inspection && ! inspection . warning && source . value === root ) $ < HTMLFormElement > ( 'setup-form' ) . requestSubmit ( ) ;
}
$ ( 'file-open' ) . onclick = ( ) = > {
if ( closingWorkspace ) return ;
closeFileMenu ( ) ; showSetup ( ) ; ++ inspecting ;
source . value = '' ; build . value = '' ; clangdPath . value = 'clangd' ; inspectedRoot = '' ; inspection = null ;
reset . checked = false ; reset . disabled = true ; $ ( 'saved-state' ) . textContent = '' ; $ ( 'setup-title' ) . textContent = 'Open project' ; source . focus ( ) ;
} ;
$ ( 'file-recents' ) . addEventListener ( 'toggle' , async ( ) = > {
if ( ! $ < HTMLDetailsElement > ( 'file-recents' ) . open ) return ;
$ ( 'file-recent-list' ) . textContent = 'Loading…' ;
try {
const projects = await invoke < { root :string ; last_opened :number } [ ] > ( 'recent_projects' ) ;
$ ( 'file-recent-list' ) . replaceChildren ( . . . projects . map ( project = > {
const item = button ( '' , 'file-recent' ) ;
const name = document . createElement ( 'strong' ) ; name . textContent = project . root . split ( '/' ) . pop ( ) || project . root ;
const path = document . createElement ( 'small' ) ; path . textContent = project . root ;
item . append ( name , path ) ; item . title = project . root ; item . onclick = ( ) = > void openRecent ( project . root ) ; return item ;
} ) ) ;
if ( ! projects . length ) $ ( 'file-recent-list' ) . textContent = 'No recent projects yet.' ;
} catch ( error ) { $ ( 'file-recent-list' ) . textContent = errorText ( error ) ; }
} ) ;
$ ( 'file-menu' ) . addEventListener ( 'toggle' , ( ) = > {
if ( $ < HTMLDetailsElement > ( 'file-menu' ) . open ) {
$ < HTMLButtonElement > ( 'file-welcome' ) . disabled = ! workspace || closingWorkspace ;
$ < HTMLDetailsElement > ( 'file-recents' ) . open = false ;
}
} ) ;
document . addEventListener ( 'pointerdown' , event = > { if ( ! $ ( 'file-menu' ) . contains ( event . target as Node ) ) closeFileMenu ( ) ; } ) ;
document . addEventListener ( 'keydown' , event = > {
if ( event . key === 'Escape' && $ < HTMLDetailsElement > ( 'file-menu' ) . open ) { closeFileMenu ( ) ; $ ( 'file-menu' ) . querySelector ( 'summary' ) ! . focus ( ) ; }
} ) ;
$ ( 'file-welcome' ) . onclick = async ( ) = > {
if ( ! workspace || closingWorkspace ) return ;
const root = workspace . root ;
closingWorkspace = true ; closeFileMenu ( ) ; ++ epoch ; ++ fileRequest ; ++ searchRequest ;
clearTimeout ( searchTimer ) ; graph . reset ( ) ; explanations . reset ( ) ;
status ( 'Closing project…' ) ;
try {
await rememberQueue ;
await invoke ( 'close_workspace' , { root } ) ;
workspace = null ; currentPath = '' ; viewStates . clear ( ) ; navigation . reset ( ) ; vim . reset ( ) ;
const model = editor . getModel ( ) ; editor . setModel ( null ) ; model ? . dispose ( ) ;
graph . reset ( ) ; $ < HTMLButtonElement > ( 'toggle-sidebar' ) . disabled = true ;
$ ( 'landing' ) . hidden = false ; document . querySelector ( 'main' ) ! . classList . add ( 'landing' ) ;
$ ( 'project-name' ) . textContent = 'No project open' ; $ ( 'project-name' ) . title = '' ;
$ ( 'workspace-button' ) . textContent = 'Open project…' ; $ ( 'file-path' ) . textContent = 'Welcome to CEX' ;
$ ( 'cursor' ) . textContent = '' ; $ ( 'tree' ) . replaceChildren ( ) ; $ ( 'results' ) . replaceChildren ( ) ; query . value = '' ;
status ( 'Ready' ) ; await loadRecents ( ) ; $ ( 'recent-open' ) . focus ( ) ;
} catch ( error ) { status ( ` Could not close project: ${ errorText ( error ) } ` , true ) ; }
finally { closingWorkspace = false ; }
} ;