The extension shows Find All References results in a syntax-highlighted virtual document. Two things were wrong and roadmap item 1 was missing. Fixes, all reproduced against a real C# solution with DotRush: - Navigation and hover were dead on every result. setTextDocumentLanguage() closes and re-opens the document under the same URI, which fired onDidCloseTextDocument and dropped the results from the store. The text still rendered because VS Code caches the model, so the pane looked correct while Enter / F12 / Ctrl+Click / hover all returned nothing. The virtual URI now carries the source file's extension so VS Code infers the language without recreating the document, and a close only discards results once no tab or document for that URI remains. - reuseTab never reused: the symbol name is part of the URI, so every new symbol opened another tab. The previous results tab is now closed and its group taken over. - The reference count in the title double-counted when two language servers answer the same request (DotRush plus C# Dev Kit). It now comes from the de-duplicated set. - A results tab hidden behind another editor was duplicated into a new group instead of being revealed. - Decorations are reapplied on active-editor change; a results tab restored from a previous window is closed instead of left as a dead empty document; searching with no symbol under the cursor no longer searches for "symbol". Roadmap item 1 - webview results panel: - Shared gathering, grouping and rendering moved to references.ts so both views work from the same data. - panel.ts plus media/ render the results as a table with resizable, sortable Code / File / Line / Project / Containing member columns, collapsible per-file groups, a filter box, keyboard navigation, and single-click preview versus Enter to jump. Column widths default to a share of the panel width until dragged. - Containing member comes from executeDocumentSymbolProvider, nested types included. - coloredReferences.view selects the default view; toggleView switches the current results between the two. Adds an integration suite that launches a real VS Code against a C# solution and asserts on the rendered output, including that every displayed line maps back to the source line it claims. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
634 lines
23 KiB
JavaScript
634 lines
23 KiB
JavaScript
// @ts-check
|
|
/**
|
|
* Colored References — results panel.
|
|
*
|
|
* Renders reference results as a table with resizable, sortable columns.
|
|
* All state (sort order, grouping, column widths, filter) is round-tripped through
|
|
* the webview state so it survives the panel being hidden and restored.
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
const vscode = acquireVsCodeApi();
|
|
|
|
// `share` is the fraction of the panel a column gets before anyone drags it.
|
|
const COLUMNS = [
|
|
{ key: 'code', label: 'Code', share: 0.40, min: 120, align: 'left' },
|
|
{ key: 'file', label: 'File', share: 0.20, min: 80, align: 'left' },
|
|
{ key: 'line', label: 'Line', share: 0.06, min: 44, align: 'right' },
|
|
{ key: 'project', label: 'Project', share: 0.14, min: 60, align: 'left' },
|
|
{ key: 'member', label: 'Containing member', share: 0.20, min: 80, align: 'left' },
|
|
];
|
|
|
|
/** @type {{symbol: string, languageId: string, rows: any[], fileCount: number}} */
|
|
let data = { symbol: '', languageId: '', rows: [], fileCount: 0 };
|
|
|
|
const restored = vscode.getState() || {};
|
|
let state = {
|
|
sortKey: restored.sortKey || 'file',
|
|
sortDir: restored.sortDir === 'desc' ? 'desc' : 'asc',
|
|
group: restored.group !== false,
|
|
filter: restored.filter || '',
|
|
widths: Object.assign({}, restored.widths),
|
|
collapsed: Object.assign({}, restored.collapsed),
|
|
selectedId: restored.selectedId,
|
|
};
|
|
|
|
const el = {
|
|
summary: /** @type {HTMLElement} */ (document.getElementById('summary')),
|
|
filter: /** @type {HTMLInputElement} */ (document.getElementById('filter')),
|
|
group: /** @type {HTMLButtonElement} */ (document.getElementById('toggle-group')),
|
|
refresh: /** @type {HTMLButtonElement} */ (document.getElementById('refresh')),
|
|
table: /** @type {HTMLElement} */ (document.getElementById('table')),
|
|
head: /** @type {HTMLElement} */ (document.getElementById('head')),
|
|
body: /** @type {HTMLElement} */ (document.getElementById('body')),
|
|
empty: /** @type {HTMLElement} */ (document.getElementById('empty')),
|
|
};
|
|
|
|
function saveState() {
|
|
vscode.setState(state);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Theme-aware token palette
|
|
// -----------------------------------------------------------------------
|
|
|
|
// Token colours are not exposed to webviews as CSS variables, so approximate the
|
|
// stock Dark+/Light+ hues. Roadmap item 2 (semantic tokens) can replace this.
|
|
const PALETTES = {
|
|
dark: {
|
|
'fg-default': '#d4d4d4', comment: '#6a9955', string: '#ce9178',
|
|
keyword: '#569cd6', control: '#c586c0', number: '#b5cea8',
|
|
type: '#4ec9b0', call: '#dcdcaa',
|
|
},
|
|
light: {
|
|
'fg-default': '#000000', comment: '#008000', string: '#a31515',
|
|
keyword: '#0000ff', control: '#af00db', number: '#098658',
|
|
type: '#267f99', call: '#795e26',
|
|
},
|
|
contrast: {
|
|
'fg-default': '#ffffff', comment: '#7ca668', string: '#ce9178',
|
|
keyword: '#569cd6', control: '#c586c0', number: '#b5cea8',
|
|
type: '#4ec9b0', call: '#dcdcaa',
|
|
},
|
|
};
|
|
|
|
function applyPalette() {
|
|
const cls = document.body.className;
|
|
const palette = cls.indexOf('vscode-high-contrast-light') >= 0 ? PALETTES.light
|
|
: cls.indexOf('vscode-high-contrast') >= 0 ? PALETTES.contrast
|
|
: cls.indexOf('vscode-light') >= 0 ? PALETTES.light
|
|
: PALETTES.dark;
|
|
for (const name of Object.keys(palette)) {
|
|
document.documentElement.style.setProperty('--cr-' + name, palette[name]);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Tiny tokenizer, good enough for one-line snippets
|
|
// -----------------------------------------------------------------------
|
|
|
|
const KEYWORDS = new Set((
|
|
'abstract as async await base bool byte char class const decimal default delegate double dynamic ' +
|
|
'enum event explicit extern false fixed float implicit in init int interface internal is let lock ' +
|
|
'long nameof namespace new null object operator out override params partial private protected public ' +
|
|
'readonly record ref sbyte sealed short sizeof stackalloc static string struct this true typeof uint ' +
|
|
'ulong unchecked unsafe ushort using value var virtual void volatile where with ' +
|
|
'function const let type def fn impl mut pub struct trait unsigned auto extends implements ' +
|
|
'export import from declare any number boolean unknown never'
|
|
).split(' '));
|
|
|
|
const CONTROL = new Set((
|
|
'break case catch continue do else finally for foreach goto if return switch throw try while yield ' +
|
|
'match loop elif except raise pass'
|
|
).split(' '));
|
|
|
|
const TOKEN = new RegExp([
|
|
'(\\/\\/[^\\n]*|#[^\\n]*|--[^\\n]*|\\/\\*.*?(?:\\*\\/|$))', // 1 comment
|
|
'(@?"(?:[^"\\\\]|\\\\.)*"?|\'(?:[^\'\\\\]|\\\\.)*\'?|`(?:[^`\\\\]|\\\\.)*`?)', // 2 string
|
|
'(\\b\\d[\\w.]*\\b)', // 3 number
|
|
'([A-Za-z_$][\\w$]*)', // 4 word
|
|
'(\\s+)', // 5 space
|
|
'([^\\w\\s])', // 6 punctuation
|
|
].join('|'), 'gs');
|
|
|
|
/**
|
|
* Splits a line into [className, text] pairs.
|
|
* @param {string} text
|
|
* @returns {[string, string][]}
|
|
*/
|
|
function tokenize(text) {
|
|
/** @type {[string, string][]} */
|
|
const out = [];
|
|
TOKEN.lastIndex = 0;
|
|
let match;
|
|
while ((match = TOKEN.exec(text)) !== null) {
|
|
const [all, comment, str, num, word, space, punct] = match;
|
|
if (comment) {
|
|
out.push(['tok-comment', all]);
|
|
} else if (str) {
|
|
out.push(['tok-string', all]);
|
|
} else if (num) {
|
|
out.push(['tok-number', all]);
|
|
} else if (word) {
|
|
const next = text[TOKEN.lastIndex];
|
|
out.push([
|
|
KEYWORDS.has(word) ? 'tok-keyword'
|
|
: CONTROL.has(word) ? 'tok-control'
|
|
: next === '(' ? 'tok-call'
|
|
: /^[A-Z]/.test(word) ? 'tok-type'
|
|
: '',
|
|
all,
|
|
]);
|
|
} else if (space) {
|
|
out.push(['', all]);
|
|
} else if (punct) {
|
|
out.push(['tok-punct', all]);
|
|
} else {
|
|
out.push(['', all]);
|
|
}
|
|
if (match.index === TOKEN.lastIndex) {
|
|
TOKEN.lastIndex++; // never spin on a zero-width match
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Renders a code line: tokenized, with the referenced symbol boxed.
|
|
* @param {string} text
|
|
* @param {[number, number][]} hits
|
|
*/
|
|
function renderCode(text, hits) {
|
|
const fragment = document.createDocumentFragment();
|
|
// Split token boundaries on hit boundaries so a hit never straddles two spans.
|
|
const cuts = new Set([0, text.length]);
|
|
for (const [start, end] of hits || []) {
|
|
cuts.add(start);
|
|
cuts.add(end);
|
|
}
|
|
let offset = 0;
|
|
for (const [cls, piece] of tokenize(text)) {
|
|
let from = offset;
|
|
const to = offset + piece.length;
|
|
const inner = [...cuts].filter(c => c > from && c < to).sort((a, b) => a - b);
|
|
for (const cut of inner.concat([to])) {
|
|
appendPiece(fragment, text.slice(from, cut), cls, from, hits || []);
|
|
from = cut;
|
|
}
|
|
offset = to;
|
|
}
|
|
return fragment;
|
|
}
|
|
|
|
function appendPiece(parent, text, cls, start, hits) {
|
|
if (!text) {
|
|
return;
|
|
}
|
|
const span = document.createElement('span');
|
|
if (cls) {
|
|
span.className = cls;
|
|
}
|
|
span.textContent = text;
|
|
const inHit = hits.some(([s, e]) => start >= s && start < e);
|
|
if (inHit) {
|
|
const box = document.createElement('span');
|
|
box.className = 'hit';
|
|
box.appendChild(span);
|
|
parent.appendChild(box);
|
|
} else {
|
|
parent.appendChild(span);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Columns
|
|
// -----------------------------------------------------------------------
|
|
|
|
/** Widths derived from the panel width, used until the user drags a column. */
|
|
let autoWidths = {};
|
|
|
|
function computeAutoWidths() {
|
|
const available = el.table.clientWidth || 900;
|
|
autoWidths = {};
|
|
for (const column of COLUMNS) {
|
|
autoWidths[column.key] = Math.max(column.min, Math.round(available * column.share));
|
|
}
|
|
}
|
|
|
|
function width(column) {
|
|
const stored = state.widths[column.key];
|
|
return typeof stored === 'number' ? stored : autoWidths[column.key] ?? column.min;
|
|
}
|
|
|
|
function applyGrid() {
|
|
// Every column is a fixed track so its edge can be dragged; a trailing filler
|
|
// track soaks up the slack so rows still span the full width. When the fixed
|
|
// tracks are wider than the panel, #table scrolls the header and rows together.
|
|
const template = COLUMNS.map(c => `${width(c)}px`).join(' ') + ' minmax(0, 1fr)';
|
|
document.documentElement.style.setProperty('--cr-grid', template);
|
|
}
|
|
|
|
function customized() {
|
|
return Object.keys(state.widths).length > 0;
|
|
}
|
|
|
|
function buildHead() {
|
|
el.head.textContent = '';
|
|
for (const column of COLUMNS) {
|
|
const th = document.createElement('div');
|
|
th.className = 'th';
|
|
th.dataset.key = column.key;
|
|
th.tabIndex = 0;
|
|
th.setAttribute('role', 'columnheader');
|
|
th.title = `Sort by ${column.label}`;
|
|
|
|
const label = document.createElement('span');
|
|
label.className = 'label';
|
|
label.textContent = column.label;
|
|
|
|
const arrow = document.createElement('span');
|
|
arrow.className = 'arrow';
|
|
arrow.textContent = state.sortDir === 'desc' ? '▼' : '▲';
|
|
|
|
if (state.sortKey === column.key) {
|
|
th.dataset.sorted = state.sortDir;
|
|
th.setAttribute('aria-sort', state.sortDir === 'desc' ? 'descending' : 'ascending');
|
|
}
|
|
if (column.align === 'right') {
|
|
th.style.justifyContent = 'flex-end';
|
|
}
|
|
|
|
th.appendChild(label);
|
|
th.appendChild(arrow);
|
|
|
|
const grip = document.createElement('div');
|
|
grip.className = 'grip';
|
|
grip.addEventListener('mousedown', event => startResize(event, column));
|
|
grip.addEventListener('dblclick', event => {
|
|
event.stopPropagation();
|
|
delete state.widths[column.key];
|
|
saveState();
|
|
applyGrid();
|
|
});
|
|
th.appendChild(grip);
|
|
|
|
th.addEventListener('click', () => sortBy(column.key));
|
|
th.addEventListener('keydown', event => {
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault();
|
|
sortBy(column.key);
|
|
}
|
|
});
|
|
el.head.appendChild(th);
|
|
}
|
|
}
|
|
|
|
function startResize(event, column) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const startX = event.clientX;
|
|
const startWidth = width(column);
|
|
document.body.classList.add('resizing');
|
|
|
|
function move(e) {
|
|
state.widths[column.key] = Math.max(column.min, Math.round(startWidth + (e.clientX - startX)));
|
|
applyGrid();
|
|
}
|
|
function up() {
|
|
document.removeEventListener('mousemove', move);
|
|
document.removeEventListener('mouseup', up);
|
|
document.body.classList.remove('resizing');
|
|
saveState();
|
|
}
|
|
document.addEventListener('mousemove', move);
|
|
document.addEventListener('mouseup', up);
|
|
}
|
|
|
|
function sortBy(key) {
|
|
if (state.sortKey === key) {
|
|
state.sortDir = state.sortDir === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
state.sortKey = key;
|
|
state.sortDir = 'asc';
|
|
}
|
|
saveState();
|
|
buildHead();
|
|
renderRows();
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Rows
|
|
// -----------------------------------------------------------------------
|
|
|
|
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
|
|
|
|
function compare(a, b) {
|
|
const key = state.sortKey;
|
|
let result;
|
|
if (key === 'line') {
|
|
result = a.line - b.line;
|
|
} else if (key === 'file') {
|
|
result = collator.compare(a.relPath, b.relPath) || (a.line - b.line);
|
|
} else {
|
|
result = collator.compare(a[key] || '', b[key] || '');
|
|
}
|
|
if (result === 0) {
|
|
// Stable, predictable secondary order.
|
|
result = collator.compare(a.relPath, b.relPath) || (a.line - b.line) || (a.col - b.col);
|
|
}
|
|
return state.sortDir === 'desc' ? -result : result;
|
|
}
|
|
|
|
function matches(row, needle) {
|
|
if (!needle) {
|
|
return true;
|
|
}
|
|
return (row.code + ' ' + row.relPath + ' ' + row.project + ' ' + row.member)
|
|
.toLowerCase().indexOf(needle) >= 0;
|
|
}
|
|
|
|
function visibleRows() {
|
|
const needle = state.filter.trim().toLowerCase();
|
|
return data.rows.filter(r => matches(r, needle)).sort(compare);
|
|
}
|
|
|
|
function cell(className, text) {
|
|
const div = document.createElement('div');
|
|
div.className = 'td ' + className;
|
|
if (text !== undefined) {
|
|
div.textContent = text;
|
|
}
|
|
return div;
|
|
}
|
|
|
|
function rowElement(row) {
|
|
const div = document.createElement('div');
|
|
div.className = 'row' + (row.id === state.selectedId ? ' selected' : '');
|
|
div.dataset.id = String(row.id);
|
|
div.setAttribute('role', 'row');
|
|
|
|
const code = cell('code');
|
|
code.appendChild(renderCode(row.code, row.hits));
|
|
code.title = row.code;
|
|
div.appendChild(code);
|
|
|
|
const file = cell('file');
|
|
const name = document.createElement('span');
|
|
name.className = 'name';
|
|
name.textContent = row.file;
|
|
file.appendChild(name);
|
|
if (row.dir) {
|
|
file.appendChild(document.createTextNode(' ' + row.dir));
|
|
}
|
|
file.title = row.relPath;
|
|
div.appendChild(file);
|
|
|
|
div.appendChild(cell('line', String(row.line)));
|
|
div.appendChild(cell('project', row.project));
|
|
|
|
const member = cell('member', row.member);
|
|
member.title = row.member;
|
|
div.appendChild(member);
|
|
|
|
return div;
|
|
}
|
|
|
|
function groupElement(relPath, project, count, collapsed) {
|
|
const div = document.createElement('div');
|
|
div.className = 'group' + (collapsed ? ' collapsed' : '');
|
|
div.dataset.group = relPath;
|
|
|
|
const twisty = document.createElement('span');
|
|
twisty.className = 'twisty';
|
|
twisty.textContent = collapsed ? '▶' : '▼';
|
|
div.appendChild(twisty);
|
|
|
|
if (project) {
|
|
const tag = document.createElement('span');
|
|
tag.className = 'project';
|
|
tag.textContent = '[' + project + ']';
|
|
div.appendChild(tag);
|
|
}
|
|
|
|
const label = document.createElement('span');
|
|
label.textContent = relPath;
|
|
div.appendChild(label);
|
|
|
|
const badge = document.createElement('span');
|
|
badge.className = 'count';
|
|
badge.textContent = '(' + count + ')';
|
|
div.appendChild(badge);
|
|
|
|
div.title = 'Click to collapse, double-click to open the file';
|
|
return div;
|
|
}
|
|
|
|
function renderRows() {
|
|
const rows = visibleRows();
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
if (state.group) {
|
|
/** @type {Map<string, any[]>} */
|
|
const groups = new Map();
|
|
for (const row of rows) {
|
|
const list = groups.get(row.relPath);
|
|
if (list) {
|
|
list.push(row);
|
|
} else {
|
|
groups.set(row.relPath, [row]);
|
|
}
|
|
}
|
|
for (const [relPath, list] of groups) {
|
|
const collapsed = !!state.collapsed[relPath];
|
|
fragment.appendChild(groupElement(relPath, list[0].project, list.length, collapsed));
|
|
if (!collapsed) {
|
|
for (const row of list) {
|
|
fragment.appendChild(rowElement(row));
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
for (const row of rows) {
|
|
fragment.appendChild(rowElement(row));
|
|
}
|
|
}
|
|
|
|
el.body.textContent = '';
|
|
el.body.appendChild(fragment);
|
|
el.empty.hidden = rows.length > 0;
|
|
el.empty.textContent = data.rows.length === 0
|
|
? 'No references.'
|
|
: `No result matches “${state.filter}”.`;
|
|
|
|
const shown = rows.length;
|
|
const total = data.rows.length;
|
|
el.summary.textContent = '';
|
|
const strong = document.createElement('b');
|
|
strong.textContent = data.symbol;
|
|
el.summary.appendChild(document.createTextNode(shown === total ? '' : `${shown} of `));
|
|
el.summary.appendChild(document.createTextNode(`${total} reference${total === 1 ? '' : 's'} to `));
|
|
el.summary.appendChild(strong);
|
|
el.summary.appendChild(document.createTextNode(
|
|
` in ${data.fileCount} file${data.fileCount === 1 ? '' : 's'}`));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Interaction
|
|
// -----------------------------------------------------------------------
|
|
|
|
function select(rowDiv, reveal) {
|
|
const previous = el.body.querySelector('.row.selected');
|
|
if (previous) {
|
|
previous.classList.remove('selected');
|
|
}
|
|
if (!rowDiv) {
|
|
state.selectedId = undefined;
|
|
saveState();
|
|
return;
|
|
}
|
|
rowDiv.classList.add('selected');
|
|
state.selectedId = Number(rowDiv.dataset.id);
|
|
saveState();
|
|
if (reveal) {
|
|
rowDiv.scrollIntoView({ block: 'nearest' });
|
|
}
|
|
}
|
|
|
|
function open(id, preview) {
|
|
vscode.postMessage({ type: 'open', id: Number(id), preview: !!preview });
|
|
}
|
|
|
|
el.body.addEventListener('click', event => {
|
|
const target = /** @type {HTMLElement} */ (event.target);
|
|
const group = target.closest('.group');
|
|
if (group) {
|
|
const key = group.dataset.group;
|
|
state.collapsed[key] = !state.collapsed[key];
|
|
saveState();
|
|
renderRows();
|
|
return;
|
|
}
|
|
const row = target.closest('.row');
|
|
if (row) {
|
|
select(row, false);
|
|
open(row.dataset.id, true);
|
|
}
|
|
});
|
|
|
|
el.body.addEventListener('dblclick', event => {
|
|
const target = /** @type {HTMLElement} */ (event.target);
|
|
const group = target.closest('.group');
|
|
if (group) {
|
|
vscode.postMessage({ type: 'openFile', relPath: group.dataset.group });
|
|
return;
|
|
}
|
|
const row = target.closest('.row');
|
|
if (row) {
|
|
open(row.dataset.id, false);
|
|
}
|
|
});
|
|
|
|
el.body.addEventListener('keydown', event => {
|
|
const rows = [...el.body.querySelectorAll('.row')];
|
|
if (rows.length === 0) {
|
|
return;
|
|
}
|
|
const current = el.body.querySelector('.row.selected');
|
|
let index = current ? rows.indexOf(current) : -1;
|
|
|
|
switch (event.key) {
|
|
case 'ArrowDown': index = Math.min(rows.length - 1, index + 1); break;
|
|
case 'ArrowUp': index = Math.max(0, index <= 0 ? 0 : index - 1); break;
|
|
case 'Home': index = 0; break;
|
|
case 'End': index = rows.length - 1; break;
|
|
case 'PageDown': index = Math.min(rows.length - 1, index + 15); break;
|
|
case 'PageUp': index = Math.max(0, index - 15); break;
|
|
case 'Enter':
|
|
if (current) {
|
|
open(current.dataset.id, false);
|
|
}
|
|
event.preventDefault();
|
|
return;
|
|
default:
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
const next = /** @type {HTMLElement} */ (rows[index]);
|
|
select(next, true);
|
|
open(next.dataset.id, true);
|
|
});
|
|
|
|
el.filter.addEventListener('input', () => {
|
|
state.filter = el.filter.value;
|
|
saveState();
|
|
renderRows();
|
|
});
|
|
|
|
el.group.addEventListener('click', () => {
|
|
state.group = !state.group;
|
|
el.group.setAttribute('aria-pressed', String(state.group));
|
|
saveState();
|
|
renderRows();
|
|
});
|
|
|
|
el.refresh.addEventListener('click', () => vscode.postMessage({ type: 'refresh' }));
|
|
|
|
document.addEventListener('keydown', event => {
|
|
if ((event.ctrlKey || event.metaKey) && event.key === 'f') {
|
|
event.preventDefault();
|
|
el.filter.focus();
|
|
el.filter.select();
|
|
} else if (event.key === 'Escape' && document.activeElement === el.filter) {
|
|
el.filter.value = '';
|
|
state.filter = '';
|
|
saveState();
|
|
renderRows();
|
|
el.body.focus();
|
|
} else if (event.key === 'F5') {
|
|
event.preventDefault();
|
|
vscode.postMessage({ type: 'refresh' });
|
|
}
|
|
});
|
|
|
|
new MutationObserver(applyPalette).observe(document.body, {
|
|
attributes: true, attributeFilter: ['class'],
|
|
});
|
|
|
|
window.addEventListener('message', event => {
|
|
const message = event.data;
|
|
if (message.type === 'results') {
|
|
const newSymbol = message.symbol !== data.symbol;
|
|
data = message;
|
|
// Row ids are positional, so a selection carried over from another symbol
|
|
// would point at an unrelated reference.
|
|
if (newSymbol || !data.rows.some(r => r.id === state.selectedId)) {
|
|
state.selectedId = data.rows.length > 0 ? data.rows[0].id : undefined;
|
|
}
|
|
renderRows();
|
|
el.body.focus();
|
|
}
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Boot
|
|
// -----------------------------------------------------------------------
|
|
|
|
window.addEventListener('resize', () => {
|
|
if (!customized()) {
|
|
computeAutoWidths();
|
|
applyGrid();
|
|
}
|
|
});
|
|
|
|
applyPalette();
|
|
el.filter.value = state.filter;
|
|
el.group.setAttribute('aria-pressed', String(state.group));
|
|
el.body.tabIndex = 0;
|
|
computeAutoWidths();
|
|
applyGrid();
|
|
buildHead();
|
|
renderRows();
|
|
vscode.postMessage({ type: 'ready' });
|
|
})();
|