mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-25 00:02:16 +00:00
search: allow search of symbols w/o source code
This commit is contained in:
1 parent
242cb71fcf
commit
fb3fb958b7
6 files changed
+260
-20
No files matched your search
@@ -12,7 +12,7 @@ export type SymbolKind =
|
||||
| 'doc'
|
||||
| 'text';
|
||||
|
||||
export type SymbolSourceKind = 'project' | 'toolchain' | 'docs';
|
||||
export type SymbolSourceKind = 'project' | 'toolchain' | 'docs' | 'debugger';
|
||||
|
||||
export interface SymbolRecord {
|
||||
id: string; // stable, unique within a corpus: \`${file}:${line}:${name}\`
|
||||
@@ -26,6 +26,7 @@ export interface SymbolRecord {
|
||||
file: string; // project path | '/include/joystick.h' | page URL
|
||||
line?: number; // 1-based
|
||||
url?: string; // docs only
|
||||
addr?: number; // debugger only: runtime address of the symbol
|
||||
}
|
||||
|
||||
/** On-disk shape of a prebuilt index (gen/symidx/<name>.json). */
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* DebuggerHit - resolves search queries with no source hits as runtime
|
||||
* symbols / hex addresses, and builds synthetic search hits that jump to
|
||||
* them in the Disassembly (code segments) or Memory Browser (everything else).
|
||||
*
|
||||
* Kept free of DOM / ui.ts dependencies so it can be unit tested.
|
||||
*/
|
||||
|
||||
import { SymbolRecord } from "../../common/searchtypes";
|
||||
import { Segment } from "../../common/workertypes";
|
||||
import { SearchHit } from "./types";
|
||||
|
||||
export type SymbolMap = { [sym: string]: number };
|
||||
|
||||
export interface DebuggerTarget {
|
||||
addr: number;
|
||||
isSymbol: boolean; // resolved from the symbol map (vs. raw hex input)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a needle as a runtime symbol (exact match first, then
|
||||
* case-insensitive) or a hex address ("$1234", "0x1234", "1234" -- bare
|
||||
* digits are parsed as hex, like the listings).
|
||||
* Returns null if it's neither.
|
||||
*/
|
||||
export function resolveDebuggerTarget(needle: string, symbolmap?: SymbolMap): DebuggerTarget | null {
|
||||
needle = needle.trim();
|
||||
if (!needle) return null;
|
||||
if (symbolmap) {
|
||||
if (needle in symbolmap) return { addr: symbolmap[needle], isSymbol: true };
|
||||
const ls = needle.toLowerCase();
|
||||
for (var sym in symbolmap) {
|
||||
if (sym.toLowerCase() === ls) return { addr: symbolmap[sym], isSymbol: true };
|
||||
}
|
||||
}
|
||||
if (/^(0x|\$)[0-9a-f]+$/i.test(needle)) return { addr: parseInt(needle.replace(/^(0x|\$)/i, ''), 16), isSymbol: false };
|
||||
if (/^[0-9a-f]+$/i.test(needle)) return { addr: parseInt(needle, 16), isSymbol: false };
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Find the segment containing an address, if the memory map has one. */
|
||||
export function findSegmentAt(addr: number, segments?: Segment[]): Segment | null {
|
||||
for (const seg of segments || []) {
|
||||
const end = seg.last != null ? seg.last : seg.start + seg.size - 1;
|
||||
if (addr >= seg.start && addr <= end) return seg;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the synthetic search hit for a resolved symbol/address.
|
||||
* 'rom' segments are treated as code (open in the Disassembler, kind=label);
|
||||
* anything else as data (open in the Memory Browser, kind=var).
|
||||
*/
|
||||
export function makeDebuggerHit(needle: string, target: DebuggerTarget, segments?: Segment[]): SearchHit {
|
||||
const seg = findSegmentAt(target.addr, segments);
|
||||
const isCode = seg?.type === 'rom';
|
||||
const rec: SymbolRecord = {
|
||||
id: 'debugger:' + needle.toLowerCase(),
|
||||
name: needle,
|
||||
kind: isCode ? 'label' : 'var',
|
||||
brief: isCode ? 'open in Disassembly' : 'open in Memory Browser',
|
||||
source: 'debugger',
|
||||
file: '',
|
||||
addr: target.addr,
|
||||
};
|
||||
return { record: rec, score: 500 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to offer the debugger hit alongside regular search results:
|
||||
* exact symbol matches are always offered (below source hits);
|
||||
* raw hex addresses are only offered when there are no other hits
|
||||
* (a query like "add" is valid hex and shouldn't add noise).
|
||||
*/
|
||||
export function shouldOfferDebuggerHit(target: DebuggerTarget, hasOtherHits: boolean): boolean {
|
||||
return target.isSymbol || !hasOtherHits;
|
||||
}
|
||||
@@ -6,8 +6,11 @@
|
||||
|
||||
import { searchService } from "./service";
|
||||
import { SearchHit } from "./types";
|
||||
import { projectWindows, openHeaderFile } from "../ui";
|
||||
import { SourceEditor, HeaderView } from "../views/editors";
|
||||
import { hex } from "../../common/util";
|
||||
import { projectWindows, openHeaderFile, current_project, platform } from "../ui";
|
||||
import { SourceEditor, HeaderView, DisassemblerView } from "../views/editors";
|
||||
import { MemoryView } from "../views/debugviews";
|
||||
import { resolveDebuggerTarget, makeDebuggerHit, shouldOfferDebuggerHit } from "./debuggerhit";
|
||||
|
||||
declare var $: JQueryStatic;
|
||||
declare var bootbox: any;
|
||||
@@ -51,6 +54,18 @@ export function openSearchHit(hit: SearchHit) {
|
||||
if (wnd instanceof HeaderView && rec.line) {
|
||||
wnd.navigateToLine(rec.line);
|
||||
}
|
||||
} else if (rec.source === 'debugger') {
|
||||
// Runtime symbol/address with no source: jump to it in the
|
||||
// disassembler (code segments) or memory browser (everything else)
|
||||
const addr = rec.addr ?? -1;
|
||||
if (addr < 0) return;
|
||||
if (rec.kind === 'label' && projectWindows.isWindow('#disasm')) {
|
||||
const wnd = projectWindows.createOrShow('#disasm');
|
||||
if (wnd instanceof DisassemblerView) wnd.goToAddress(addr);
|
||||
} else {
|
||||
const wnd = projectWindows.createOrShow('#memory');
|
||||
if (wnd instanceof MemoryView) wnd.goToAddress(addr);
|
||||
}
|
||||
} else if (rec.source === 'docs') {
|
||||
// External docs: open the URL in a new tab
|
||||
const url = rec.detail || rec.file || '';
|
||||
@@ -148,7 +163,8 @@ export function openSearchDialog() {
|
||||
rec.kind === 'struct' ? 'S' :
|
||||
rec.kind === 'enum' ? 'E' :
|
||||
rec.kind === 'text' ? '≡' : '•';
|
||||
const loc = rec.file && rec.line ? `${rec.file}:${rec.line}` : (rec.file || '');
|
||||
const loc = rec.source === 'debugger' ? hex(rec.addr, 4)
|
||||
: rec.file && rec.line ? `${rec.file}:${rec.line}` : (rec.file || '');
|
||||
const isSmart = rec.kind !== 'text';
|
||||
const li = $('<li class="list-group-item search-hit" style="cursor:pointer;padding:6px 10px;display:flex;align-items:center;overflow:hidden"></li>');
|
||||
li.html(
|
||||
@@ -179,6 +195,13 @@ export function openSearchDialog() {
|
||||
}
|
||||
try {
|
||||
const hits = await searchService.query(needle, MAX_RESULTS);
|
||||
// offer a jump to the emulator views for runtime symbols/addresses;
|
||||
// exact symbol matches always appear (below source hits), raw hex
|
||||
// addresses only when there are no source hits
|
||||
const target = resolveDebuggerTarget(needle, platform && platform.debugSymbols && platform.debugSymbols.symbolmap);
|
||||
if (target && shouldOfferDebuggerHit(target, hits.length > 0)) {
|
||||
hits.push(makeDebuggerHit(needle, target, current_project && current_project.segments));
|
||||
}
|
||||
render(hits);
|
||||
} catch (e) {
|
||||
resultsDiv.html('<div class="text-danger" style="padding:8px">Search failed.</div>');
|
||||
|
||||
@@ -10,6 +10,7 @@ const SOURCE_WEIGHTS: Record<SymbolSourceKind, number> = {
|
||||
project: 1.3,
|
||||
toolchain: 1.0,
|
||||
docs: 0.7,
|
||||
debugger: 0.9, // runtime symbols (never indexed here; weight unused)
|
||||
};
|
||||
|
||||
// Declarations (real code symbols) rank above docs; text hits rank lowest.
|
||||
|
||||
+4
-16
@@ -23,6 +23,7 @@ import { _importProjectFromGithub, _loginToGithub, _logoutOfGithub, _publishProj
|
||||
import { Toolbar } from "./toolbar";
|
||||
import { Shortcut, initShortcutBar, setGlobalShortcutsFn, setViewShortcutsFn, setBarVisible, refreshShortcutBar } from "./shortcutbar";
|
||||
import { openSearchDialog } from "./search/searchview";
|
||||
import { resolveDebuggerTarget } from "./search/debuggerhit";
|
||||
import { setProjectProvider } from "./search/projectsource";
|
||||
import { AssetEditorView } from "./views/asseteditor";
|
||||
import { isMobileDevice } from "./views/baseviews";
|
||||
@@ -1106,22 +1107,9 @@ function getGoToAddressView(): DisassemblerView | MemoryView | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseGoToTarget(s: string): number {
|
||||
s = s.trim();
|
||||
if (!s) return -1;
|
||||
// symbol name first (so symbols like "add" aren't parsed as hex)
|
||||
var symmap = platform.debugSymbols && platform.debugSymbols.symbolmap;
|
||||
if (symmap && s in symmap) return symmap[s];
|
||||
var ls = s.toLowerCase();
|
||||
if (symmap) {
|
||||
for (var sym in symmap) {
|
||||
if (sym.toLowerCase() === ls) return symmap[sym];
|
||||
}
|
||||
}
|
||||
// hex address (bare digits are parsed as hex, like the listings)
|
||||
if (/^(0x|\$)[0-9a-f]+$/i.test(s)) return parseInt(s.replace(/^(0x|\$)/i, ''), 16);
|
||||
if (/^[0-9a-f]+$/i.test(s)) return parseInt(s, 16);
|
||||
return -1;
|
||||
export function parseGoToTarget(s: string): number {
|
||||
var target = resolveDebuggerTarget(s, platform && platform.debugSymbols && platform.debugSymbols.symbolmap);
|
||||
return target ? target.addr : -1;
|
||||
}
|
||||
|
||||
function promptGoToAddress(): boolean {
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import assert from "assert";
|
||||
import { describe, it } from "mocha";
|
||||
import {
|
||||
resolveDebuggerTarget,
|
||||
findSegmentAt,
|
||||
makeDebuggerHit,
|
||||
shouldOfferDebuggerHit,
|
||||
} from "../../src/ide/search/debuggerhit";
|
||||
import { Segment } from "../../src/common/workertypes";
|
||||
|
||||
const SYMBOLS = {
|
||||
main: 0xc000,
|
||||
JOY_READ: 0xf000,
|
||||
videoTop: 0x0200,
|
||||
};
|
||||
|
||||
describe('DebuggerHit', function () {
|
||||
|
||||
describe('resolveDebuggerTarget', function () {
|
||||
it('should resolve exact symbol names', function () {
|
||||
const t = resolveDebuggerTarget('main', SYMBOLS);
|
||||
assert.ok(t);
|
||||
assert.strictEqual(t!.addr, 0xc000);
|
||||
assert.strictEqual(t!.isSymbol, true);
|
||||
});
|
||||
|
||||
it('should resolve symbol names case-insensitively', function () {
|
||||
const t = resolveDebuggerTarget('joy_read', SYMBOLS);
|
||||
assert.ok(t);
|
||||
assert.strictEqual(t!.addr, 0xf000);
|
||||
assert.strictEqual(t!.isSymbol, true);
|
||||
});
|
||||
|
||||
it('should prefer symbol map over hex parse', function () {
|
||||
// "add" is valid hex (0xadd) but if a symbol exists, it wins
|
||||
const t = resolveDebuggerTarget('add', { add: 0x123 });
|
||||
assert.ok(t);
|
||||
assert.strictEqual(t!.addr, 0x123);
|
||||
assert.strictEqual(t!.isSymbol, true);
|
||||
});
|
||||
|
||||
it('should parse $-prefixed hex', function () {
|
||||
const t = resolveDebuggerTarget('$c000');
|
||||
assert.ok(t);
|
||||
assert.strictEqual(t!.addr, 0xc000);
|
||||
assert.strictEqual(t!.isSymbol, false);
|
||||
});
|
||||
|
||||
it('should parse 0x-prefixed hex', function () {
|
||||
const t = resolveDebuggerTarget('0xC000');
|
||||
assert.ok(t);
|
||||
assert.strictEqual(t!.addr, 0xc000);
|
||||
assert.strictEqual(t!.isSymbol, false);
|
||||
});
|
||||
|
||||
it('should parse bare hex digits', function () {
|
||||
const t = resolveDebuggerTarget('C000');
|
||||
assert.ok(t);
|
||||
assert.strictEqual(t!.addr, 0xc000);
|
||||
assert.strictEqual(t!.isSymbol, false);
|
||||
});
|
||||
|
||||
it('should return null for non-hex identifiers', function () {
|
||||
assert.strictEqual(resolveDebuggerTarget('zzz'), null);
|
||||
assert.strictEqual(resolveDebuggerTarget('main loop'), null);
|
||||
assert.strictEqual(resolveDebuggerTarget(''), null);
|
||||
assert.strictEqual(resolveDebuggerTarget(' '), null);
|
||||
});
|
||||
|
||||
it('should return null for hex-like names when no symbol map exists... or resolve as hex', function () {
|
||||
// without a symbol map, hex-like needles still resolve (as raw hex)
|
||||
const t = resolveDebuggerTarget('add');
|
||||
assert.ok(t);
|
||||
assert.strictEqual(t!.addr, 0xadd);
|
||||
assert.strictEqual(t!.isSymbol, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findSegmentAt', function () {
|
||||
const segs: Segment[] = [
|
||||
{ name: 'Code', start: 0xc000, size: 0x1000, type: 'rom' },
|
||||
{ name: 'RAM', start: 0x0200, size: 0x100, type: 'ram' },
|
||||
{ name: 'IO', start: 0x8000, last: 0x8003, size: 0x4, type: 'io' },
|
||||
];
|
||||
|
||||
it('should find the segment containing an address', function () {
|
||||
assert.strictEqual(findSegmentAt(0xc123, segs)?.name, 'Code');
|
||||
assert.strictEqual(findSegmentAt(0x02ff, segs)?.name, 'RAM');
|
||||
});
|
||||
|
||||
it('should respect the last field when present', function () {
|
||||
assert.strictEqual(findSegmentAt(0x8003, segs)?.name, 'IO');
|
||||
assert.strictEqual(findSegmentAt(0x8004, segs), null);
|
||||
});
|
||||
|
||||
it('should return null for unmapped addresses', function () {
|
||||
assert.strictEqual(findSegmentAt(0x0000, segs), null);
|
||||
assert.strictEqual(findSegmentAt(0x10000, segs), null);
|
||||
});
|
||||
|
||||
it('should handle missing segment lists', function () {
|
||||
assert.strictEqual(findSegmentAt(0xc000, undefined), null);
|
||||
assert.strictEqual(findSegmentAt(0xc000, []), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeDebuggerHit', function () {
|
||||
it('should build a disassembler hit for code (rom) segments', function () {
|
||||
const segs: Segment[] = [{ name: 'Code', start: 0xc000, size: 0x1000, type: 'rom' }];
|
||||
const hit = makeDebuggerHit('main', { addr: 0xc100, isSymbol: true }, segs);
|
||||
assert.strictEqual(hit.record.source, 'debugger');
|
||||
assert.strictEqual(hit.record.kind, 'label');
|
||||
assert.strictEqual(hit.record.brief, 'open in Disassembly');
|
||||
assert.strictEqual(hit.record.addr, 0xc100);
|
||||
assert.strictEqual(hit.record.name, 'main');
|
||||
});
|
||||
|
||||
it('should build a memory browser hit for data (ram) segments', function () {
|
||||
const segs: Segment[] = [{ name: 'RAM', start: 0x0200, size: 0x100, type: 'ram' }];
|
||||
const hit = makeDebuggerHit('videoTop', { addr: 0x0200, isSymbol: true }, segs);
|
||||
assert.strictEqual(hit.record.kind, 'var');
|
||||
assert.strictEqual(hit.record.brief, 'open in Memory Browser');
|
||||
assert.strictEqual(hit.record.addr, 0x0200);
|
||||
});
|
||||
|
||||
it('should default to the memory browser when no segment matches', function () {
|
||||
const hit = makeDebuggerHit('$9000', { addr: 0x9000, isSymbol: false }, []);
|
||||
assert.strictEqual(hit.record.kind, 'var');
|
||||
assert.strictEqual(hit.record.brief, 'open in Memory Browser');
|
||||
});
|
||||
|
||||
it('should give debugger hits a low score so source hits rank first', function () {
|
||||
const hit = makeDebuggerHit('main', { addr: 0xc000, isSymbol: true }, []);
|
||||
assert.ok(hit.score < 1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldOfferDebuggerHit', function () {
|
||||
it('should always offer exact symbol matches', function () {
|
||||
assert.strictEqual(shouldOfferDebuggerHit({ addr: 0xc000, isSymbol: true }, true), true);
|
||||
assert.strictEqual(shouldOfferDebuggerHit({ addr: 0xc000, isSymbol: true }, false), true);
|
||||
});
|
||||
|
||||
it('should only offer raw hex when there are no other hits', function () {
|
||||
assert.strictEqual(shouldOfferDebuggerHit({ addr: 0xadd, isSymbol: false }, false), true);
|
||||
assert.strictEqual(shouldOfferDebuggerHit({ addr: 0xadd, isSymbol: false }, true), false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user