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'; import { installSidebarResize } from './resize'; 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 $ = (id: string) => document.getElementById(id) as T; document.querySelector('#app')!.innerHTML = `
File
Open Recent
Loading…
cexCODE EXPLORER
No project open
{ cex }

Open Recent

Pick up where you left off, or explore a new project.

Recent projects

Loading recent projects…

History is stored in ~/.cex. Project settings stay with each project.
Welcome to CEXREAD ONLY
{ cex }

Explore the code.

Browse C and C++ source, fold sections,
and find text across your project.

Select a Meson or CMake build to enable definitions, references, and callers.
ReadyC / C++ · Read only

Open project

Choose the source directory. CEX stores its settings and caches in that directory’s .cex folder.

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.

Executable name on PATH or an absolute path. Changing it restarts analysis.

`; 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; let inspecting = 0; let inspectedRoot = ''; let inspection: Inspection | null = null; let currentPath = ''; const viewStates = new Map(); let rememberQueue: Promise = Promise.resolve(); const setup = $('setup'); const source = $('source'); const build = $('build'); const reset = $('reset'); const query = $('query'); const exclusions = $('exclusions'); const clangdPath = $('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); installSidebarResize(editor); 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; } let closingWorkspace = false; function showSetup() { if (closingWorkspace) return; 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('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('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('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'; $('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 { const entries = await invoke('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, 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; $('find-file').disabled = false; document.querySelectorAll('[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; $('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 { $('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('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; open.onclick = () => void openRecent(project.root); 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(); function closeFileMenu() { $('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) $('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 (!$('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 ($('file-menu').open) { $('file-welcome').disabled = !workspace || closingWorkspace; $('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' && $('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(); $('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; } };