support #asseteditor/filename/lineno

- Track selected asset in hash
- Highlight selected asset on navigation
This commit is contained in:
Fred Sauer committed 2026-03-22 10:40:31 -07:00
1 parent 091bd25bd9
commit fb2b6ea9b6
6 files changed
+67 -12

No files matched your search

+9
View File
@@ -497,6 +497,15 @@ div.asset_file_header {
}
div.asset_block {
margin-top: 8px;
transition: outline-color 1.5s ease-out;
outline: 2px solid transparent;
background-color: transparent;
}
div.asset_block.asset_highlight {
outline: 2px solid #99cc99;
border-radius: 4px;
transition: none;
}
.asset_snip {
font-family: "Andale Mono", "Menlo", "Lucida Console", monospace;
+3
View File
@@ -1747,6 +1747,8 @@ function replaceURLState() {
function hashToViewIdResolved(hash: string): string | null {
if (!hash || hash === '#') return null;
var id = decodeURIComponent(hash.substring(1));
// check for extended asset editor hash (e.g. #asseteditor/filename/startline)
if (id.startsWith('asseteditor/')) return '#asseteditor';
// check if it's a registered tool window (e.g. #asseteditor)
if (projectWindows.isWindow('#' + id)) return '#' + id;
// otherwise treat as a file path (e.g. hello.asm)
@@ -1769,6 +1771,7 @@ function installHashChangeHandler() {
function navigateToInitialHash(initialHash: string) {
if (initialHash && initialHash !== '#') {
const viewId = hashToViewIdResolved(initialHash) || getCurrentMainFilename();
history.replaceState(null, '', initialHash);
projectWindows.createOrShow(viewId);
}
}
+14 -7
View File
@@ -2,7 +2,7 @@ import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate, WidgetTy
// Asset header detection — shows a clickable badge on lines with ;;{json};; or /*{json}*/
class AssetHeaderWidget extends WidgetType {
constructor(readonly header: string, readonly onClick: () => void) { super() }
constructor(readonly header: string, readonly handleClick: (span: HTMLElement) => void) { super() }
toDOM() {
const span = document.createElement("span");
@@ -12,7 +12,7 @@ class AssetHeaderWidget extends WidgetType {
span.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
this.onClick();
this.handleClick(span);
});
return span;
}
@@ -23,7 +23,7 @@ class AssetHeaderWidget extends WidgetType {
const assetHeaderRegex = /[/;][*;](\{.+?\})[*;][/;]/g;
function buildAssetHeaderDecorations(view: EditorView, onClick: () => void): DecorationSet {
function buildAssetHeaderDecorations(view: EditorView, handleClick: (span: HTMLElement) => void): DecorationSet {
const widgets: any[] = [];
for (let { from, to } of view.visibleRanges) {
const text = view.state.sliceDoc(from, to);
@@ -36,7 +36,7 @@ function buildAssetHeaderDecorations(view: EditorView, onClick: () => void): Dec
const lineEnd = lineStart + line.length;
widgets.push(
Decoration.widget({
widget: new AssetHeaderWidget(m[0], onClick),
widget: new AssetHeaderWidget(m[0], handleClick),
side: 1,
}).range(lineEnd)
);
@@ -47,15 +47,22 @@ function buildAssetHeaderDecorations(view: EditorView, onClick: () => void): Dec
return Decoration.set(widgets, true);
}
export function createAssetHeaderPlugin(onClick: () => void) {
export function createAssetHeaderPlugin(onClick: (lineNumber: number) => void) {
let currentView: EditorView;
function handleClick(span: HTMLElement) {
const pos = currentView.posAtDOM(span);
onClick(currentView.state.doc.lineAt(pos).number);
}
return ViewPlugin.fromClass(class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = buildAssetHeaderDecorations(view, onClick);
currentView = view;
this.decorations = buildAssetHeaderDecorations(view, handleClick);
}
update(update: ViewUpdate) {
currentView = update.view;
if (update.docChanged || update.viewportChanged) {
this.decorations = buildAssetHeaderDecorations(update.view, onClick);
this.decorations = buildAssetHeaderDecorations(update.view, handleClick);
}
}
}, {
+37 -2
View File
@@ -22,7 +22,6 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext {
cureditnode: pixed.PixNode;
rootnodes: pixed.PixNode[];
deferrednodes: pixed.PixNode[];
createDiv(parent: HTMLElement) {
this.maindiv = newDiv(parent, "vertical-scroll");
return this.maindiv[0];
@@ -134,6 +133,21 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext {
node = node.left;
}
this.cureditnode = node;
this.updateHashForSelection(div);
}
updateHashForSelection(div: JQuery) {
if (typeof window === 'undefined') return;
var block = div ? div.closest('.asset_block[data-fileid]') : null;
var fileid = block && block.attr('data-fileid');
var startline = block && block.attr('data-startline');
var hash = '#asseteditor';
if (fileid && startline) {
hash += '/' + encodeURIComponent(fileid) + '/' + startline;
}
if (window.location.hash !== hash) {
history.replaceState(null, '', hash);
}
}
scanFileTextForAssets(id: string, data: string) {
@@ -330,7 +344,9 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext {
} else if (typeof data === 'string') {
let textfrags = this.scanFileTextForAssets(fileid, data);
for (let frag of textfrags) {
const block = $('<div class="asset_block"/>').appendTo(this.ensureFileDiv(fileid));
const block = $('<div class="asset_block"/>')
.attr({ 'data-fileid': fileid, 'data-startline': frag.startline })
.appendTo(this.ensureFileDiv(fileid));
var snip = $('<div class="asset_snip"/>').appendTo(block);
var linenos = $('<span class="asset_linenos"/>').appendTo(snip);
$('<span class="asset_lineno"/>').text(frag.startline).appendTo(linenos);
@@ -422,6 +438,7 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext {
}
});
this.deferrednodes = [];
this.scrollToAssetFromHash();
} else {
for (var node of this.rootnodes) {
node.refreshRight();
@@ -429,6 +446,24 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext {
}
}
scrollToAssetFromHash(): void {
var hash = window.location.hash;
if (!hash || !hash.startsWith('#asseteditor/')) return;
var parts = hash.substring(1).split('/'); // ['asseteditor', ...filename, startline]
var fileid = decodeURIComponent(parts.slice(1, -1).join('/'));
var startline = parts[parts.length - 1];
if (!fileid || !startline) return;
// defer to allow DOM to settle
setTimeout(() => {
const block = this.maindiv.find(`.asset_block[data-fileid="${fileid}"][data-startline="${startline}"]`);
if (block.length) {
block[0].scrollIntoView({ behavior: "smooth", block: "center" });
block.addClass('asset_highlight');
setTimeout(() => block.removeClass('asset_highlight'), 500);
}
}, 0);
}
setVisible?(showing: boolean): void {
// TODO: make into toolbar?
if (showing) {
+2 -2
View File
@@ -245,8 +245,8 @@ export class SourceEditor implements ProjectView {
highlightLines.field,
createAssetHeaderPlugin(() => {
projectWindows.createOrShow('#asseteditor');
createAssetHeaderPlugin((lineNumber: number) => {
window.location.hash = 'asseteditor/' + encodeURIComponent(this.path) + '/' + lineNumber;
}),
textTransformFilterCompartment.of([]),
+2 -1
View File
@@ -83,7 +83,8 @@ export class ProjectWindows {
}
} else {
const hash = id.startsWith('#') ? id : '#' + encodeURIComponent(id);
if (window.location.hash !== hash) {
// don't overwrite an extended hash (e.g. #asseteditor/file/line) with the base hash
if (window.location.hash !== hash && !window.location.hash.startsWith(hash + '/')) {
history.replaceState(null, '', hash);
}
}