return head + ' = ' + renderArg(fi.fiRhs, fi.fiTyVars);
}
+ // Render the argument list of a class/family/instance application as
+ // it would appear in source: space-separated, with each individual arg
+ // self-parenthesising when it's a multi-arg tycon (handled by
+ // renderArg). Multi-param classes thus print as `Foo a b`, not the
+ // tuple-shaped `Foo (a, b)`.
function renderArgsCompact(args, boundTvs) {
if (!args || args.length === 0) return '';
- if (args.length === 1) return renderArg(args[0], boundTvs);
- return '(' + args.map(a => renderArg(a, boundTvs)).join(', ') + ')';
+ return args.map(a => renderArg(a, boundTvs)).join(' ');
}
function renderArg(a, boundTvs) {
// ---------------------------------------------------------------------------
// Side panel
+ // Last node populated into the side panel — kept so settings changes
+ // (e.g. flipping the editor scheme) can re-render the panel without
+ // requiring the user to click the node again.
+ let lastSelectedTarget = null;
+
function showSelection(target) {
+ lastSelectedTarget = target || null;
const el = document.getElementById('selected');
if (!target) { el.innerHTML = '<p class="empty">No selection.</p>'; return; }
const data = target.data();
}
}
+ function refreshSelection() {
+ if (lastSelectedTarget) showSelection(lastSelectedTarget);
+ }
+
function panelButtons(cid, opts) {
// opts.canPin: whether this entity can be added to the focus set.
// opts.canMute: whether this entity can be muted.
return parts.length === 0 ? '' : `<div class="panel-actions">${parts.join('')}</div>`;
}
+ // Editor-link settings: persisted in localStorage. The user picks an
+ // editor scheme (vscode, cursor, idea, …) and an absolute "source root"
+ // prefix; we then turn each `Defined at` row into a clickable link
+ // that opens the file at the right line in that editor. Without a
+ // scheme set we just render plain text.
+ const EDITOR_SCHEME_KEY = 'classgraph.editorScheme';
+ const EDITOR_ROOT_KEY = 'classgraph.editorRoot';
+
+ function readEditorSettings() {
+ return {
+ scheme: localStorage.getItem(EDITOR_SCHEME_KEY) || '',
+ root: localStorage.getItem(EDITOR_ROOT_KEY) || '',
+ };
+ }
+
+ // Build the editor URL for the given file:line:col. Returns null when
+ // no scheme is configured (caller falls back to plain text).
+ function buildEditorUrl(file, line, col) {
+ const { scheme, root } = readEditorSettings();
+ if (!scheme || !file) return null;
+ let abs = file;
+ if (!/^([a-zA-Z]:)?\//.test(file) && root) {
+ // Relative path — prepend the configured root.
+ abs = root.replace(/\/+$/, '') + '/' + file.replace(/^\/+/, '');
+ }
+ const enc = encodeURI(abs);
+ switch (scheme) {
+ case 'vscode':
+ case 'vscode-insiders':
+ case 'cursor':
+ return `${scheme}://file${enc.startsWith('/') ? '' : '/'}${enc}:${line}:${col || 1}`;
+ case 'idea':
+ return `idea://open?file=${encodeURIComponent(abs)}&line=${line}`;
+ case 'txmt':
+ return `txmt://open?url=file://${enc}&line=${line}&column=${col || 1}`;
+ case 'file':
+ return `file://${enc.startsWith('/') ? '' : '/'}${enc}`;
+ default:
+ return null;
+ }
+ }
+
+ // Render a SrcSpanInfo as a `<dd>` cell — clickable when an editor
+ // scheme is configured, plain text otherwise.
+ function renderDefinedAt(src) {
+ if (!src) return '<dd><em>unknown</em></dd>';
+ const text = `${src.ssFile}:${src.ssStartLine}:${src.ssStartCol}`;
+ const url = buildEditorUrl(src.ssFile, src.ssStartLine, src.ssStartCol);
+ if (!url) return `<dd>${escape(text)}</dd>`;
+ return `<dd><a class="src-link" href="${escapeAttr(url)}" title="Open in your editor">${escape(text)}</a></dd>`;
+ }
+
// Render a Haddock comment as a simple block of paragraphs.
// Empty strings, plain whitespace, and missing values all collapse to "".
// We do NOT try to interpret Haddock markup (links, code blocks, etc.) —
c.ciAssocTypes.map(a => `<dd>${escape(a.qnName)}</dd>`).join('');
const meths = c.ciMethods.length === 0 ? '<dd><em>none</em></dd>' :
`<dd>${c.ciMethods.map(escape).join(', ')}</dd>`;
- const src = c.ciSrc
- ? `<dd>${escape(c.ciSrc.ssFile)}:${c.ciSrc.ssStartLine}:${c.ciSrc.ssStartCol}</dd>`
- : '<dd><em>unknown</em></dd>';
+ const src = renderDefinedAt(c.ciSrc);
const numInsts = (instancesByClass.get(cid) || []).length;
return `
<h2>${escape(c.ciName.qnName)}</h2>
.join('');
const flav = (typeof f.tfFlavor === 'string')
? f.tfFlavor : (f.tfFlavor.tag || JSON.stringify(f.tfFlavor));
+ const src = renderDefinedAt(f.tfSrc);
return `
<h2>${escape(f.tfName.qnName)}</h2>
<p class="pkgmod">${escape(f.tfName.qnPackage)} · ${escape(f.tfName.qnModule)}</p>
<dt>Flavor</dt><dd>${escape(String(flav))}</dd>
<dt>Type variables</dt><dd><ul>${tvs}</ul></dd>
<dt>Result kind</dt><dd>${escape(f.tfResultKind)}</dd>
+ <dt>Defined at</dt>${src}
</dl>`;
}
const tvs = inst.iiTyVars.length === 0 ? '<dd><em>none</em></dd>' :
`<dd><ul>${inst.iiTyVars.map(v =>
`<li>${escape(v.tvName)}<span style="color:#888"> :: ${escape(v.tvKind)}</span></li>`).join('')}</ul></dd>`;
- const src = inst.iiSrc
- ? `<dd>${escape(inst.iiSrc.ssFile)}:${inst.iiSrc.ssStartLine}:${inst.iiSrc.ssStartCol}</dd>`
- : '<dd><em>unknown</em></dd>';
+ const src = renderDefinedAt(inst.iiSrc);
return `
<h2>${head}</h2>
<p class="pkgmod">${escape(inst.iiClass.qnPackage)} · ${escape(inst.iiClass.qnModule)}</p>
</dl>`;
}
+ // Append a class/family's args to its name with a leading space, in
+ // applied form: `Foo a b`, not `Foo (a, b)`.
function renderArgsParens(subTvs, args) {
if (!args || args.length === 0) return '';
- return ' (' + args.map(a => renderArg(a, subTvs)).join(', ') + ')';
+ return ' ' + args.map(a => renderArg(a, subTvs)).join(' ');
}
function renderFamInstPanel(fi) {
const tvs = fi.fiTyVars.length === 0 ? '<dd><em>none</em></dd>' :
`<dd><ul>${fi.fiTyVars.map(v =>
`<li>${escape(v.tvName)}<span style="color:#888"> :: ${escape(v.tvKind)}</span></li>`).join('')}</ul></dd>`;
- const src = fi.fiSrc
- ? `<dd>${escape(fi.fiSrc.ssFile)}:${fi.fiSrc.ssStartLine}:${fi.fiSrc.ssStartCol}</dd>`
- : '<dd><em>unknown</em></dd>';
+ const src = renderDefinedAt(fi.fiSrc);
return `
<h2>${head}</h2>
<p class="pkgmod">${escape(fi.fiFamily.qnPackage)} · ${escape(fi.fiFamily.qnModule)}</p>
orphanToggle.addEventListener('change', applyOrphanMarker);
}
+ // Editor link settings: select an editor scheme and a source-root
+ // prefix, both persisted to localStorage. Changes refresh the
+ // currently-shown panel so links pick up the new values immediately.
+ (function initEditorSettings() {
+ const schemeSel = document.getElementById('editor-scheme');
+ const rootInput = document.getElementById('editor-root');
+ const summary = document.getElementById('editor-summary');
+ if (!schemeSel || !rootInput) return;
+ const { scheme, root } = readEditorSettings();
+ schemeSel.value = scheme;
+ rootInput.value = root;
+ updateEditorSummary();
+
+ schemeSel.addEventListener('change', () => {
+ localStorage.setItem(EDITOR_SCHEME_KEY, schemeSel.value);
+ updateEditorSummary();
+ refreshSelection();
+ });
+ // Re-render on input rather than blur so the user sees the link
+ // light up as they type the source root.
+ rootInput.addEventListener('input', () => {
+ localStorage.setItem(EDITOR_ROOT_KEY, rootInput.value);
+ updateEditorSummary();
+ refreshSelection();
+ });
+
+ function updateEditorSummary() {
+ if (!summary) return;
+ const s = schemeSel.value;
+ summary.textContent = s ? `(${s})` : '(off)';
+ }
+ })();
+
// -------------------------------------------------------------------------
// Side-panel event delegation: toggle Pin/Mute buttons, follow class links.