Skip to content

Commit c7f44b8

Browse files
committed
support #asseteditor/filename/lineno
- Track selected asset in hash - Highlight select asset on navigation
1 parent 514820b commit c7f44b8

6 files changed

Lines changed: 67 additions & 12 deletions

File tree

css/ui.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,15 @@ div.asset_file_header {
497497
}
498498
div.asset_block {
499499
margin-top: 8px;
500+
transition: outline-color 1.5s ease-out;
501+
outline: 2px solid transparent;
502+
background-color: transparent;
503+
504+
}
505+
div.asset_block.asset_highlight {
506+
outline: 2px solid #99cc99;
507+
border-radius: 4px;
508+
transition: none;
500509
}
501510
.asset_snip {
502511
font-family: "Andale Mono", "Menlo", "Lucida Console", monospace;

src/ide/ui.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1696,6 +1696,8 @@ function replaceURLState() {
16961696
function hashToViewIdResolved(hash: string): string | null {
16971697
if (!hash || hash === '#') return null;
16981698
var id = decodeURIComponent(hash.substring(1));
1699+
// check for extended asset editor hash (e.g. #asseteditor/filename/startline)
1700+
if (id.startsWith('asseteditor/')) return '#asseteditor';
16991701
// check if it's a registered tool window (e.g. #asseteditor)
17001702
if (projectWindows.isWindow('#' + id)) return '#' + id;
17011703
// otherwise treat as a file path (e.g. hello.asm)
@@ -1719,6 +1721,7 @@ function installHashChangeHandler() {
17191721
function navigateToInitialHash(initialHash: string) {
17201722
if (initialHash && initialHash !== '#') {
17211723
const viewId = hashToViewIdResolved(initialHash) || getCurrentMainFilename();
1724+
history.replaceState(null, '', initialHash);
17221725
projectWindows.createOrShow(viewId);
17231726
}
17241727
}

src/ide/views/assetdecorations.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate, WidgetTy
22

33
// Asset header detection — shows a clickable badge on lines with ;;{json};; or /*{json}*/
44
class AssetHeaderWidget extends WidgetType {
5-
constructor(readonly header: string, readonly onClick: () => void) { super() }
5+
constructor(readonly header: string, readonly handleClick: (span: HTMLElement) => void) { super() }
66

77
toDOM() {
88
const span = document.createElement("span");
@@ -12,7 +12,7 @@ class AssetHeaderWidget extends WidgetType {
1212
span.addEventListener("click", (e) => {
1313
e.preventDefault();
1414
e.stopPropagation();
15-
this.onClick();
15+
this.handleClick(span);
1616
});
1717
return span;
1818
}
@@ -23,7 +23,7 @@ class AssetHeaderWidget extends WidgetType {
2323

2424
const assetHeaderRegex = /[/;][*;](\{.+?\})[*;][/;]/g;
2525

26-
function buildAssetHeaderDecorations(view: EditorView, onClick: () => void): DecorationSet {
26+
function buildAssetHeaderDecorations(view: EditorView, handleClick: (span: HTMLElement) => void): DecorationSet {
2727
const widgets: any[] = [];
2828
for (let { from, to } of view.visibleRanges) {
2929
const text = view.state.sliceDoc(from, to);
@@ -36,7 +36,7 @@ function buildAssetHeaderDecorations(view: EditorView, onClick: () => void): Dec
3636
const lineEnd = lineStart + line.length;
3737
widgets.push(
3838
Decoration.widget({
39-
widget: new AssetHeaderWidget(m[0], onClick),
39+
widget: new AssetHeaderWidget(m[0], handleClick),
4040
side: 1,
4141
}).range(lineEnd)
4242
);
@@ -47,15 +47,22 @@ function buildAssetHeaderDecorations(view: EditorView, onClick: () => void): Dec
4747
return Decoration.set(widgets, true);
4848
}
4949

50-
export function createAssetHeaderPlugin(onClick: () => void) {
50+
export function createAssetHeaderPlugin(onClick: (lineNumber: number) => void) {
51+
let currentView: EditorView;
52+
function handleClick(span: HTMLElement) {
53+
const pos = currentView.posAtDOM(span);
54+
onClick(currentView.state.doc.lineAt(pos).number);
55+
}
5156
return ViewPlugin.fromClass(class {
5257
decorations: DecorationSet;
5358
constructor(view: EditorView) {
54-
this.decorations = buildAssetHeaderDecorations(view, onClick);
59+
currentView = view;
60+
this.decorations = buildAssetHeaderDecorations(view, handleClick);
5561
}
5662
update(update: ViewUpdate) {
63+
currentView = update.view;
5764
if (update.docChanged || update.viewportChanged) {
58-
this.decorations = buildAssetHeaderDecorations(update.view, onClick);
65+
this.decorations = buildAssetHeaderDecorations(update.view, handleClick);
5966
}
6067
}
6168
}, {

src/ide/views/asseteditor.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext {
2222
cureditnode: pixed.PixNode;
2323
rootnodes: pixed.PixNode[];
2424
deferrednodes: pixed.PixNode[];
25-
2625
createDiv(parent: HTMLElement) {
2726
this.maindiv = newDiv(parent, "vertical-scroll");
2827
return this.maindiv[0];
@@ -134,6 +133,21 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext {
134133
node = node.left;
135134
}
136135
this.cureditnode = node;
136+
this.updateHashForSelection(div);
137+
}
138+
139+
updateHashForSelection(div: JQuery) {
140+
if (typeof window === 'undefined') return;
141+
var block = div ? div.closest('.asset_block[data-fileid]') : null;
142+
var fileid = block && block.attr('data-fileid');
143+
var startline = block && block.attr('data-startline');
144+
var hash = '#asseteditor';
145+
if (fileid && startline) {
146+
hash += '/' + encodeURIComponent(fileid) + '/' + startline;
147+
}
148+
if (window.location.hash !== hash) {
149+
history.replaceState(null, '', hash);
150+
}
137151
}
138152

139153
scanFileTextForAssets(id: string, data: string) {
@@ -330,7 +344,9 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext {
330344
} else if (typeof data === 'string') {
331345
let textfrags = this.scanFileTextForAssets(fileid, data);
332346
for (let frag of textfrags) {
333-
const block = $('<div class="asset_block"/>').appendTo(this.ensureFileDiv(fileid));
347+
const block = $('<div class="asset_block"/>')
348+
.attr({ 'data-fileid': fileid, 'data-startline': frag.startline })
349+
.appendTo(this.ensureFileDiv(fileid));
334350
var snip = $('<div class="asset_snip"/>').appendTo(block);
335351
var linenos = $('<span class="asset_linenos"/>').appendTo(snip);
336352
$('<span class="asset_lineno"/>').text(frag.startline).appendTo(linenos);
@@ -422,13 +438,32 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext {
422438
}
423439
});
424440
this.deferrednodes = [];
441+
this.scrollToAssetFromHash();
425442
} else {
426443
for (var node of this.rootnodes) {
427444
node.refreshRight();
428445
}
429446
}
430447
}
431448

449+
scrollToAssetFromHash(): void {
450+
var hash = window.location.hash;
451+
if (!hash || !hash.startsWith('#asseteditor/')) return;
452+
var parts = hash.substring(1).split('/'); // ['asseteditor', ...filename, startline]
453+
var fileid = decodeURIComponent(parts.slice(1, -1).join('/'));
454+
var startline = parts[parts.length - 1];
455+
if (!fileid || !startline) return;
456+
// defer to allow DOM to settle
457+
setTimeout(() => {
458+
const block = this.maindiv.find(`.asset_block[data-fileid="${fileid}"][data-startline="${startline}"]`);
459+
if (block.length) {
460+
block[0].scrollIntoView({ behavior: "smooth", block: "center" });
461+
block.addClass('asset_highlight');
462+
setTimeout(() => block.removeClass('asset_highlight'), 500);
463+
}
464+
}, 0);
465+
}
466+
432467
setVisible?(showing: boolean): void {
433468
// TODO: make into toolbar?
434469
if (showing) {

src/ide/views/editors.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,8 @@ export class SourceEditor implements ProjectView {
245245

246246
highlightLines.field,
247247

248-
createAssetHeaderPlugin(() => {
249-
projectWindows.createOrShow('#asseteditor');
248+
createAssetHeaderPlugin((lineNumber: number) => {
249+
window.location.hash = 'asseteditor/' + encodeURIComponent(this.path) + '/' + lineNumber;
250250
}),
251251

252252
textTransformFilterCompartment.of([]),

src/ide/windows.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ export class ProjectWindows {
7676
this.activeid = id;
7777
if (typeof window !== 'undefined') {
7878
const hash = id.startsWith('#') ? id : '#' + encodeURIComponent(id);
79-
if (window.location.hash !== hash) {
79+
// don't overwrite an extended hash (e.g. #asseteditor/file/line) with the base hash
80+
if (window.location.hash !== hash && !window.location.hash.startsWith(hash + '/')) {
8081
history.replaceState(null, '', hash);
8182
}
8283
}

0 commit comments

Comments
 (0)