function cexpApi() {
return window.LctxApi && window.LctxApi.apiFetch ? window.LctxApi.apiFetch : null;
}
function cexpEsc(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
return '&#' + c.charCodeAt(0) + ';';
});
}
function cexpKindClass(kind) {
var k = String(kind || '').toLowerCase();
if (k.indexOf('fn') !== -1 || k.indexOf('func') !== -1 || k.indexOf('method') !== -1) return 'sym-fn';
if (k.indexOf('struct') !== -1 || k.indexOf('class') !== -1 || k.indexOf('type') !== -1 || k.indexOf('enum') !== -1 || k.indexOf('interface') !== -1) return 'sym-type';
if (k.indexOf('const') !== -1 || k.indexOf('static') !== -1 || k.indexOf('var') !== -1) return 'sym-const';
if (k.indexOf('trait') !== -1 || k.indexOf('impl') !== -1) return 'sym-trait';
return 'sym-other';
}
class CockpitExplorer extends HTMLElement {
connectedCallback() {
if (this._wired) return;
this._wired = true;
this.innerHTML = '<div class="exp-wrap"><div class="exp-loading">Loading explorer…</div></div>';
}
async loadData() {
var fetchJson = cexpApi();
if (!fetchJson) { this._renderError('API client not loaded'); return; }
try {
this._data = await fetchJson('/api/tree', { timeoutMs: 20000 });
this._render();
} catch (e) {
this._renderError((e && e.error) || 'Failed to load explorer');
}
}
_renderError(msg) {
this.innerHTML = '<div class="exp-wrap"><div class="exp-error">' + cexpEsc(msg) + '</div></div>';
}
_render() {
var d = this._data || {};
var stats = (d.file_count || 0) + ' files \u00b7 ' + (d.symbol_count || 0) + ' symbols';
this.innerHTML =
'<div class="exp-wrap">' +
'<div class="exp-toolbar">' +
'<input type="text" class="exp-filter" placeholder="Filter files & symbols\u2026" spellcheck="false" autocomplete="off" aria-label="Filter files and symbols">' +
'<span class="exp-stats">' + cexpEsc(stats) + '</span>' +
'</div>' +
'<div class="exp-tree" id="expTree" role="tree" aria-label="Project files and symbols"></div>' +
'</div>';
this._treeEl = this.querySelector('#expTree');
this._renderTree();
var self = this;
var filter = this.querySelector('.exp-filter');
var deb = null;
filter.addEventListener('input', function () {
if (deb) clearTimeout(deb);
var q = filter.value;
deb = setTimeout(function () { self._applyFilter(q); }, 120);
});
this._treeEl.addEventListener('click', function (ev) {
var row = ev.target && ev.target.closest ? ev.target.closest('.exp-row') : null;
if (row) {
self._focusRow(row);
self._toggleRow(row);
}
});
this._treeEl.addEventListener('keydown', function (ev) { self._onTreeKeydown(ev); });
}
_visibleRows() {
var all = this._treeEl.querySelectorAll('.exp-row');
var out = [];
for (var i = 0; i < all.length; i++) {
var row = all[i];
var wrap = row.parentElement ? row.parentElement.parentElement : null;
var hidden = false;
while (wrap && wrap !== this._treeEl) {
if (wrap.classList && wrap.classList.contains('exp-children') &&
wrap.parentElement && wrap.parentElement.classList.contains('collapsed')) {
hidden = true;
break;
}
wrap = wrap.parentElement;
}
if (!hidden) out.push(row);
}
return out;
}
_focusRow(row) {
if (this._focused && this._focused !== row) this._focused.setAttribute('tabindex', '-1');
this._focused = row;
row.setAttribute('tabindex', '0');
row.focus();
}
_onTreeKeydown(ev) {
var row = ev.target && ev.target.closest ? ev.target.closest('.exp-row') : null;
if (!row) return;
var rows, idx;
switch (ev.key) {
case 'ArrowDown':
case 'ArrowUp':
rows = this._visibleRows();
idx = rows.indexOf(row) + (ev.key === 'ArrowDown' ? 1 : -1);
if (idx >= 0 && idx < rows.length) this._focusRow(rows[idx]);
break;
case 'ArrowRight':
if (row.getAttribute('aria-expanded') === 'false') {
this._toggleRow(row);
} else if (row.getAttribute('aria-expanded') === 'true') {
rows = this._visibleRows();
idx = rows.indexOf(row) + 1;
if (idx < rows.length) this._focusRow(rows[idx]);
}
break;
case 'ArrowLeft':
if (row.getAttribute('aria-expanded') === 'true') {
this._toggleRow(row);
} else {
var parentLi = row.closest('.exp-children') ?
row.closest('.exp-children').parentElement : null;
var parentRow = parentLi ? parentLi.querySelector(':scope > .exp-row') : null;
if (parentRow) this._focusRow(parentRow);
}
break;
case 'Enter':
case ' ':
this._toggleRow(row);
break;
case 'Home':
rows = this._visibleRows();
if (rows.length) this._focusRow(rows[0]);
break;
case 'End':
rows = this._visibleRows();
if (rows.length) this._focusRow(rows[rows.length - 1]);
break;
default:
return;
}
ev.preventDefault();
ev.stopPropagation();
}
_renderTree() {
this._treeEl.classList.remove('exp-filtered');
this._mountInto(this._treeEl, this._data.tree || []);
var first = this._treeEl.querySelector('.exp-row');
if (first) {
first.setAttribute('tabindex', '0');
this._focused = first;
}
}
_mountInto(containerEl, nodes) {
containerEl.innerHTML = this._listHtml(nodes);
var ul = containerEl.querySelector(':scope > .exp-list');
if (!ul) return;
var lis = ul.children;
for (var i = 0; i < lis.length && i < nodes.length; i++) lis[i]._node = nodes[i];
}
_listHtml(nodes) {
var html = '<ul class="exp-list" role="group">';
for (var i = 0; i < nodes.length; i++) {
var n = nodes[i];
if (n.type === 'dir') {
html +=
'<li class="exp-node exp-dir collapsed" role="none">' +
'<div class="exp-row" role="treeitem" tabindex="-1" aria-expanded="false" data-kind="dir" aria-label="' + cexpEsc(n.name) + ', directory, ' + (n.files || 0) + ' files">' +
'<span class="exp-caret" aria-hidden="true">\u25B8</span>' +
'<span class="exp-icon exp-dir-icon" aria-hidden="true">\uD83D\uDCC1</span>' +
'<span class="exp-name">' + cexpEsc(n.name) + '</span>' +
'<span class="exp-count">' + (n.files || 0) + '</span>' +
'</div>' +
'<div class="exp-children" data-lazy="dir" role="none"></div>' +
'</li>';
} else {
var hasSyms = (n.symbol_count || 0) > 0;
html +=
'<li class="exp-node exp-file collapsed" role="none">' +
'<div class="exp-row" role="treeitem" tabindex="-1"' + (hasSyms ? ' aria-expanded="false"' : '') + ' data-kind="file"' + (hasSyms ? '' : ' data-leaf="1"') + ' title="' + cexpEsc(n.path || n.name) + '" aria-label="' + cexpEsc(n.name) + ', file, ' + (n.symbol_count || 0) + ' symbols">' +
'<span class="exp-caret" aria-hidden="true">' + (hasSyms ? '\u25B8' : '') + '</span>' +
'<span class="exp-icon exp-file-icon" aria-hidden="true">' + cexpEsc(cexpLangBadge(n.language)) + '</span>' +
'<span class="exp-name">' + cexpEsc(n.name) + '</span>' +
'<span class="exp-count">' + (n.symbol_count || 0) + '</span>' +
'</div>' +
'<div class="exp-children" data-lazy="file" role="none"></div>' +
'</li>';
}
}
html += '</ul>';
return html;
}
_toggleRow(row) {
var li = row.parentElement;
if (!li) return;
var node = li._node;
if (!node) return;
if (row.getAttribute('data-leaf') === '1') return;
var childWrap = li.querySelector(':scope > .exp-children');
if (li.classList.contains('collapsed')) {
if (childWrap && !childWrap._rendered) {
childWrap._rendered = true;
if (node.type === 'dir') {
this._mountInto(childWrap, node.children || []);
} else {
childWrap.innerHTML = this._symbolsHtml(node.symbols || []);
}
}
li.classList.remove('collapsed');
row.setAttribute('aria-expanded', 'true');
} else {
li.classList.add('collapsed');
row.setAttribute('aria-expanded', 'false');
}
}
_symbolsHtml(symbols) {
if (!symbols.length) return '<div class="exp-empty">no symbols</div>';
var html = '<ul class="exp-syms" role="group">';
for (var i = 0; i < symbols.length; i++) {
var s = symbols[i];
html +=
'<li class="exp-sym ' + cexpKindClass(s.kind) + '" role="treeitem" aria-label="' + cexpEsc(s.name) + ', ' + cexpEsc(s.kind || 'symbol') + ', line ' + (s.line || 0) + '">' +
'<span class="exp-sym-kind">' + cexpEsc(s.kind || '?') + '</span>' +
'<span class="exp-sym-name">' + cexpEsc(s.name) + (s.exported ? ' <span class="exp-sym-exp">export</span>' : '') + '</span>' +
'<span class="exp-sym-line">:' + (s.line || 0) + '</span>' +
'</li>';
}
return html + '</ul>';
}
_applyFilter(query) {
var q = String(query || '').trim().toLowerCase();
if (!q) {
this._treeEl.classList.remove('exp-filtered');
this._renderTree();
return;
}
var matches = [];
var walk = function (nodes, dirPath) {
for (var i = 0; i < nodes.length; i++) {
var n = nodes[i];
if (n.type === 'dir') {
walk(n.children || [], dirPath ? dirPath + '/' + n.name : n.name);
} else {
var fileHit = n.name.toLowerCase().indexOf(q) !== -1 || (n.path || '').toLowerCase().indexOf(q) !== -1;
var symHits = (n.symbols || []).filter(function (s) { return s.name.toLowerCase().indexOf(q) !== -1; });
if (fileHit || symHits.length) {
matches.push({ file: n, syms: fileHit ? (n.symbols || []) : symHits });
}
}
}
};
walk(this._data.tree || [], '');
this._treeEl.classList.add('exp-filtered');
if (!matches.length) {
this._treeEl.innerHTML = '<div class="exp-empty">no matches for "' + cexpEsc(query) + '"</div>';
return;
}
var html = '<div class="exp-matches">';
for (var m = 0; m < matches.length && m < 200; m++) {
var f = matches[m].file;
html +=
'<div class="exp-match-file">' +
'<span class="exp-icon exp-file-icon">' + cexpEsc(cexpLangBadge(f.language)) + '</span>' +
'<span class="exp-name">' + cexpEsc(f.path || f.name) + '</span>' +
'<span class="exp-count">' + matches[m].syms.length + '</span>' +
'</div>';
if (matches[m].syms.length) html += this._symbolsHtml(matches[m].syms.slice(0, 40));
}
html += '</div>';
this._treeEl.innerHTML = html;
}
}
function cexpLangBadge(language) {
var l = String(language || '').toLowerCase();
var map = { rust: 'rs', typescript: 'ts', javascript: 'js', python: 'py', go: 'go', java: 'java', csharp: 'cs', cpp: 'c++', c: 'c', kotlin: 'kt', ruby: 'rb', php: 'php', swift: 'sw' };
return map[l] || (l ? l.slice(0, 3) : '\uD83D\uDCC4');
}
customElements.define('cockpit-explorer', CockpitExplorer);