From 91d0031366644fc76164f22f90949ea60c90adee Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Wed, 4 Aug 2021 17:14:26 -0500 Subject: [PATCH] split ide/views.ts into ide/views/* --- src/ide/ui.ts | 44 +- src/ide/views.ts | 2127 ---------------------------------- src/ide/views/asseteditor.ts | 389 +++++++ src/ide/views/baseviews.ts | 33 + src/ide/views/debugviews.ts | 796 +++++++++++++ src/ide/views/editors.ts | 583 ++++++++++ src/ide/views/treeviews.ts | 350 ++++++ src/ide/windows.ts | 2 +- src/platform/basic.ts | 4 +- 9 files changed, 2178 insertions(+), 2150 deletions(-) delete mode 100644 src/ide/views.ts create mode 100644 src/ide/views/asseteditor.ts create mode 100644 src/ide/views/baseviews.ts create mode 100644 src/ide/views/debugviews.ts create mode 100644 src/ide/views/editors.ts create mode 100644 src/ide/views/treeviews.ts diff --git a/src/ide/ui.ts b/src/ide/ui.ts index e1a77d6f..3c024b3a 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -7,13 +7,17 @@ import { WorkerResult, WorkerOutput, VerilogOutput, SourceFile, WorkerError, Fil import { ProjectWindows } from "./windows"; import { Platform, Preset, DebugSymbols, DebugEvalCondition, isDebuggable, EmuState, BasePlatform } from "../common/baseplatform"; import { PLATFORMS, EmuHalt, Toolbar } from "../common/emu"; -import * as Views from "./views"; import { getFilenameForPath, getFilenamePrefix, highlightDifferences, invertMap, byteArrayToString, compressLZG, stringToByteArray, byteArrayToUTF8, isProbablyBinary, getWithBinary, getBasePlatform, getRootBasePlatform, hex, loadScript, decodeQueryString, parseBool } from "../common/util"; import { StateRecorderImpl } from "../common/recorder"; import { GHSession, GithubService, getRepos, parseGithubURL, FirebaseProjectFilesystem } from "./services"; import Split = require('split.js'); import { importPlatform } from "../platform/_index"; +import { DisassemblerView, ListingView, SourceEditor } from "./views/editors"; +import { AddressHeatMapView, BinaryFileView, MemoryMapView, MemoryView, ProbeLogView, ProbeSymbolView, RasterPCHeatMapView, ScanlineIOView, VRAMMemoryView } from "./views/debugviews"; +import { AssetEditorView } from "./views/asseteditor"; +import { isMobileDevice } from "./views/baseviews"; +import { CallStackView, DebugBrowserView } from "./views/treeviews"; // external libs (TODO) declare var Tour, GIF, firebase; @@ -286,7 +290,7 @@ function refreshWindowList() { function loadEditor(path:string) { var tool = platform.getToolForFilename(path); var mode = tool && TOOL_TO_SOURCE_STYLE[tool]; - return new Views.SourceEditor(path, mode); + return new SourceEditor(path, mode); } function addEditorItem(id:string) { @@ -295,7 +299,7 @@ function refreshWindowList() { if (typeof data === 'string') return loadEditor(id); else if (data instanceof Uint8Array) - return new Views.BinaryFileView(id, data as Uint8Array); + return new BinaryFileView(id, data as Uint8Array); }); } @@ -318,7 +322,7 @@ function refreshWindowList() { // add listing if source/assembly file exists and has text if ((lst.assemblyfile && lst.assemblyfile.text) || (lst.sourcefile && lst.sourcefile.text)) { addWindowItem(lstfn, getFilenameForPath(lstfn), (path) => { - return new Views.ListingView(path); + return new ListingView(path); }); } } @@ -328,57 +332,57 @@ function refreshWindowList() { separate = true; if (platform.disassemble && platform.saveState) { addWindowItem("#disasm", "Disassembly", () => { - return new Views.DisassemblerView(); + return new DisassemblerView(); }); } if (platform.readAddress) { addWindowItem("#memory", "Memory Browser", () => { - return new Views.MemoryView(); + return new MemoryView(); }); } if (current_project.segments && current_project.segments.length) { addWindowItem("#memmap", "Memory Map", () => { - return new Views.MemoryMapView(); + return new MemoryMapView(); }); } if (platform.readVRAMAddress) { addWindowItem("#memvram", "VRAM Browser", () => { - return new Views.VRAMMemoryView(); + return new VRAMMemoryView(); }); } if (platform.startProbing) { addWindowItem("#memheatmap", "Memory Probe", () => { - return new Views.AddressHeatMapView(); + return new AddressHeatMapView(); }); // TODO: only if raster addWindowItem("#crtheatmap", "CRT Probe", () => { - return new Views.RasterPCHeatMapView(); + return new RasterPCHeatMapView(); }); addWindowItem("#probelog", "Probe Log", () => { - return new Views.ProbeLogView(); + return new ProbeLogView(); }); addWindowItem("#scanlineio", "Scanline I/O", () => { - return new Views.ScanlineIOView(); + return new ScanlineIOView(); }); addWindowItem("#symbolprobe", "Symbol Profiler", () => { - return new Views.ProbeSymbolView(); + return new ProbeSymbolView(); }); addWindowItem("#callstack", "Call Stack", () => { - return new Views.CallStackView(); + return new CallStackView(); }); /* addWindowItem("#framecalls", "Frame Profiler", () => { - return new Views.FrameCallsView(); + return new FrameCallsView(); }); */ } if (platform.getDebugTree) { addWindowItem("#debugview", "Debug Tree", () => { - return new Views.DebugBrowserView(); + return new DebugBrowserView(); }); } addWindowItem('#asseteditor', 'Asset Editor', () => { - return new Views.AssetEditorView(); + return new AssetEditorView(); }); } @@ -1213,7 +1217,7 @@ function getErrorElement(err : WorkerError) { if (projectWindows.isWindow(path)) { link.click((ev) => { var wnd = projectWindows.createOrShow(path); - if (wnd instanceof Views.SourceEditor) { + if (wnd instanceof SourceEditor) { wnd.setCurrentLine(err, true); } }); @@ -2233,7 +2237,7 @@ export function setupSplits() { var sizes; if (platform_id.startsWith('vcs')) sizes = [0, 50, 50]; - else if (isEmbed || Views.isMobileDevice) + else if (isEmbed || isMobileDevice) sizes = [0, 60, 40]; else sizes = [12, 44, 44]; @@ -2483,7 +2487,7 @@ function writeOutputROMFile() { } export function highlightSearch(query: string) { // TODO: filename? var wnd = projectWindows.getActive(); - if (wnd instanceof Views.SourceEditor) { + if (wnd instanceof SourceEditor) { var sc = wnd.editor.getSearchCursor(query); if (sc.findNext()) { wnd.editor.setSelection(sc.pos.to, sc.pos.from); diff --git a/src/ide/views.ts b/src/ide/views.ts deleted file mode 100644 index 4190065d..00000000 --- a/src/ide/views.ts +++ /dev/null @@ -1,2127 +0,0 @@ - -//import CodeMirror = require("codemirror"); -import { SourceFile, WorkerError, Segment, FileData, SourceLocation, SourceLine } from "../common/workertypes"; -import { BaseZ80MachinePlatform, BaseZ80Platform } from "../common/baseplatform"; -import { hex, lpad, rpad, safeident, rgb2bgr } from "../common/util"; -import { CodeAnalyzer } from "../common/analysis"; -import { platform, platform_id, compparams, current_project, lastDebugState, projectWindows, runToPC, qs } from "./ui"; -import { ProbeRecorder, ProbeFlags } from "../common/recorder"; -import { getMousePos, dumpRAM, getVisibleEditorLineHeight, VirtualTextScroller, VirtualTextLine } from "../common/emu"; -import * as pixed from "./pixeleditor"; -import Mousetrap = require('mousetrap'); -import { VirtualList } from "../common/vlist"; - -export interface ProjectView { - createDiv(parent:HTMLElement) : HTMLElement; - setVisible?(showing : boolean) : void; - refresh(moveCursor:boolean) : void; - tick?() : void; - getPath?() : string; - getValue?() : string; - setText?(text : string) : void; - insertText?(text : string) : void; - getCursorPC?() : number; - getSourceFile?() : SourceFile; - setGutterBytes?(line:number, s:string) : void; - markErrors?(errors:WorkerError[]) : void; - clearErrors?() : void; - setTimingResult?(result:CodeAnalyzer) : void; - recreateOnResize? : boolean; - undoStep?() : void; -}; - -// detect mobile (https://stackoverflow.com/questions/3514784/what-is-the-best-way-to-detect-a-mobile-device) -export var isMobileDevice = window.matchMedia && window.matchMedia("only screen and (max-width: 760px)").matches; - -declare var CodeMirror; - -// helper function for editor -function jumpToLine(ed, i:number) { - var t = ed.charCoords({line: i, ch: 0}, "local").top; - var middleHeight = ed.getScrollerElement().offsetHeight / 2; - ed.scrollTo(null, t - middleHeight - 5); -} - -function createTextSpan(text:string, className:string) : HTMLElement { - var span = document.createElement("span"); - span.setAttribute("class", className); - span.appendChild(document.createTextNode(text)); - return span; -} - -function newDiv(parent?, cls? : string) { - var div = $(document.createElement("div")); - if (parent) div.appendTo(parent) - if (cls) div.addClass(cls); - return div; -} - -///// - -const MAX_ERRORS = 200; - -const MODEDEFS = { - default: { theme: 'mbo' }, // NOTE: Not merged w/ other modes - '6502': { isAsm: true }, - z80: { isAsm: true }, - jsasm: { isAsm: true }, - gas: { isAsm: true }, - vasm: { isAsm: true }, - inform6: { theme: 'cobalt' }, - markdown: { lineWrap: true }, - fastbasic: { noGutters: true }, - basic: { noLineNumbers: true, noGutters: true }, // TODO: not used? -} - -export var textMapFunctions = { - input: null -}; - -export class SourceEditor implements ProjectView { - constructor(path:string, mode:string) { - this.path = path; - this.mode = mode; - } - path : string; - mode : string; - editor; - dirtylisting = true; - sourcefile : SourceFile; - currentDebugLine : SourceLocation; - markCurrentPC; // TextMarker - errormsgs = []; - errorwidgets = []; - errormarks = []; - inspectWidget; - - createDiv(parent:HTMLElement) { - var div = document.createElement('div'); - div.setAttribute("class", "editor"); - parent.appendChild(div); - var text = current_project.getFile(this.path) as string; - var asmOverride = text && this.mode=='verilog' && /__asm\b([\s\S]+?)\b__endasm\b/.test(text); - this.newEditor(div, asmOverride); - if (text) { - this.setText(text); // TODO: this calls setCode() and builds... it shouldn't - this.editor.setSelection({line:0,ch:0}, {line:0,ch:0}, {scroll:true}); // move cursor to start - } - this.setupEditor(); - return div; - } - - newEditor(parent:HTMLElement, isAsmOverride?:boolean) { - var modedef = MODEDEFS[this.mode] || MODEDEFS.default; - var isAsm = isAsmOverride || modedef.isAsm; - var lineWrap = !!modedef.lineWrap; - var theme = modedef.theme || MODEDEFS.default.theme; - var lineNums = !modedef.noLineNumbers && !isMobileDevice; - if (qs['embed']) { - lineNums = false; // no line numbers while embedded - isAsm = false; // no opcode bytes either - } - var gutters = ["CodeMirror-linenumbers", "gutter-offset", "gutter-info"]; - if (isAsm) gutters = ["CodeMirror-linenumbers", "gutter-offset", "gutter-bytes", "gutter-clock", "gutter-info"]; - if (modedef.noGutters || isMobileDevice) gutters = ["gutter-info"]; - this.editor = CodeMirror(parent, { - theme: theme, - lineNumbers: lineNums, - matchBrackets: true, - tabSize: 8, - indentAuto: true, - lineWrapping: lineWrap, - gutters: gutters - }); - } - - setupEditor() { - var timer; - // update file in project (and recompile) when edits made - this.editor.on('changes', (ed, changeobj) => { - clearTimeout(timer); - timer = setTimeout( () => { - current_project.updateFile(this.path, this.editor.getValue()); - }, 300); - }); - // inspect symbol when it's highlighted (double-click) - this.editor.on('cursorActivity', (ed) => { - var start = this.editor.getCursor(true); - var end = this.editor.getCursor(false); - if (start.line == end.line && start.ch < end.ch && end.ch-start.ch < 80) { - var name = this.editor.getSelection(); - this.inspect(name); - } else { - this.inspect(null); - } - }); - // gutter clicked - this.editor.on("gutterClick", (cm, n) => { - this.toggleBreakpoint(n); - }); - // set editor mode for highlighting, etc - this.editor.setOption("mode", this.mode); - // change text? - this.editor.on('beforeChange', (cm, chgobj) => { - if (textMapFunctions.input && chgobj.text) chgobj.text = chgobj.text.map(textMapFunctions.input); - }); - } - - - inspect(ident : string) : void { - var result; - if (platform.inspect) { - result = platform.inspect(ident); - } - if (this.inspectWidget) { - this.inspectWidget.clear(); - this.inspectWidget = null; - } - if (result) { - var infospan = createTextSpan(result, "tooltipinfoline"); - var line = this.editor.getCursor().line; - this.inspectWidget = this.editor.addLineWidget(line, infospan, {above:false}); - } - } - - setText(text:string) { - var i,j; - var oldtext = this.editor.getValue(); - if (oldtext != text) { - this.editor.setValue(text); - /* - // find minimum range to undo - for (i=0; i= numLines) line = 0; - this.addErrorMarker(line, info.msg); - if (info.start != null) { - var markOpts = {className:"mark-error", inclusiveLeft:true}; - var start = {line:line, ch:info.end?info.start:info.start-1}; - var end = {line:line, ch:info.end?info.end:info.start}; - var mark = this.editor.markText(start, end, markOpts); - this.errormarks.push(mark); - } - } - } - - addErrorMarker(line:number, msg:string) { - var div = document.createElement("div"); - div.setAttribute("class", "tooltipbox tooltiperror"); - div.appendChild(document.createTextNode("\u24cd")); - this.editor.setGutterMarker(line, "gutter-info", div); - this.errormsgs.push({line:line, msg:msg}); - // expand line widgets when mousing over errors - $(div).mouseover((e) => { - this.expandErrors(); - }); - } - - addErrorLine(line:number, msg:string) { - var errspan = createTextSpan(msg, "tooltiperrorline"); - this.errorwidgets.push(this.editor.addLineWidget(line, errspan)); - } - - expandErrors() { - var e; - while (e = this.errormsgs.shift()) { - this.addErrorLine(e.line, e.msg); - } - } - - markErrors(errors:WorkerError[]) { - // TODO: move cursor to error line if offscreen? - this.clearErrors(); - errors = errors.slice(0, MAX_ERRORS); - for (var info of errors) { - this.addError(info); - } - } - - clearErrors() { - this.dirtylisting = true; - // clear line widgets - this.editor.clearGutter("gutter-info"); - this.errormsgs = []; - while (this.errorwidgets.length) this.errorwidgets.shift().clear(); - while (this.errormarks.length) this.errormarks.shift().clear(); - } - - getSourceFile() : SourceFile { return this.sourcefile; } - - updateListing() { - // update editor annotations - // TODO: recreate editor if gutter-bytes is used (verilog) - this.clearErrors(); - this.editor.clearGutter("gutter-bytes"); - this.editor.clearGutter("gutter-offset"); - this.editor.clearGutter("gutter-clock"); - var lstlines = this.sourcefile.lines || []; - for (var info of lstlines) { - if (info.offset >= 0) { - this.setGutter("gutter-offset", info.line-1, hex(info.offset&0xffff,4)); - } - if (info.insns) { - var insnstr = info.insns.length > 9 ? ("...") : info.insns; - this.setGutter("gutter-bytes", info.line-1, insnstr); - if (info.iscode) { - // TODO: labels trick this part? - if (info.cycles) { - this.setGutter("gutter-clock", info.line-1, info.cycles+""); - } else if (platform.getOpcodeMetadata) { - var opcode = parseInt(info.insns.split(" ")[0], 16); - var meta = platform.getOpcodeMetadata(opcode, info.offset); - if (meta && meta.minCycles) { - var clockstr = meta.minCycles+""; - this.setGutter("gutter-clock", info.line-1, clockstr); - } - } - } - } - } - } - - setGutter(type:string, line:number, text:string) { - var lineinfo = this.editor.lineInfo(line); - if (lineinfo && lineinfo.gutterMarkers && lineinfo.gutterMarkers[type]) { - // do not replace existing marker - } else { - var textel = document.createTextNode(text); - this.editor.setGutterMarker(line, type, textel); - } - } - - setGutterBytes(line:number, s:string) { - this.setGutter("gutter-bytes", line-1, s); - } - - setTimingResult(result:CodeAnalyzer) : void { - this.editor.clearGutter("gutter-bytes"); - if (this.sourcefile == null) return; - // show the lines - for (const line of Object.keys(this.sourcefile.line2offset)) { - var pc = this.sourcefile.line2offset[line]; - var minclocks = result.pc2minclocks[pc]; - var maxclocks = result.pc2maxclocks[pc]; - if (minclocks>=0 && maxclocks>=0) { - var s; - if (maxclocks == minclocks) - s = minclocks + ""; - else - s = minclocks + "-" + maxclocks; - if (maxclocks == result.MAX_CLOCKS) - s += "+"; - this.setGutterBytes(parseInt(line), s); - } - } - } - - setCurrentLine(line:SourceLocation, moveCursor:boolean) { - var blocked = platform.isBlocked && platform.isBlocked(); - - var addCurrentMarker = (line:SourceLocation) => { - var div = document.createElement("div"); - var cls = blocked ? 'currentpc-marker-blocked' : 'currentpc-marker'; - div.classList.add(cls); - div.appendChild(document.createTextNode("\u25b6")); - this.editor.setGutterMarker(line.line-1, "gutter-info", div); - } - - this.clearCurrentLine(moveCursor); - if (line) { - addCurrentMarker(line); - if (moveCursor) { - this.editor.setCursor({line:line.line-1,ch:line.start||0}, {scroll:true}); - } - var cls = blocked ? 'currentpc-span-blocked' : 'currentpc-span'; - var markOpts = {className:cls, inclusiveLeft:true}; - if (line.start || line.end) - this.markCurrentPC = this.editor.markText({line:line.line-1,ch:line.start}, {line:line.line-1,ch:line.end||line.start+1}, markOpts); - else - this.markCurrentPC = this.editor.markText({line:line.line-1,ch:0}, {line:line.line,ch:0}, markOpts); - this.currentDebugLine = line; - } - } - - clearCurrentLine(moveCursor:boolean) { - if (this.currentDebugLine) { - this.editor.clearGutter("gutter-info"); - if (moveCursor) this.editor.setSelection(this.editor.getCursor()); - this.currentDebugLine = null; - } - if (this.markCurrentPC) { - this.markCurrentPC.clear(); - this.markCurrentPC = null; - } - } - - getActiveLine() : SourceLocation { - if (this.sourcefile) { - var cpustate = lastDebugState && lastDebugState.c; - if (!cpustate && platform.getCPUState && !platform.isRunning()) - cpustate = platform.getCPUState(); - if (cpustate) { - var EPC = (cpustate && (cpustate.EPC || cpustate.PC)); - var res = this.sourcefile.findLineForOffset(EPC, 15); - return res; - } - } - } - - refreshDebugState(moveCursor:boolean) { - // TODO: only if line changed - // TODO: remove after compilation - this.clearCurrentLine(moveCursor); - var line = this.getActiveLine(); - if (line) { - this.setCurrentLine(line, moveCursor); - } - } - - refreshListing() { - // lookup corresponding sourcefile for this file, using listing - var lst = current_project.getListingForFile(this.path); - if (lst && lst.sourcefile && lst.sourcefile !== this.sourcefile) { - this.sourcefile = lst.sourcefile; - this.dirtylisting = true; - } - if (!this.sourcefile || !this.dirtylisting) return; - this.updateListing(); - this.dirtylisting = false; - } - - refresh(moveCursor: boolean) { - this.refreshListing(); - this.refreshDebugState(moveCursor); - } - - tick() { - this.refreshDebugState(false); - } - - getLine(line : number) { - return this.editor.getLine(line-1); - } - - getCurrentLine() : number { - return this.editor.getCursor().line+1; - } - - getCursorPC() : number { - var line = this.getCurrentLine(); - while (this.sourcefile && line >= 0) { - var pc = this.sourcefile.line2offset[line]; - if (pc >= 0) return pc; - line--; - } - return -1; - } - - undoStep() { - this.editor.execCommand('undo'); - } - - toggleBreakpoint(lineno: number) { - // TODO: we have to always start at beginning of frame - if (this.sourcefile != null) { - var targetPC = this.sourcefile.line2offset[lineno+1]; - /* - var bpid = "pc" + targetPC; - if (platform.hasBreakpoint(bpid)) { - platform.clearBreakpoint(bpid); - } else { - platform.setBreakpoint(bpid, () => { - return platform.getPC() == targetPC; - }); - } - */ - runToPC(targetPC); - } - } -} - -/// - -const disasmWindow = 1024; // disassemble this many bytes around cursor - -export class DisassemblerView implements ProjectView { - disasmview; - - getDisasmView() { return this.disasmview; } - - createDiv(parent : HTMLElement) { - var div = document.createElement('div'); - div.setAttribute("class", "editor"); - parent.appendChild(div); - this.newEditor(div); - return div; - } - - newEditor(parent : HTMLElement) { - this.disasmview = CodeMirror(parent, { - mode: 'z80', // TODO: pick correct one - theme: 'cobalt', - tabSize: 8, - readOnly: true, - styleActiveLine: true - }); - } - - // TODO: too many globals - refresh(moveCursor: boolean) { - let state = lastDebugState || platform.saveState(); // TODO? - let pc = state.c ? state.c.PC : 0; - let curline = 0; - let selline = 0; - let addr2symbol = (platform.debugSymbols && platform.debugSymbols.addr2symbol) || {}; - // TODO: not perfect disassembler - let disassemble = (start, len) => { - // TODO: use pc2visits - let s = ""; - let ofs = 0; - while (ofs < len) { - let a = (start + ofs) | 0; - let disasm = platform.disassemble(a, platform.readAddress.bind(platform)); - /* TODO: look thru all source files - let srclinenum = sourcefile && this.sourcefile.offset2line[a]; - if (srclinenum) { - let srcline = getActiveEditor().getLine(srclinenum); - if (srcline && srcline.trim().length) { - s += "; " + srclinenum + ":\t" + srcline + "\n"; - curline++; - } - } - */ - let bytes = ""; - let comment = ""; - for (let i=0; i { - let addr = parseInt(args[1], 16); - let sym = addr2symbol[addr]; - if (sym) return (args[0] + sym); - sym = addr2symbol[addr-1]; - if (sym) return (args[0] + sym + "+1"); - return substr; - }); - } - if (addr2symbol) { - let sym = addr2symbol[a]; - if (sym) { - comment = "; " + sym; - } - } - let dline = hex(a, 4) + "\t" + rpad(bytes,14) + "\t" + rpad(dstr,30) + comment + "\n"; - s += dline; - if (a == pc) selline = curline; - curline++; - ofs += disasm.nbytes || 1; - } - return s; - } - var startpc = pc < 0 ? pc-disasmWindow : Math.max(0, pc-disasmWindow); // for 32-bit PCs w/ hi bit set - let text = disassemble(startpc, pc-startpc) + disassemble(pc, disasmWindow); - this.disasmview.setValue(text); - if (moveCursor) { - this.disasmview.setCursor(selline, 0); - } - jumpToLine(this.disasmview, selline); - } - - getCursorPC() : number { - var line = this.disasmview.getCursor().line; - if (line >= 0) { - var toks = this.disasmview.getLine(line).trim().split(/\s+/); - if (toks && toks.length >= 1) { - var pc = parseInt(toks[0], 16); - if (pc >= 0) return pc; - } - } - return -1; - } -} - -/// - -export class ListingView extends DisassemblerView implements ProjectView { - assemblyfile : SourceFile; - path : string; - - constructor(lstfn : string) { - super(); - this.path = lstfn; - } - - refreshListing() { - // lookup corresponding assemblyfile for this file, using listing - var lst = current_project.getListingForFile(this.path); - // TODO? - this.assemblyfile = lst && (lst.assemblyfile || lst.sourcefile); - } - - refresh(moveCursor: boolean) { - this.refreshListing(); - // load listing text into editor - if (!this.assemblyfile) return; - var asmtext = this.assemblyfile.text; - var disasmview = this.getDisasmView(); - disasmview.setValue(asmtext); - // go to PC - if (!platform.saveState) return; - var state = lastDebugState || platform.saveState(); - var pc = state.c ? (state.c.EPC || state.c.PC) : 0; - if (pc >= 0 && this.assemblyfile) { - var res = this.assemblyfile.findLineForOffset(pc, 15); - if (res) { - // set cursor while debugging - if (moveCursor) { - disasmview.setCursor(res.line-1, 0); - } - jumpToLine(disasmview, res.line-1); - } - } - } -} - -/// - -function ignoreSymbol(sym:string) { - return sym.endsWith('_SIZE__') || sym.endsWith('_LAST__') || sym.endsWith('STACKSIZE__') || sym.endsWith('FILEOFFS__') - || sym.startsWith('l__') || sym.startsWith('s__') || sym.startsWith('.__.'); -} - -// TODO: make it use debug state -// TODO: make it safe (load/restore state?) -// TODO: refactor w/ VirtualTextLine -export class MemoryView implements ProjectView { - memorylist; - dumplines; - maindiv : HTMLElement; - recreateOnResize = true; - totalRows = 0x1400; - - createDiv(parent : HTMLElement) { - var div = document.createElement('div'); - div.setAttribute("class", "memdump"); - parent.appendChild(div); - this.showMemoryWindow(parent, div); - return this.maindiv = div; - } - - showMemoryWindow(workspace:HTMLElement, parent:HTMLElement) { - this.memorylist = new VirtualList({ - w: $(workspace).width(), - h: $(workspace).height(), - itemHeight: getVisibleEditorLineHeight(), - totalRows: this.totalRows, - generatorFn: (row : number) => { - var s = this.getMemoryLineAt(row); - var linediv = document.createElement("div"); - if (this.dumplines) { - var dlr = this.dumplines[row]; - if (dlr) linediv.classList.add('seg_' + this.getMemorySegment(this.dumplines[row].a)); - } - linediv.appendChild(document.createTextNode(s)); - return linediv; - } - }); - $(parent).append(this.memorylist.container); - this.tick(); - if (compparams && this.dumplines) - this.scrollToAddress(compparams.data_start); - } - - scrollToAddress(addr : number) { - if (this.dumplines) { - this.memorylist.scrollToItem(this.findMemoryWindowLine(addr)); - } - } - - refresh() { - this.dumplines = null; - this.tick(); - } - - tick() { - if (this.memorylist) { - $(this.maindiv).find('[data-index]').each( (i,e) => { - var div = $(e); - var row = parseInt(div.attr('data-index')); - var oldtext = div.text(); - var newtext = this.getMemoryLineAt(row); - if (oldtext != newtext) - div.text(newtext); - }); - } - } - - getMemoryLineAt(row : number) : string { - var offset = row * 16; - var n1 = 0; - var n2 = 16; - var sym; - if (this.getDumpLines()) { - var dl = this.dumplines[row]; - if (dl) { - offset = dl.a & 0xfff0; - n1 = dl.a - offset; - n2 = n1 + dl.l; - sym = dl.s; - } else { - return '.'; - } - } - var s = hex(offset+n1,4) + ' '; - for (var i=0; i 8) s += ' '; - for (var i=n1; i nextofs) ofs2 = nextofs; - //if (ofs < 1000) console.log(ofs, ofs2, nextofs, sym); - this.dumplines.push({a:ofs, l:ofs2-ofs, s:sym}); - ofs = ofs2; - } - } - sym = nextsym; - } - } - return this.dumplines; - } - - // TODO: use segments list? - getMemorySegment(a:number) : string { - if (compparams) { - if (a >= compparams.data_start && a < compparams.data_start+compparams.data_size) { - if (platform.getSP && a >= platform.getSP() - 15) - return 'stack'; - else - return 'data'; - } - else if (a >= compparams.code_start && a < compparams.code_start+(compparams.code_size||compparams.rom_size)) - return 'code'; - } - var segments = current_project.segments; - if (segments) { - for (var seg of segments) { - if (a >= seg.start && a < seg.start+seg.size) { - if (seg.type == 'rom') return 'code'; - if (seg.type == 'ram') return 'data'; - if (seg.type == 'io') return 'io'; - } - } - } - return 'unknown'; - } - - findMemoryWindowLine(a:number) : number { - for (var i=0; i= a) - return i; - } -} - -export class VRAMMemoryView extends MemoryView { - totalRows = 0x800; - readAddress(n : number) { - return platform.readVRAMAddress(n); - } - getMemorySegment(a:number) : string { - return 'video'; - } - getDumpLines() { - return null; - } -} - -/// - -export class BinaryFileView implements ProjectView { - vlist : VirtualTextScroller; - maindiv : HTMLElement; - path:string; - data:Uint8Array; - recreateOnResize = true; - - constructor(path:string, data:Uint8Array) { - this.path = path; - this.data = data; - } - - createDiv(parent : HTMLElement) { - this.vlist = new VirtualTextScroller(parent); - this.vlist.create(parent, ((this.data.length+15) >> 4), this.getMemoryLineAt.bind(this)); - return this.vlist.maindiv; - } - - getMemoryLineAt(row : number) : VirtualTextLine { - var offset = row * 16; - var n1 = 0; - var n2 = 16; - var s = hex(offset+n1,4) + ' '; - for (var i=0; i 8) s += ' '; - for (var i=n1; i=0?hex(read,2):' '); - } - return {text:s}; - } - - refresh() { - this.vlist.refresh(); - } - - getPath() { return this.path; } -} - -/// - -export class MemoryMapView implements ProjectView { - maindiv : JQuery; - - createDiv(parent : HTMLElement) { - this.maindiv = newDiv(parent, 'vertical-scroll'); - this.maindiv.css('display', 'grid'); - this.maindiv.css('grid-template-columns', '5em 40% 40%'); - return this.maindiv[0]; - } - - // TODO: overlapping segments (e.g. ROM + LC) - addSegment(seg : Segment, newrow : boolean) { - if (newrow) { - var offset = $('
'); - offset.text('$'+hex(seg.start,4)); - this.maindiv.append(offset); - } - var segdiv = $('
'); - if (!newrow) - segdiv.css('grid-column-start', 3); // make sure it's on right side - if (seg.last) - segdiv.text(seg.name+" ("+(seg.last-seg.start)+" / "+seg.size+" bytes used)"); - else - segdiv.text(seg.name+" ("+seg.size+" bytes)"); - if (seg.size >= 256) { - var pad = (Math.log(seg.size) - Math.log(256)) * 0.5; - segdiv.css('padding-top', pad+'em'); - segdiv.css('padding-bottom', pad+'em'); - } - if (seg.type) { - segdiv.addClass('segment-'+seg.type); - } - this.maindiv.append(segdiv); - //var row = $('
').append(offset, segdiv); - //var container = $('
').append(row); - //this.maindiv.append(container); - segdiv.click(() => { - // TODO: what if memory browser does not exist? - var memview = projectWindows.createOrShow('#memory') as MemoryView; - memview.scrollToAddress(seg.start); - }); - } - - refresh() { - this.maindiv.empty(); - var segments = current_project.segments; - if (segments) { - var curofs = 0; - var laststart = -1; - for (var seg of segments) { - //var used = seg.last ? (seg.last-seg.start) : seg.size; - if (seg.start > curofs) - this.addSegment({name:'',start:curofs, size:seg.start-curofs}, true); - this.addSegment(seg, laststart != seg.start); - laststart = seg.start; - curofs = seg.start + seg.size; - } - } - } - -} - -/// - -// TODO: clear buffer when scrubbing - -abstract class ProbeViewBaseBase { - probe : ProbeRecorder; - tooldiv : HTMLElement; - cumulativeData : boolean = false; - cyclesPerLine : number; - totalScanlines : number; - - abstract tick() : void; - - constructor() { - var width = 160; - var height = 262; // TODO: PAL? - try { - width = Math.ceil(platform['machine']['cpuCyclesPerLine']) || width; // TODO - height = Math.ceil(platform['machine']['numTotalScanlines']) || height; // TODO - } catch (e) { - } - this.cyclesPerLine = width; - this.totalScanlines = height; - } - - addr2symbol(addr : number) : string { - var _addr2sym = (platform.debugSymbols && platform.debugSymbols.addr2symbol) || {}; - return _addr2sym[addr]; - } - - addr2str(addr : number) : string { - var sym = this.addr2symbol(addr); - if (typeof sym === 'string') - return '$' + hex(addr) + ' (' + sym + ')'; - else - return '$' + hex(addr); - } - - showTooltip(s:string) { - if (s) { - if (!this.tooldiv) { - this.tooldiv = document.createElement("div"); - this.tooldiv.setAttribute("class", "tooltiptrack"); - document.body.appendChild(this.tooldiv); - } - $(this.tooldiv).text(s).show(); - } else { - $(this.tooldiv).hide(); - } - } - - setVisible(showing : boolean) : void { - if (showing) { - this.probe = platform.startProbing(); - this.probe.singleFrame = !this.cumulativeData; - this.tick(); - } else { - if (this.probe) this.probe.singleFrame = true; - platform.stopProbing(); - this.probe = null; - } - } - - redraw( eventfn:(op,addr,col,row,clk,value) => void ) { - var p = this.probe; - if (!p || !p.idx) return; // if no probe, or if empty - var row=0; - var col=0; - var clk=0; - for (var i=0; i> 16) & 0xff; - var op = word & 0xff000000; - switch (op) { - case ProbeFlags.SCANLINE: row++; col=0; break; - case ProbeFlags.FRAME: row=0; col=0; break; - case ProbeFlags.CLOCKS: col += addr; clk += addr; break; - default: - eventfn(op, addr, col, row, clk, value); - break; - } - } - } - - opToString(op:number, addr?:number, value?:number) { - var s = ""; - switch (op) { - case ProbeFlags.EXECUTE: s = "Exec"; break; - case ProbeFlags.MEM_READ: s = "Read"; break; - case ProbeFlags.MEM_WRITE: s = "Write"; break; - case ProbeFlags.IO_READ: s = "IO Read"; break; - case ProbeFlags.IO_WRITE: s = "IO Write"; break; - case ProbeFlags.VRAM_READ: s = "VRAM Read"; break; - case ProbeFlags.VRAM_WRITE: s = "VRAM Write"; break; - case ProbeFlags.INTERRUPT: s = "Interrupt"; break; - case ProbeFlags.ILLEGAL: s = "Error"; break; - case ProbeFlags.SP_PUSH: s = "Stack Push"; break; - case ProbeFlags.SP_POP: s = "Stack Pop"; break; - default: return ""; - } - if (typeof addr == 'number') s += " " + this.addr2str(addr); - if ((op & ProbeFlags.HAS_VALUE) && typeof value == 'number') s += " = $" + hex(value,2); - return s; - } - - getOpRGB(op:number) : number { - switch (op) { - case ProbeFlags.EXECUTE: return 0x018001; - case ProbeFlags.MEM_READ: return 0x800101; - case ProbeFlags.MEM_WRITE: return 0x010180; - case ProbeFlags.IO_READ: return 0x018080; - case ProbeFlags.IO_WRITE: return 0xc00180; - case ProbeFlags.VRAM_READ: return 0x808001; - case ProbeFlags.VRAM_WRITE: return 0x4080c0; - case ProbeFlags.INTERRUPT: return 0xcfcfcf; - case ProbeFlags.ILLEGAL: return 0x3f3fff; - default: return 0; - } - } -} - -abstract class ProbeViewBase extends ProbeViewBaseBase { - - maindiv : HTMLElement; - canvas : HTMLCanvasElement; - ctx : CanvasRenderingContext2D; - recreateOnResize = true; - - abstract drawEvent(op, addr, col, row); - - createCanvas(parent:HTMLElement, width:number, height:number) { - var div = document.createElement('div'); - var canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - canvas.classList.add('pixelated'); - canvas.style.width = '100%'; - canvas.style.height = '90vh'; // i hate css - canvas.style.backgroundColor = 'black'; - canvas.style.cursor = 'crosshair'; - canvas.onmousemove = (e) => { - var pos = getMousePos(canvas, e); - this.showTooltip(this.getTooltipText(pos.x, pos.y)); - $(this.tooldiv).css('left',e.pageX+10).css('top',e.pageY-30); - } - canvas.onmouseout = (e) => { - $(this.tooldiv).hide(); - } - parent.appendChild(div); - div.appendChild(canvas); - this.canvas = canvas; - this.ctx = canvas.getContext('2d'); - this.initCanvas(); - return this.maindiv = div; - } - - initCanvas() { - } - - getTooltipText(x:number, y:number) : string { - return null; - } - - clear() { - } - - tick() { - this.clear(); - this.redraw(this.drawEvent.bind(this)); - } -} - -abstract class ProbeBitmapViewBase extends ProbeViewBase { - - imageData : ImageData; - datau32 : Uint32Array; - recreateOnResize = false; - - createDiv(parent : HTMLElement) { - return this.createCanvas(parent, this.cyclesPerLine, this.totalScanlines); - } - initCanvas() { - this.imageData = this.ctx.createImageData(this.canvas.width, this.canvas.height); - this.datau32 = new Uint32Array(this.imageData.data.buffer); - } - getTooltipText(x:number, y:number) : string { - x = x|0; - y = y|0; - var s = ""; - var lastroutine = null; - var symstack = []; - var lastcol = -1; - this.redraw( (op,addr,col,row,clk,value) => { - switch (op) { - case ProbeFlags.EXECUTE: - lastroutine = this.addr2symbol(addr) || lastroutine; - break; - case ProbeFlags.SP_PUSH: - symstack.push(lastroutine); - break; - case ProbeFlags.SP_POP: - lastroutine = symstack.pop(); - break; - } - if (row == y && col <= x) { - if (col != lastcol) { - s = ""; - lastcol = col; - } - if (s == "" && lastroutine) { s += "\n" + lastroutine; } - s += "\n" + this.opToString(op, addr, value); - } - } ); - return 'X: ' + x + ' Y: ' + y + ' ' + s; - } - - refresh() { - this.tick(); - this.datau32.fill(0xff000000); - } - tick() { - super.tick(); - this.ctx.putImageData(this.imageData, 0, 0); - } - clear() { - this.datau32.fill(0xff000000); - } -} - -export class AddressHeatMapView extends ProbeBitmapViewBase implements ProjectView { - - createDiv(parent : HTMLElement) { - return this.createCanvas(parent, 256, 256); - } - - clear() { - for (var i=0; i<=0xffff; i++) { - var v = platform.readAddress(i); - var rgb = (v >> 2) | (v & 0x1f); - rgb |= (rgb<<8) | (rgb<<16); - this.datau32[i] = rgb | 0xff000000; - } - } - - drawEvent(op, addr, col, row) { - var rgb = this.getOpRGB(op); - if (!rgb) return; - var x = addr & 0xff; - var y = (addr >> 8) & 0xff; - var data = this.datau32[addr & 0xffff]; - data = data | rgb | 0xff000000; - this.datau32[addr & 0xffff] = data; - } - - getTooltipText(x:number, y:number) : string { - var a = (x & 0xff) + (y << 8); - var s = ""; - var pc = -1; - var already = {}; - var lastroutine = null; - var symstack = []; - this.redraw( (op,addr,col,row,clk,value) => { - switch (op) { - case ProbeFlags.EXECUTE: - pc = addr; - lastroutine = this.addr2symbol(addr) || lastroutine; - break; - case ProbeFlags.SP_PUSH: - symstack.push(lastroutine); - break; - case ProbeFlags.SP_POP: - lastroutine = symstack.pop(); - break; - } - var key = op|pc; - if (addr == a && !already[key]) { - if (s == "" && lastroutine) { s += "\n" + lastroutine; } - s += "\nPC " + this.addr2str(pc) + " " + this.opToString(op, null, value); - already[key] = 1; - } - } ); - return this.addr2str(a) + s; - } -} - -export class RasterPCHeatMapView extends ProbeBitmapViewBase implements ProjectView { - - drawEvent(op, addr, col, row) { - var iofs = col + row * this.canvas.width; - var rgb = this.getOpRGB(op); - if (!rgb) return; - var data = this.datau32[iofs]; - data = data | rgb | 0xff000000; - this.datau32[iofs] = data; - } -} - -export class ProbeLogView extends ProbeViewBaseBase { - vlist : VirtualTextScroller; - maindiv : HTMLElement; - recreateOnResize = true; - dumplines; - - createDiv(parent : HTMLElement) { - this.vlist = new VirtualTextScroller(parent); - this.vlist.create(parent, this.cyclesPerLine*this.totalScanlines, this.getMemoryLineAt.bind(this)); - return this.vlist.maindiv; - } - getMemoryLineAt(row : number) : VirtualTextLine { - var s : string = ""; - var c : string = "seg_data"; - var line = this.dumplines && this.dumplines[row]; - if (line != null) { - var xtra : string = line.info.join(", "); - s = "(" + lpad(line.row,4) + ", " + lpad(line.col,4) + ") " + rpad(line.asm||"",20) + xtra; - if (xtra.indexOf("Write ") >= 0) c = "seg_io"; - // if (xtra.indexOf("Stack ") >= 0) c = "seg_code"; - } - return {text:s, clas:c}; - } - refresh() { - this.tick(); - } - tick() { - const isz80 = platform instanceof BaseZ80MachinePlatform || platform instanceof BaseZ80Platform; // TODO? - // cache each line in frame - this.dumplines = {}; - this.redraw((op,addr,col,row,clk,value) => { - if (isz80) clk >>= 2; - var line = this.dumplines[clk]; - if (line == null) { - line = {op:op, addr:addr, row:row, col:col, asm:null, info:[]}; - this.dumplines[clk] = line; - } - switch (op) { - case ProbeFlags.EXECUTE: - if (platform.disassemble) { - var disasm = platform.disassemble(addr, platform.readAddress.bind(platform)); - line.asm = disasm && disasm.line; - } - break; - default: - var xtra = this.opToString(op, addr, value); - if (xtra != "") line.info.push(xtra); - break; - } - }); - this.vlist.refresh(); - } -} - -export class ScanlineIOView extends ProbeViewBaseBase { - vlist : VirtualTextScroller; - maindiv : HTMLElement; - recreateOnResize = true; - dumplines; - - createDiv(parent : HTMLElement) { - this.vlist = new VirtualTextScroller(parent); - this.vlist.create(parent, this.totalScanlines, this.getMemoryLineAt.bind(this)); - return this.vlist.maindiv; - } - getMemoryLineAt(row : number) : VirtualTextLine { - var s = lpad(row+"",3) + ' '; - var c = 'seg_code'; - var line = (this.dumplines && this.dumplines[row]) || []; - var hblankCycle = Math.round(this.cyclesPerLine/3.3); - for (var i=0; i { - var line = this.dumplines[row]; - if (line == null) { - this.dumplines[row] = line = []; - } - switch (op) { - case ProbeFlags.EXECUTE: - var sym = platform.debugSymbols.addr2symbol[addr]; - if (sym) line[-1] = sym; - break; - case ProbeFlags.MEM_WRITE: - case ProbeFlags.IO_READ: - case ProbeFlags.IO_WRITE: - case ProbeFlags.VRAM_READ: - case ProbeFlags.VRAM_WRITE: - line[col] = op | addr; - break; - } - }); - this.vlist.refresh(); - } -} - -/// - -export class ProbeSymbolView extends ProbeViewBaseBase { - vlist : VirtualTextScroller; - keys : string[]; - recreateOnResize = true; - dumplines; - cumulativeData = true; - - // TODO: auto resize - createDiv(parent : HTMLElement) { - // TODO: what if symbol list changes? - if (platform.debugSymbols && platform.debugSymbols.symbolmap) { - this.keys = Array.from(Object.keys(platform.debugSymbols.symbolmap).filter(sym => !ignoreSymbol(sym))); - } else { - this.keys = ['no symbols defined']; - } - this.vlist = new VirtualTextScroller(parent); - this.vlist.create(parent, this.keys.length + 1, this.getMemoryLineAt.bind(this)); - return this.vlist.maindiv; - } - - getMemoryLineAt(row : number) : VirtualTextLine { - // header line - if (row == 0) { - return {text: lpad("Symbol",35)+lpad("Reads",8)+lpad("Writes",8)}; - } - var sym = this.keys[row-1]; - var line = this.dumplines && this.dumplines[sym]; - function getop(op) { - var n = line[op] | 0; - return lpad(n ? n.toString() : "", 8); - } - var s : string; - var c : string; - if (line != null) { - s = lpad(sym, 35) - + getop(ProbeFlags.MEM_READ) - + getop(ProbeFlags.MEM_WRITE); - if (line[ProbeFlags.EXECUTE]) - c = 'seg_code'; - else if (line[ProbeFlags.IO_READ] || line[ProbeFlags.IO_WRITE]) - c = 'seg_io'; - else - c = 'seg_data'; - } else { - s = lpad(sym, 35); - c = 'seg_unknown'; - } - return {text:s, clas:c}; - } - - refresh() { - this.tick(); - } - - tick() { - // cache each line in frame - this.dumplines = {}; - this.redraw((op,addr,col,row,clk,value) => { - var sym = platform.debugSymbols.addr2symbol[addr]; - if (sym != null) { - var line = this.dumplines[sym]; - if (line == null) { - line = {}; - this.dumplines[sym] = line; - } - line[op] = (line[op] | 0) + 1; - } - }); - this.vlist.refresh(); - if (this.probe) this.probe.clear(); // clear cumulative data (TODO: doesnt work with seeking or debugging) - } -} - -/// - -const MAX_CHILDREN = 256; -const MAX_STRING_LEN = 100; - -var TREE_SHOW_DOLLAR_IDENTS = false; - -class TreeNode { - parent : TreeNode; - name : string; - _div : HTMLElement; - _header : HTMLElement; - _inline : HTMLElement; - _content : HTMLElement; - children : Map; - expanded = false; - level : number; - view : ProjectView; - - constructor(parent : TreeNode, name : string) { - this.parent = parent; - this.name = name; - this.children = new Map(); - this.level = parent ? (parent.level+1) : -1; - this.view = parent ? parent.view : null; - } - getDiv() { - if (this._div == null) { - this._div = document.createElement("div"); - this._div.classList.add("vertical-scroll"); - this._div.classList.add("tree-content"); - this._header = document.createElement("div"); - this._header.classList.add("tree-header"); - this._header.classList.add("tree-level-" + this.level); - this._header.append(this.name); - this._inline = document.createElement("span"); - this._inline.classList.add("tree-value"); - this._header.append(this._inline); - this._div.append(this._header); - this.parent._content.append(this._div); - this._header.onclick = (e) => { - this.toggleExpanded(); - }; - } - if (this.expanded && this._content == null) { - this._content = document.createElement("div"); - this._div.append(this._content); - } - else if (!this.expanded && this._content != null) { - this._content.remove(); - this._content = null; - this.children.clear(); - } - return this._div; - } - toggleExpanded() { - this.expanded = !this.expanded; - this.view.tick(); - } - remove() { - this._div.remove(); - this._div = null; - } - update(obj : any) { - this.getDiv(); - var text = ""; - // is it a function? call it first, if we are expanded - // TODO: only call functions w/ signature - if (obj && obj.$$ && typeof obj.$$ == 'function' && this._content != null) { - obj = obj.$$(); - } - // check null first - if (obj == null) { - text = obj+""; - // primitive types - } else if (typeof obj == 'number') { - if (obj != (obj|0)) text = obj.toString(); // must be a float - else text = obj + "\t($" + hex(obj) + ")"; - } else if (typeof obj == 'boolean') { - text = obj.toString(); - } else if (typeof obj == 'string') { - if (obj.length < MAX_STRING_LEN) - text = obj; - else - text = obj.substring(0, MAX_STRING_LEN) + "..."; - // typed byte array (TODO: other kinds) - } else if (obj.buffer && obj.length <= MAX_CHILDREN) { - text = dumpRAM(obj, 0, obj.length); - // recurse into object? (or function) - } else if (typeof obj == 'object' || typeof obj == 'function') { - // only if expanded - if (this._content != null) { - // split big arrays - if (obj.slice && obj.length > MAX_CHILDREN) { - let newobj = {}; - let oldobj = obj; - var slicelen = MAX_CHILDREN; - while (obj.length / slicelen > MAX_CHILDREN) slicelen *= 2; - for (let ofs=0; ofs { return oldobj.slice(ofs, ofs+slicelen); }} - } - obj = newobj; - } - // get object keys - let names = obj instanceof Array ? Array.from(obj.keys()) : Object.getOwnPropertyNames(obj); - if (names.length > MAX_CHILDREN) { // max # of child objects - let newobj = {}; - let oldobj = obj; - var slicelen = 100; - while (names.length / slicelen > 100) slicelen *= 2; - for (let ofs=0; ofs { - // hide $xxx idents? - var hidden = !TREE_SHOW_DOLLAR_IDENTS && typeof name === 'string' && name.startsWith("$$"); - if (!hidden) { - let childnode = this.children.get(name); - if (childnode == null) { - childnode = new TreeNode(this, name); - this.children.set(name, childnode); - } - childnode.update(obj[name]); - } - orphans.delete(name); - }); - // remove orphans - orphans.forEach((delname) => { - let childnode = this.children.get(delname); - childnode.remove(); - this.children.delete(delname); - }); - this._header.classList.add("tree-expanded"); - this._header.classList.remove("tree-collapsed"); - } else { - this._header.classList.add("tree-collapsed"); - this._header.classList.remove("tree-expanded"); - } - } else { - text = typeof obj; // fallthrough - } - // change DOM object if needed - if (this._inline.innerText != text) { - this._inline.innerText = text; - } - } -} - -function createTreeRootNode(parent : HTMLElement, view : ProjectView) : TreeNode { - var mainnode = new TreeNode(null, null); - mainnode.view = view; - mainnode._content = parent; - var root = new TreeNode(mainnode, "/"); - root.expanded = true; - root.getDiv(); // create it - root._div.style.padding = '0px'; - return root; // should be cached -} - -export abstract class TreeViewBase implements ProjectView { - root : TreeNode; - - createDiv(parent : HTMLElement) : HTMLElement { - this.root = createTreeRootNode(parent, this); - return this.root.getDiv(); - } - - refresh() { - this.tick(); - } - - tick() { - this.root.update(this.getRootObject()); - } - - abstract getRootObject() : Object; -} - -export class StateBrowserView extends TreeViewBase implements ProjectView { - getRootObject() { return platform.saveState(); } -} - -export class DebugBrowserView extends TreeViewBase implements ProjectView { - getRootObject() { return platform.getDebugTree(); } -} - -interface CallGraphNode { - $$SP : number; - $$PC : number; - count : number; - startLine : number; - endLine : number; - calls : {[id:string] : CallGraphNode}; -} - -// TODO: clear stack data when reset? -export class CallStackView extends ProbeViewBaseBase implements ProjectView { - treeroot : TreeNode; - graph : CallGraphNode; - stack : CallGraphNode[]; - lastsp : number; - lastpc : number; - jsr : boolean; - rts : boolean; - cumulativeData = true; - - createDiv(parent : HTMLElement) : HTMLElement { - this.clear(); - this.treeroot = createTreeRootNode(parent, this); - return this.treeroot.getDiv(); - } - - refresh() { - this.tick(); - } - - tick() { - this.treeroot.update(this.getRootObject()); - if (this.probe) this.probe.clear(); // clear cumulative data (TODO: doesnt work with seeking or debugging) - } - - clear() { - this.graph = null; - this.reset(); - } - - reset() { - this.stack = []; - this.lastsp = -1; - this.lastpc = 0; - this.jsr = false; - this.rts = false; - } - - newNode(pc : number, sp : number) { - return {$$SP:sp, $$PC:pc, count:0, startLine:null, endLine:null, calls:{}}; - } - - newRoot(pc : number, sp : number) { - if (this.stack.length == 0) { - this.graph = this.newNode(null, sp); - this.stack.unshift(this.graph); - } else if (sp > this.stack[0].$$SP) { - this.graph = this.newNode(null, sp); - this.graph.calls[this.addr2str(pc)] = this.stack[0]; - this.stack.unshift(this.graph); - } - } - - getRootObject() : Object { - // TODO: we don't capture every frame, so if we don't start @ the top frame we may have problems - this.redraw((op,addr,col,row,clk,value) => { - switch (op) { - case ProbeFlags.SP_POP: - this.newRoot(this.lastpc, this.lastsp); - case ProbeFlags.SP_PUSH: - if (this.stack.length) { - let top = this.stack[this.stack.length-1]; - var delta = this.lastsp - addr; - if ((delta == 2 || delta == 3) && addr < top.$$SP) { // TODO: look for opcode? - this.jsr = true; - } - if ((delta == -2 || delta == -3) && this.stack.length > 1 && addr > top.$$SP) { - this.rts = true; - } - } - this.lastsp = addr; - break; - case ProbeFlags.EXECUTE: - // TODO: better check for CALL/RET opcodes - if (Math.abs(addr - this.lastpc) >= 4) { // make sure we're jumping a distance (TODO) - if (this.jsr && this.stack.length) { - let top = this.stack[this.stack.length-1]; - let sym = this.addr2str(addr); - let child = top.calls[sym]; - if (child == null) { - child = top.calls[sym] = this.newNode(addr, this.lastsp); - } - else if (child.$$PC == null) child.$$PC = addr; - //this.stack.forEach((node) => node.count++); - this.stack.push(child); - child.count++; - child.startLine = row; - } - this.jsr = false; - if (this.rts && this.stack.length) { - this.stack.pop().endLine = row; - } - this.rts = false; - } - this.lastpc = addr; - break; - } - }); - if (this.graph) this.graph['$$Stack'] = this.stack; - return TREE_SHOW_DOLLAR_IDENTS ? this.graph : this.graph && this.graph.calls; - } -} - -export class FrameCallsView extends ProbeViewBaseBase implements ProjectView { - treeroot : TreeNode; - - createDiv(parent : HTMLElement) : HTMLElement { - this.treeroot = createTreeRootNode(parent, this); - return this.treeroot.getDiv(); - } - - refresh() { - this.tick(); - } - - tick() { - this.treeroot.update(this.getRootObject()); - } - - getRootObject() : Object { - var frame = {}; - this.redraw((op,addr,col,row,clk,value) => { - switch (op) { - case ProbeFlags.EXECUTE: - let sym = this.addr2symbol(addr); - if (sym) { - if (!frame[sym]) { - frame[sym] = row; - } - } - break; - } - }); - return frame; - } -} - - -/// - -export class AssetEditorView implements ProjectView, pixed.EditorContext { - maindiv : JQuery; - cureditordiv : JQuery; - cureditelem : JQuery; - cureditnode : pixed.PixNode; - rootnodes : pixed.PixNode[]; - deferrednodes : pixed.PixNode[]; - - createDiv(parent : HTMLElement) { - this.maindiv = newDiv(parent, "vertical-scroll"); - return this.maindiv[0]; - } - - clearAssets() { - this.rootnodes = []; - this.deferrednodes = []; - } - - registerAsset(type:string, node:pixed.PixNode, deferred:number) { - this.rootnodes.push(node); - if (deferred) { - if (deferred > 1) - this.deferrednodes.push(node); - else - this.deferrednodes.unshift(node); - } else { - node.refreshRight(); - } - } - - getPalettes(matchlen : number) : pixed.SelectablePalette[] { - var result = []; - this.rootnodes.forEach((node) => { - while (node != null) { - if (node instanceof pixed.PaletteFormatToRGB) { - // TODO: move to node class? - var palette = node.palette; - // match full palette length? - if (matchlen == palette.length) { - result.push({node:node, name:"Palette", palette:palette}); - } - // look at palette slices - if (node.layout) { - node.layout.forEach(([name, start, len]) => { - if (start < palette.length) { - if (len == matchlen) { - var rgbs = palette.slice(start, start+len); - result.push({node:node, name:name, palette:rgbs}); - } else if (-len == matchlen) { // reverse order - var rgbs = palette.slice(start, start-len); - rgbs.reverse(); - result.push({node:node, name:name, palette:rgbs}); - } else if (len+1 == matchlen) { - var rgbs = new Uint32Array(matchlen); - rgbs[0] = palette[0]; - rgbs.set(palette.slice(start, start+len), 1); - result.push({node:node, name:name, palette:rgbs}); - } - } - }); - } - break; - } - node = node.right; - } - }); - return result; - } - - getTilemaps(matchlen : number) : pixed.SelectableTilemap[] { - var result = []; - this.rootnodes.forEach((node) => { - while (node != null) { - if (node instanceof pixed.Palettizer) { - var rgbimgs = node.rgbimgs; - if (rgbimgs && rgbimgs.length >= matchlen) { - result.push({node:node, name:"Tilemap", images:node.images, rgbimgs:rgbimgs}); // TODO - } - } - node = node.right; - } - }); - return result; - } - - isEditing() { - return this.cureditordiv != null; - } - - getCurrentEditNode() { - return this.cureditnode; - } - - setCurrentEditor(div:JQuery, editing:JQuery, node:pixed.PixNode) { - const timeout = 250; - if (this.cureditordiv != div) { - if (this.cureditordiv) { - this.cureditordiv.hide(timeout); - this.cureditordiv = null; - } - if (div) { - this.cureditordiv = div; - this.cureditordiv.show(); - this.cureditordiv[0].scrollIntoView({behavior: "smooth", block: "center"}); - //setTimeout(() => { this.cureditordiv[0].scrollIntoView({behavior: "smooth", block: "center"}) }, timeout); - } - } - if (this.cureditelem) { - this.cureditelem.removeClass('selected'); - this.cureditelem = null; - } - if (editing) { - this.cureditelem = editing; - this.cureditelem.addClass('selected'); - } - while (node.left) { - node = node.left; - } - this.cureditnode = node; - } - - scanFileTextForAssets(id : string, data : string) { - // scan file for assets - // /*{json}*/ or ;;{json};; - // TODO: put before ident, look for = { - var result = []; - var re1 = /[/;][*;]([{].+[}])[*;][/;]/g; - var m; - while (m = re1.exec(data)) { - var start = m.index + m[0].length; - var end; - // TODO: verilog end - if (platform_id == 'verilog') { - end = data.indexOf("end", start); // asm - } else if (m[0].startsWith(';;')) { - end = data.indexOf(';;', start); // asm - } else { - end = data.indexOf(';', start); // C - } - //console.log(id, start, end, m[1], data.substring(start,end)); - if (end > start) { - try { - var jsontxt = m[1].replace(/([A-Za-z]+):/g, '"$1":'); // fix lenient JSON - var json = JSON.parse(jsontxt); - // TODO: name? - result.push({fileid:id,fmt:json,start:start,end:end}); - } catch (e) { - console.log(e); - } - } - } - // look for DEF_METASPRITE_2x2(playerRStand, 0xd8, 0) - // TODO: could also look in ROM - var re2 = /DEF_METASPRITE_(\d+)x(\d+)[(](\w+),\s*(\w+),\s*(\w+)/gi; - while (m = re2.exec(data)) { - var width = parseInt(m[1]); - var height = parseInt(m[2]); - var ident = m[3]; - var tile = parseInt(m[4]); - var attr = parseInt(m[5]); - var metadefs = []; - for (var x=0; x').appendTo(parentdiv); - var aeditor = $('
').hide(); // contains editor, when selected - // TODO: they need to update when refreshed from right - var allrgbimgs = []; - pal2rgb.getAllColors().forEach((rgba) => { allrgbimgs.push(new Uint32Array([rgba])); }); // array of array of 1 rgb color (for picker) - var atable = $('').appendTo(adual); - aeditor.appendTo(adual); - // make default layout if not exists - var layout = pal2rgb.layout; - if (!layout) { - var len = pal2rgb.palette.length; - var imgsperline = len > 32 ? 8 : 4; // TODO: use 'n'? - layout = []; - for (var i=0; i { - if (start < pal2rgb.palette.length) { // skip row if out of range - var arow = $('').appendTo(atable); - $('
').text(name).appendTo(arow); - var inds = []; - for (var k=start; k { - var cell = $('').addClass('asset_cell asset_editable').appendTo(arow); - updateCell(cell, i); - cell.click((e) => { - var chooser = new pixed.ImageChooser(); - chooser.rgbimgs = allrgbimgs; - chooser.width = 1; - chooser.height = 1; - chooser.recreate(aeditor, (index, newvalue) => { - callback(i, index); - updateCell(cell, i); - }); - this.setCurrentEditor(aeditor, cell, pal2rgb); - }); - }); - } - }); - } - - addPixelEditor(parentdiv:JQuery, firstnode:pixed.PixNode, fmt:pixed.PixelEditorImageFormat) { - // data -> pixels - fmt.xform = 'scale(2)'; - var mapper = new pixed.Mapper(fmt); - // TODO: rotate node? - firstnode.addRight(mapper); - // pixels -> RGBA - var palizer = new pixed.Palettizer(this, fmt); - mapper.addRight(palizer); - // add view objects - palizer.addRight(new pixed.CharmapEditor(this, newDiv(parentdiv), fmt)); - } - - addPaletteEditor(parentdiv:JQuery, firstnode:pixed.PixNode, palfmt?) { - // palette -> RGBA - var pal2rgb = new pixed.PaletteFormatToRGB(palfmt); - firstnode.addRight(pal2rgb); - // TODO: refresh twice? - firstnode.refreshRight(); - // TODO: add view objects - // TODO: show which one is selected? - this.addPaletteEditorViews(parentdiv, pal2rgb, - (index, newvalue) => { - console.log('set entry', index, '=', newvalue); - // TODO: this forces update of palette rgb colors and file data - firstnode.words[index] = newvalue; - pal2rgb.words = null; - pal2rgb.updateRight(); - pal2rgb.refreshLeft(); - }); - } - - ensureFileDiv(fileid : string) : JQuery { - var divid = this.getFileDivId(fileid); - var body = $(document.getElementById(divid)); - if (body.length === 0) { - var filediv = newDiv(this.maindiv, 'asset_file'); - var header = newDiv(filediv, 'asset_file_header').text(fileid); - body = newDiv(filediv).attr('id',divid).addClass('disable-select'); - } - return body; - } - - refreshAssetsInFile(fileid : string, data : FileData) : number { - let nassets = 0; - // TODO: check fmt w/h/etc limits - // TODO: defer editor creation - // TODO: only refresh when needed - if (platform_id.startsWith('nes') && fileid.endsWith('.chr') && data instanceof Uint8Array) { - // is this a NES CHR? - let node = new pixed.FileDataNode(projectWindows, fileid); - const neschrfmt = {w:8,h:8,bpp:1,count:(data.length>>4),brev:true,np:2,pofs:8,remap:[0,1,2,4,5,6,7,8,9,10,11,12]}; // TODO - this.addPixelEditor(this.ensureFileDiv(fileid), node, neschrfmt); - this.registerAsset("charmap", node, 1); - nassets++; - } else if (platform_id.startsWith('nes') && fileid.endsWith('.pal') && data instanceof Uint8Array) { - // is this a NES PAL? - let node = new pixed.FileDataNode(projectWindows, fileid); - const nespalfmt = {pal:"nes",layout:"nes"}; - this.addPaletteEditor(this.ensureFileDiv(fileid), node, nespalfmt); - this.registerAsset("palette", node, 0); - nassets++; - } else if (typeof data === 'string') { - let textfrags = this.scanFileTextForAssets(fileid, data); - for (let frag of textfrags) { - if (frag.fmt) { - let label = fileid; // TODO: label - let node : pixed.PixNode = new pixed.TextDataNode(projectWindows, fileid, label, frag.start, frag.end); - let first = node; - // rle-compressed? TODO: how to edit? - if (frag.fmt.comp == 'rletag') { - node = node.addRight(new pixed.Compressor()); - } - // is this a nes nametable? - if (frag.fmt.map == 'nesnt') { - node = node.addRight(new pixed.NESNametableConverter(this)); - node = node.addRight(new pixed.Palettizer(this, {w:8,h:8,bpp:4})); - const fmt = {w:8*(frag.fmt.w||32),h:8*(frag.fmt.h||30),count:1}; // TODO: can't do custom sizes - node = node.addRight(new pixed.MapEditor(this, newDiv(this.ensureFileDiv(fileid)), fmt)); - this.registerAsset("nametable", first, 2); - nassets++; - } - // is this a bitmap? - else if (frag.fmt.w > 0 && frag.fmt.h > 0) { - this.addPixelEditor(this.ensureFileDiv(fileid), node, frag.fmt); - this.registerAsset("charmap", first, 1); - nassets++; - } - // is this a palette? - else if (frag.fmt.pal) { - this.addPaletteEditor(this.ensureFileDiv(fileid), node, frag.fmt); - this.registerAsset("palette", first, 0); - nassets++; - } - else { - // TODO: other kinds of resources? - } - } - } - } - return nassets; - } - - getFileDivId(id : string) { - return '__asset__' + safeident(id); - } - -// TODO: recreate editors when refreshing -// TODO: look for changes, not moveCursor - refresh(moveCursor : boolean) { - // clear and refresh all files/nodes? - if (moveCursor) { - this.maindiv.empty(); - this.clearAssets(); - current_project.iterateFiles((fileid, data) => { - try { - var nassets = this.refreshAssetsInFile(fileid, data); - } catch (e) { - console.log(e); - this.ensureFileDiv(fileid).text(e+""); // TODO: error msg? - } - }); - console.log("Found " + this.rootnodes.length + " assets"); - this.deferrednodes.forEach((node) => { - try { - node.refreshRight(); - } catch (e) { - console.log(e); - alert(e+""); - } - }); - this.deferrednodes = []; - } else { - // only refresh nodes if not actively editing - // since we could be in the middle of an operation that hasn't been committed - for (var node of this.rootnodes) { - if (node !== this.getCurrentEditNode()) { - node.refreshRight(); - } - } - } - } - - setVisible?(showing : boolean) : void { - // TODO: make into toolbar? - if (showing) { - if (Mousetrap.bind) Mousetrap.bind('ctrl+z', projectWindows.undoStep.bind(projectWindows)); - } else { - if (Mousetrap.unbind) Mousetrap.unbind('ctrl+z'); - } - } - -} diff --git a/src/ide/views/asseteditor.ts b/src/ide/views/asseteditor.ts new file mode 100644 index 00000000..e212c5ea --- /dev/null +++ b/src/ide/views/asseteditor.ts @@ -0,0 +1,389 @@ + +import { newDiv, ProjectView } from "./baseviews"; +import { platform_id, current_project, projectWindows } from "../ui"; +import { FileData } from "../../common/workertypes"; +import { hex, safeident, rgb2bgr } from "../../common/util"; +import * as pixed from "../pixeleditor"; +import Mousetrap = require('mousetrap'); + +export class AssetEditorView implements ProjectView, pixed.EditorContext { + maindiv : JQuery; + cureditordiv : JQuery; + cureditelem : JQuery; + cureditnode : pixed.PixNode; + rootnodes : pixed.PixNode[]; + deferrednodes : pixed.PixNode[]; + + createDiv(parent : HTMLElement) { + this.maindiv = newDiv(parent, "vertical-scroll"); + return this.maindiv[0]; + } + + clearAssets() { + this.rootnodes = []; + this.deferrednodes = []; + } + + registerAsset(type:string, node:pixed.PixNode, deferred:number) { + this.rootnodes.push(node); + if (deferred) { + if (deferred > 1) + this.deferrednodes.push(node); + else + this.deferrednodes.unshift(node); + } else { + node.refreshRight(); + } + } + + getPalettes(matchlen : number) : pixed.SelectablePalette[] { + var result = []; + this.rootnodes.forEach((node) => { + while (node != null) { + if (node instanceof pixed.PaletteFormatToRGB) { + // TODO: move to node class? + var palette = node.palette; + // match full palette length? + if (matchlen == palette.length) { + result.push({node:node, name:"Palette", palette:palette}); + } + // look at palette slices + if (node.layout) { + node.layout.forEach(([name, start, len]) => { + if (start < palette.length) { + if (len == matchlen) { + var rgbs = palette.slice(start, start+len); + result.push({node:node, name:name, palette:rgbs}); + } else if (-len == matchlen) { // reverse order + var rgbs = palette.slice(start, start-len); + rgbs.reverse(); + result.push({node:node, name:name, palette:rgbs}); + } else if (len+1 == matchlen) { + var rgbs = new Uint32Array(matchlen); + rgbs[0] = palette[0]; + rgbs.set(palette.slice(start, start+len), 1); + result.push({node:node, name:name, palette:rgbs}); + } + } + }); + } + break; + } + node = node.right; + } + }); + return result; + } + + getTilemaps(matchlen : number) : pixed.SelectableTilemap[] { + var result = []; + this.rootnodes.forEach((node) => { + while (node != null) { + if (node instanceof pixed.Palettizer) { + var rgbimgs = node.rgbimgs; + if (rgbimgs && rgbimgs.length >= matchlen) { + result.push({node:node, name:"Tilemap", images:node.images, rgbimgs:rgbimgs}); // TODO + } + } + node = node.right; + } + }); + return result; + } + + isEditing() { + return this.cureditordiv != null; + } + + getCurrentEditNode() { + return this.cureditnode; + } + + setCurrentEditor(div:JQuery, editing:JQuery, node:pixed.PixNode) { + const timeout = 250; + if (this.cureditordiv != div) { + if (this.cureditordiv) { + this.cureditordiv.hide(timeout); + this.cureditordiv = null; + } + if (div) { + this.cureditordiv = div; + this.cureditordiv.show(); + this.cureditordiv[0].scrollIntoView({behavior: "smooth", block: "center"}); + //setTimeout(() => { this.cureditordiv[0].scrollIntoView({behavior: "smooth", block: "center"}) }, timeout); + } + } + if (this.cureditelem) { + this.cureditelem.removeClass('selected'); + this.cureditelem = null; + } + if (editing) { + this.cureditelem = editing; + this.cureditelem.addClass('selected'); + } + while (node.left) { + node = node.left; + } + this.cureditnode = node; + } + + scanFileTextForAssets(id : string, data : string) { + // scan file for assets + // /*{json}*/ or ;;{json};; + // TODO: put before ident, look for = { + var result = []; + var re1 = /[/;][*;]([{].+[}])[*;][/;]/g; + var m; + while (m = re1.exec(data)) { + var start = m.index + m[0].length; + var end; + // TODO: verilog end + if (platform_id == 'verilog') { + end = data.indexOf("end", start); // asm + } else if (m[0].startsWith(';;')) { + end = data.indexOf(';;', start); // asm + } else { + end = data.indexOf(';', start); // C + } + //console.log(id, start, end, m[1], data.substring(start,end)); + if (end > start) { + try { + var jsontxt = m[1].replace(/([A-Za-z]+):/g, '"$1":'); // fix lenient JSON + var json = JSON.parse(jsontxt); + // TODO: name? + result.push({fileid:id,fmt:json,start:start,end:end}); + } catch (e) { + console.log(e); + } + } + } + // look for DEF_METASPRITE_2x2(playerRStand, 0xd8, 0) + // TODO: could also look in ROM + var re2 = /DEF_METASPRITE_(\d+)x(\d+)[(](\w+),\s*(\w+),\s*(\w+)/gi; + while (m = re2.exec(data)) { + var width = parseInt(m[1]); + var height = parseInt(m[2]); + var ident = m[3]; + var tile = parseInt(m[4]); + var attr = parseInt(m[5]); + var metadefs = []; + for (var x=0; x').appendTo(parentdiv); + var aeditor = $('
').hide(); // contains editor, when selected + // TODO: they need to update when refreshed from right + var allrgbimgs = []; + pal2rgb.getAllColors().forEach((rgba) => { allrgbimgs.push(new Uint32Array([rgba])); }); // array of array of 1 rgb color (for picker) + var atable = $('').appendTo(adual); + aeditor.appendTo(adual); + // make default layout if not exists + var layout = pal2rgb.layout; + if (!layout) { + var len = pal2rgb.palette.length; + var imgsperline = len > 32 ? 8 : 4; // TODO: use 'n'? + layout = []; + for (var i=0; i { + if (start < pal2rgb.palette.length) { // skip row if out of range + var arow = $('').appendTo(atable); + $('
').text(name).appendTo(arow); + var inds = []; + for (var k=start; k { + var cell = $('').addClass('asset_cell asset_editable').appendTo(arow); + updateCell(cell, i); + cell.click((e) => { + var chooser = new pixed.ImageChooser(); + chooser.rgbimgs = allrgbimgs; + chooser.width = 1; + chooser.height = 1; + chooser.recreate(aeditor, (index, newvalue) => { + callback(i, index); + updateCell(cell, i); + }); + this.setCurrentEditor(aeditor, cell, pal2rgb); + }); + }); + } + }); + } + + addPixelEditor(parentdiv:JQuery, firstnode:pixed.PixNode, fmt:pixed.PixelEditorImageFormat) { + // data -> pixels + fmt.xform = 'scale(2)'; + var mapper = new pixed.Mapper(fmt); + // TODO: rotate node? + firstnode.addRight(mapper); + // pixels -> RGBA + var palizer = new pixed.Palettizer(this, fmt); + mapper.addRight(palizer); + // add view objects + palizer.addRight(new pixed.CharmapEditor(this, newDiv(parentdiv), fmt)); + } + + addPaletteEditor(parentdiv:JQuery, firstnode:pixed.PixNode, palfmt?) { + // palette -> RGBA + var pal2rgb = new pixed.PaletteFormatToRGB(palfmt); + firstnode.addRight(pal2rgb); + // TODO: refresh twice? + firstnode.refreshRight(); + // TODO: add view objects + // TODO: show which one is selected? + this.addPaletteEditorViews(parentdiv, pal2rgb, + (index, newvalue) => { + console.log('set entry', index, '=', newvalue); + // TODO: this forces update of palette rgb colors and file data + firstnode.words[index] = newvalue; + pal2rgb.words = null; + pal2rgb.updateRight(); + pal2rgb.refreshLeft(); + }); + } + + ensureFileDiv(fileid : string) : JQuery { + var divid = this.getFileDivId(fileid); + var body = $(document.getElementById(divid)); + if (body.length === 0) { + var filediv = newDiv(this.maindiv, 'asset_file'); + var header = newDiv(filediv, 'asset_file_header').text(fileid); + body = newDiv(filediv).attr('id',divid).addClass('disable-select'); + } + return body; + } + + refreshAssetsInFile(fileid : string, data : FileData) : number { + let nassets = 0; + // TODO: check fmt w/h/etc limits + // TODO: defer editor creation + // TODO: only refresh when needed + if (platform_id.startsWith('nes') && fileid.endsWith('.chr') && data instanceof Uint8Array) { + // is this a NES CHR? + let node = new pixed.FileDataNode(projectWindows, fileid); + const neschrfmt = {w:8,h:8,bpp:1,count:(data.length>>4),brev:true,np:2,pofs:8,remap:[0,1,2,4,5,6,7,8,9,10,11,12]}; // TODO + this.addPixelEditor(this.ensureFileDiv(fileid), node, neschrfmt); + this.registerAsset("charmap", node, 1); + nassets++; + } else if (platform_id.startsWith('nes') && fileid.endsWith('.pal') && data instanceof Uint8Array) { + // is this a NES PAL? + let node = new pixed.FileDataNode(projectWindows, fileid); + const nespalfmt = {pal:"nes",layout:"nes"}; + this.addPaletteEditor(this.ensureFileDiv(fileid), node, nespalfmt); + this.registerAsset("palette", node, 0); + nassets++; + } else if (typeof data === 'string') { + let textfrags = this.scanFileTextForAssets(fileid, data); + for (let frag of textfrags) { + if (frag.fmt) { + let label = fileid; // TODO: label + let node : pixed.PixNode = new pixed.TextDataNode(projectWindows, fileid, label, frag.start, frag.end); + let first = node; + // rle-compressed? TODO: how to edit? + if (frag.fmt.comp == 'rletag') { + node = node.addRight(new pixed.Compressor()); + } + // is this a nes nametable? + if (frag.fmt.map == 'nesnt') { + node = node.addRight(new pixed.NESNametableConverter(this)); + node = node.addRight(new pixed.Palettizer(this, {w:8,h:8,bpp:4})); + const fmt = {w:8*(frag.fmt.w||32),h:8*(frag.fmt.h||30),count:1}; // TODO: can't do custom sizes + node = node.addRight(new pixed.MapEditor(this, newDiv(this.ensureFileDiv(fileid)), fmt)); + this.registerAsset("nametable", first, 2); + nassets++; + } + // is this a bitmap? + else if (frag.fmt.w > 0 && frag.fmt.h > 0) { + this.addPixelEditor(this.ensureFileDiv(fileid), node, frag.fmt); + this.registerAsset("charmap", first, 1); + nassets++; + } + // is this a palette? + else if (frag.fmt.pal) { + this.addPaletteEditor(this.ensureFileDiv(fileid), node, frag.fmt); + this.registerAsset("palette", first, 0); + nassets++; + } + else { + // TODO: other kinds of resources? + } + } + } + } + return nassets; + } + + getFileDivId(id : string) { + return '__asset__' + safeident(id); + } + + // TODO: recreate editors when refreshing + // TODO: look for changes, not moveCursor + refresh(moveCursor : boolean) { + // clear and refresh all files/nodes? + if (moveCursor) { + this.maindiv.empty(); + this.clearAssets(); + current_project.iterateFiles((fileid, data) => { + try { + var nassets = this.refreshAssetsInFile(fileid, data); + } catch (e) { + console.log(e); + this.ensureFileDiv(fileid).text(e+""); // TODO: error msg? + } + }); + console.log("Found " + this.rootnodes.length + " assets"); + this.deferrednodes.forEach((node) => { + try { + node.refreshRight(); + } catch (e) { + console.log(e); + alert(e+""); + } + }); + this.deferrednodes = []; + } else { + // only refresh nodes if not actively editing + // since we could be in the middle of an operation that hasn't been committed + for (var node of this.rootnodes) { + if (node !== this.getCurrentEditNode()) { + node.refreshRight(); + } + } + } + } + + setVisible?(showing : boolean) : void { + // TODO: make into toolbar? + if (showing) { + if (Mousetrap.bind) Mousetrap.bind('ctrl+z', projectWindows.undoStep.bind(projectWindows)); + } else { + if (Mousetrap.unbind) Mousetrap.unbind('ctrl+z'); + } + } + + } + diff --git a/src/ide/views/baseviews.ts b/src/ide/views/baseviews.ts new file mode 100644 index 00000000..4b87f647 --- /dev/null +++ b/src/ide/views/baseviews.ts @@ -0,0 +1,33 @@ + +import { CodeAnalyzer } from "../../common/analysis"; +import { SourceFile, WorkerError } from "../../common/workertypes"; + +export interface ProjectView { + createDiv(parent: HTMLElement): HTMLElement; + setVisible?(showing: boolean): void; + refresh(moveCursor: boolean): void; + tick?(): void; + getPath?(): string; + getValue?(): string; + setText?(text: string): void; + insertText?(text: string): void; + getCursorPC?(): number; + getSourceFile?(): SourceFile; + setGutterBytes?(line: number, s: string): void; + markErrors?(errors: WorkerError[]): void; + clearErrors?(): void; + setTimingResult?(result: CodeAnalyzer): void; + recreateOnResize?: boolean; + undoStep?(): void; +}; + +// detect mobile (https://stackoverflow.com/questions/3514784/what-is-the-best-way-to-detect-a-mobile-device) +export var isMobileDevice = window.matchMedia && window.matchMedia("only screen and (max-width: 760px)").matches; + +export function newDiv(parent?, cls?: string) { + var div = $(document.createElement("div")); + if (parent) div.appendTo(parent) + if (cls) div.addClass(cls); + return div; +} + diff --git a/src/ide/views/debugviews.ts b/src/ide/views/debugviews.ts new file mode 100644 index 00000000..8fec2c1f --- /dev/null +++ b/src/ide/views/debugviews.ts @@ -0,0 +1,796 @@ + +import { newDiv, ProjectView } from "./baseviews"; +import { Segment } from "../../common/workertypes"; +import { platform, compparams, current_project, projectWindows } from "../ui"; +import { hex, lpad, rpad } from "../../common/util"; +import { VirtualList } from "../../common/vlist"; +import { getMousePos, getVisibleEditorLineHeight, VirtualTextLine, VirtualTextScroller } from "../../common/emu"; +import { ProbeFlags, ProbeRecorder } from "../../common/recorder"; +import { BaseZ80MachinePlatform, BaseZ80Platform } from "../../common/baseplatform"; + +/// + +function ignoreSymbol(sym:string) { + return sym.endsWith('_SIZE__') || sym.endsWith('_LAST__') || sym.endsWith('STACKSIZE__') || sym.endsWith('FILEOFFS__') + || sym.startsWith('l__') || sym.startsWith('s__') || sym.startsWith('.__.'); +} + +// TODO: make it use debug state +// TODO: make it safe (load/restore state?) +// TODO: refactor w/ VirtualTextLine +export class MemoryView implements ProjectView { + memorylist; + dumplines; + maindiv : HTMLElement; + recreateOnResize = true; + totalRows = 0x1400; + + createDiv(parent : HTMLElement) { + var div = document.createElement('div'); + div.setAttribute("class", "memdump"); + parent.appendChild(div); + this.showMemoryWindow(parent, div); + return this.maindiv = div; + } + + showMemoryWindow(workspace:HTMLElement, parent:HTMLElement) { + this.memorylist = new VirtualList({ + w: $(workspace).width(), + h: $(workspace).height(), + itemHeight: getVisibleEditorLineHeight(), + totalRows: this.totalRows, + generatorFn: (row : number) => { + var s = this.getMemoryLineAt(row); + var linediv = document.createElement("div"); + if (this.dumplines) { + var dlr = this.dumplines[row]; + if (dlr) linediv.classList.add('seg_' + this.getMemorySegment(this.dumplines[row].a)); + } + linediv.appendChild(document.createTextNode(s)); + return linediv; + } + }); + $(parent).append(this.memorylist.container); + this.tick(); + if (compparams && this.dumplines) + this.scrollToAddress(compparams.data_start); + } + + scrollToAddress(addr : number) { + if (this.dumplines) { + this.memorylist.scrollToItem(this.findMemoryWindowLine(addr)); + } + } + + refresh() { + this.dumplines = null; + this.tick(); + } + + tick() { + if (this.memorylist) { + $(this.maindiv).find('[data-index]').each( (i,e) => { + var div = $(e); + var row = parseInt(div.attr('data-index')); + var oldtext = div.text(); + var newtext = this.getMemoryLineAt(row); + if (oldtext != newtext) + div.text(newtext); + }); + } + } + + getMemoryLineAt(row : number) : string { + var offset = row * 16; + var n1 = 0; + var n2 = 16; + var sym; + if (this.getDumpLines()) { + var dl = this.dumplines[row]; + if (dl) { + offset = dl.a & 0xfff0; + n1 = dl.a - offset; + n2 = n1 + dl.l; + sym = dl.s; + } else { + return '.'; + } + } + var s = hex(offset+n1,4) + ' '; + for (var i=0; i 8) s += ' '; + for (var i=n1; i nextofs) ofs2 = nextofs; + //if (ofs < 1000) console.log(ofs, ofs2, nextofs, sym); + this.dumplines.push({a:ofs, l:ofs2-ofs, s:sym}); + ofs = ofs2; + } + } + sym = nextsym; + } + } + return this.dumplines; + } + + // TODO: use segments list? + getMemorySegment(a:number) : string { + if (compparams) { + if (a >= compparams.data_start && a < compparams.data_start+compparams.data_size) { + if (platform.getSP && a >= platform.getSP() - 15) + return 'stack'; + else + return 'data'; + } + else if (a >= compparams.code_start && a < compparams.code_start+(compparams.code_size||compparams.rom_size)) + return 'code'; + } + var segments = current_project.segments; + if (segments) { + for (var seg of segments) { + if (a >= seg.start && a < seg.start+seg.size) { + if (seg.type == 'rom') return 'code'; + if (seg.type == 'ram') return 'data'; + if (seg.type == 'io') return 'io'; + } + } + } + return 'unknown'; + } + + findMemoryWindowLine(a:number) : number { + for (var i=0; i= a) + return i; + } +} + +export class VRAMMemoryView extends MemoryView { + totalRows = 0x800; + readAddress(n : number) { + return platform.readVRAMAddress(n); + } + getMemorySegment(a:number) : string { + return 'video'; + } + getDumpLines() { + return null; + } +} + +/// + +export class BinaryFileView implements ProjectView { + vlist : VirtualTextScroller; + maindiv : HTMLElement; + path:string; + data:Uint8Array; + recreateOnResize = true; + + constructor(path:string, data:Uint8Array) { + this.path = path; + this.data = data; + } + + createDiv(parent : HTMLElement) { + this.vlist = new VirtualTextScroller(parent); + this.vlist.create(parent, ((this.data.length+15) >> 4), this.getMemoryLineAt.bind(this)); + return this.vlist.maindiv; + } + + getMemoryLineAt(row : number) : VirtualTextLine { + var offset = row * 16; + var n1 = 0; + var n2 = 16; + var s = hex(offset+n1,4) + ' '; + for (var i=0; i 8) s += ' '; + for (var i=n1; i=0?hex(read,2):' '); + } + return {text:s}; + } + + refresh() { + this.vlist.refresh(); + } + + getPath() { return this.path; } +} + +/// + +export class MemoryMapView implements ProjectView { + maindiv : JQuery; + + createDiv(parent : HTMLElement) { + this.maindiv = newDiv(parent, 'vertical-scroll'); + this.maindiv.css('display', 'grid'); + this.maindiv.css('grid-template-columns', '5em 40% 40%'); + return this.maindiv[0]; + } + + // TODO: overlapping segments (e.g. ROM + LC) + addSegment(seg : Segment, newrow : boolean) { + if (newrow) { + var offset = $('
'); + offset.text('$'+hex(seg.start,4)); + this.maindiv.append(offset); + } + var segdiv = $('
'); + if (!newrow) + segdiv.css('grid-column-start', 3); // make sure it's on right side + if (seg.last) + segdiv.text(seg.name+" ("+(seg.last-seg.start)+" / "+seg.size+" bytes used)"); + else + segdiv.text(seg.name+" ("+seg.size+" bytes)"); + if (seg.size >= 256) { + var pad = (Math.log(seg.size) - Math.log(256)) * 0.5; + segdiv.css('padding-top', pad+'em'); + segdiv.css('padding-bottom', pad+'em'); + } + if (seg.type) { + segdiv.addClass('segment-'+seg.type); + } + this.maindiv.append(segdiv); + //var row = $('
').append(offset, segdiv); + //var container = $('
').append(row); + //this.maindiv.append(container); + segdiv.click(() => { + // TODO: what if memory browser does not exist? + var memview = projectWindows.createOrShow('#memory') as MemoryView; + memview.scrollToAddress(seg.start); + }); + } + + refresh() { + this.maindiv.empty(); + var segments = current_project.segments; + if (segments) { + var curofs = 0; + var laststart = -1; + for (var seg of segments) { + //var used = seg.last ? (seg.last-seg.start) : seg.size; + if (seg.start > curofs) + this.addSegment({name:'',start:curofs, size:seg.start-curofs}, true); + this.addSegment(seg, laststart != seg.start); + laststart = seg.start; + curofs = seg.start + seg.size; + } + } + } + +} + +/// + +// TODO: clear buffer when scrubbing + +export abstract class ProbeViewBaseBase { + probe : ProbeRecorder; + tooldiv : HTMLElement; + cumulativeData : boolean = false; + cyclesPerLine : number; + totalScanlines : number; + + abstract tick() : void; + + constructor() { + var width = 160; + var height = 262; // TODO: PAL? + try { + width = Math.ceil(platform['machine']['cpuCyclesPerLine']) || width; // TODO + height = Math.ceil(platform['machine']['numTotalScanlines']) || height; // TODO + } catch (e) { + } + this.cyclesPerLine = width; + this.totalScanlines = height; + } + + addr2symbol(addr : number) : string { + var _addr2sym = (platform.debugSymbols && platform.debugSymbols.addr2symbol) || {}; + return _addr2sym[addr]; + } + + addr2str(addr : number) : string { + var sym = this.addr2symbol(addr); + if (typeof sym === 'string') + return '$' + hex(addr) + ' (' + sym + ')'; + else + return '$' + hex(addr); + } + + showTooltip(s:string) { + if (s) { + if (!this.tooldiv) { + this.tooldiv = document.createElement("div"); + this.tooldiv.setAttribute("class", "tooltiptrack"); + document.body.appendChild(this.tooldiv); + } + $(this.tooldiv).text(s).show(); + } else { + $(this.tooldiv).hide(); + } + } + + setVisible(showing : boolean) : void { + if (showing) { + this.probe = platform.startProbing(); + this.probe.singleFrame = !this.cumulativeData; + this.tick(); + } else { + if (this.probe) this.probe.singleFrame = true; + platform.stopProbing(); + this.probe = null; + } + } + + redraw( eventfn:(op,addr,col,row,clk,value) => void ) { + var p = this.probe; + if (!p || !p.idx) return; // if no probe, or if empty + var row=0; + var col=0; + var clk=0; + for (var i=0; i> 16) & 0xff; + var op = word & 0xff000000; + switch (op) { + case ProbeFlags.SCANLINE: row++; col=0; break; + case ProbeFlags.FRAME: row=0; col=0; break; + case ProbeFlags.CLOCKS: col += addr; clk += addr; break; + default: + eventfn(op, addr, col, row, clk, value); + break; + } + } + } + + opToString(op:number, addr?:number, value?:number) { + var s = ""; + switch (op) { + case ProbeFlags.EXECUTE: s = "Exec"; break; + case ProbeFlags.MEM_READ: s = "Read"; break; + case ProbeFlags.MEM_WRITE: s = "Write"; break; + case ProbeFlags.IO_READ: s = "IO Read"; break; + case ProbeFlags.IO_WRITE: s = "IO Write"; break; + case ProbeFlags.VRAM_READ: s = "VRAM Read"; break; + case ProbeFlags.VRAM_WRITE: s = "VRAM Write"; break; + case ProbeFlags.INTERRUPT: s = "Interrupt"; break; + case ProbeFlags.ILLEGAL: s = "Error"; break; + case ProbeFlags.SP_PUSH: s = "Stack Push"; break; + case ProbeFlags.SP_POP: s = "Stack Pop"; break; + default: return ""; + } + if (typeof addr == 'number') s += " " + this.addr2str(addr); + if ((op & ProbeFlags.HAS_VALUE) && typeof value == 'number') s += " = $" + hex(value,2); + return s; + } + + getOpRGB(op:number) : number { + switch (op) { + case ProbeFlags.EXECUTE: return 0x018001; + case ProbeFlags.MEM_READ: return 0x800101; + case ProbeFlags.MEM_WRITE: return 0x010180; + case ProbeFlags.IO_READ: return 0x018080; + case ProbeFlags.IO_WRITE: return 0xc00180; + case ProbeFlags.VRAM_READ: return 0x808001; + case ProbeFlags.VRAM_WRITE: return 0x4080c0; + case ProbeFlags.INTERRUPT: return 0xcfcfcf; + case ProbeFlags.ILLEGAL: return 0x3f3fff; + default: return 0; + } + } +} + +abstract class ProbeViewBase extends ProbeViewBaseBase { + + maindiv : HTMLElement; + canvas : HTMLCanvasElement; + ctx : CanvasRenderingContext2D; + recreateOnResize = true; + + abstract drawEvent(op, addr, col, row); + + createCanvas(parent:HTMLElement, width:number, height:number) { + var div = document.createElement('div'); + var canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + canvas.classList.add('pixelated'); + canvas.style.width = '100%'; + canvas.style.height = '90vh'; // i hate css + canvas.style.backgroundColor = 'black'; + canvas.style.cursor = 'crosshair'; + canvas.onmousemove = (e) => { + var pos = getMousePos(canvas, e); + this.showTooltip(this.getTooltipText(pos.x, pos.y)); + $(this.tooldiv).css('left',e.pageX+10).css('top',e.pageY-30); + } + canvas.onmouseout = (e) => { + $(this.tooldiv).hide(); + } + parent.appendChild(div); + div.appendChild(canvas); + this.canvas = canvas; + this.ctx = canvas.getContext('2d'); + this.initCanvas(); + return this.maindiv = div; + } + + initCanvas() { + } + + getTooltipText(x:number, y:number) : string { + return null; + } + + clear() { + } + + tick() { + this.clear(); + this.redraw(this.drawEvent.bind(this)); + } +} + +abstract class ProbeBitmapViewBase extends ProbeViewBase { + + imageData : ImageData; + datau32 : Uint32Array; + recreateOnResize = false; + + createDiv(parent : HTMLElement) { + return this.createCanvas(parent, this.cyclesPerLine, this.totalScanlines); + } + initCanvas() { + this.imageData = this.ctx.createImageData(this.canvas.width, this.canvas.height); + this.datau32 = new Uint32Array(this.imageData.data.buffer); + } + getTooltipText(x:number, y:number) : string { + x = x|0; + y = y|0; + var s = ""; + var lastroutine = null; + var symstack = []; + var lastcol = -1; + this.redraw( (op,addr,col,row,clk,value) => { + switch (op) { + case ProbeFlags.EXECUTE: + lastroutine = this.addr2symbol(addr) || lastroutine; + break; + case ProbeFlags.SP_PUSH: + symstack.push(lastroutine); + break; + case ProbeFlags.SP_POP: + lastroutine = symstack.pop(); + break; + } + if (row == y && col <= x) { + if (col != lastcol) { + s = ""; + lastcol = col; + } + if (s == "" && lastroutine) { s += "\n" + lastroutine; } + s += "\n" + this.opToString(op, addr, value); + } + } ); + return 'X: ' + x + ' Y: ' + y + ' ' + s; + } + + refresh() { + this.tick(); + this.datau32.fill(0xff000000); + } + tick() { + super.tick(); + this.ctx.putImageData(this.imageData, 0, 0); + } + clear() { + this.datau32.fill(0xff000000); + } +} + +export class AddressHeatMapView extends ProbeBitmapViewBase implements ProjectView { + + createDiv(parent : HTMLElement) { + return this.createCanvas(parent, 256, 256); + } + + clear() { + for (var i=0; i<=0xffff; i++) { + var v = platform.readAddress(i); + var rgb = (v >> 2) | (v & 0x1f); + rgb |= (rgb<<8) | (rgb<<16); + this.datau32[i] = rgb | 0xff000000; + } + } + + drawEvent(op, addr, col, row) { + var rgb = this.getOpRGB(op); + if (!rgb) return; + var x = addr & 0xff; + var y = (addr >> 8) & 0xff; + var data = this.datau32[addr & 0xffff]; + data = data | rgb | 0xff000000; + this.datau32[addr & 0xffff] = data; + } + + getTooltipText(x:number, y:number) : string { + var a = (x & 0xff) + (y << 8); + var s = ""; + var pc = -1; + var already = {}; + var lastroutine = null; + var symstack = []; + this.redraw( (op,addr,col,row,clk,value) => { + switch (op) { + case ProbeFlags.EXECUTE: + pc = addr; + lastroutine = this.addr2symbol(addr) || lastroutine; + break; + case ProbeFlags.SP_PUSH: + symstack.push(lastroutine); + break; + case ProbeFlags.SP_POP: + lastroutine = symstack.pop(); + break; + } + var key = op|pc; + if (addr == a && !already[key]) { + if (s == "" && lastroutine) { s += "\n" + lastroutine; } + s += "\nPC " + this.addr2str(pc) + " " + this.opToString(op, null, value); + already[key] = 1; + } + } ); + return this.addr2str(a) + s; + } +} + +export class RasterPCHeatMapView extends ProbeBitmapViewBase implements ProjectView { + + drawEvent(op, addr, col, row) { + var iofs = col + row * this.canvas.width; + var rgb = this.getOpRGB(op); + if (!rgb) return; + var data = this.datau32[iofs]; + data = data | rgb | 0xff000000; + this.datau32[iofs] = data; + } +} + +export class ProbeLogView extends ProbeViewBaseBase { + vlist : VirtualTextScroller; + maindiv : HTMLElement; + recreateOnResize = true; + dumplines; + + createDiv(parent : HTMLElement) { + this.vlist = new VirtualTextScroller(parent); + this.vlist.create(parent, this.cyclesPerLine*this.totalScanlines, this.getMemoryLineAt.bind(this)); + return this.vlist.maindiv; + } + getMemoryLineAt(row : number) : VirtualTextLine { + var s : string = ""; + var c : string = "seg_data"; + var line = this.dumplines && this.dumplines[row]; + if (line != null) { + var xtra : string = line.info.join(", "); + s = "(" + lpad(line.row,4) + ", " + lpad(line.col,4) + ") " + rpad(line.asm||"",20) + xtra; + if (xtra.indexOf("Write ") >= 0) c = "seg_io"; + // if (xtra.indexOf("Stack ") >= 0) c = "seg_code"; + } + return {text:s, clas:c}; + } + refresh() { + this.tick(); + } + tick() { + const isz80 = platform instanceof BaseZ80MachinePlatform || platform instanceof BaseZ80Platform; // TODO? + // cache each line in frame + this.dumplines = {}; + this.redraw((op,addr,col,row,clk,value) => { + if (isz80) clk >>= 2; + var line = this.dumplines[clk]; + if (line == null) { + line = {op:op, addr:addr, row:row, col:col, asm:null, info:[]}; + this.dumplines[clk] = line; + } + switch (op) { + case ProbeFlags.EXECUTE: + if (platform.disassemble) { + var disasm = platform.disassemble(addr, platform.readAddress.bind(platform)); + line.asm = disasm && disasm.line; + } + break; + default: + var xtra = this.opToString(op, addr, value); + if (xtra != "") line.info.push(xtra); + break; + } + }); + this.vlist.refresh(); + } +} + +export class ScanlineIOView extends ProbeViewBaseBase { + vlist : VirtualTextScroller; + maindiv : HTMLElement; + recreateOnResize = true; + dumplines; + + createDiv(parent : HTMLElement) { + this.vlist = new VirtualTextScroller(parent); + this.vlist.create(parent, this.totalScanlines, this.getMemoryLineAt.bind(this)); + return this.vlist.maindiv; + } + getMemoryLineAt(row : number) : VirtualTextLine { + var s = lpad(row+"",3) + ' '; + var c = 'seg_code'; + var line = (this.dumplines && this.dumplines[row]) || []; + var hblankCycle = Math.round(this.cyclesPerLine/3.3); + for (var i=0; i { + var line = this.dumplines[row]; + if (line == null) { + this.dumplines[row] = line = []; + } + switch (op) { + case ProbeFlags.EXECUTE: + var sym = platform.debugSymbols.addr2symbol[addr]; + if (sym) line[-1] = sym; + break; + case ProbeFlags.MEM_WRITE: + case ProbeFlags.IO_READ: + case ProbeFlags.IO_WRITE: + case ProbeFlags.VRAM_READ: + case ProbeFlags.VRAM_WRITE: + line[col] = op | addr; + break; + } + }); + this.vlist.refresh(); + } +} + +/// + +export class ProbeSymbolView extends ProbeViewBaseBase { + vlist : VirtualTextScroller; + keys : string[]; + recreateOnResize = true; + dumplines; + cumulativeData = true; + + // TODO: auto resize + createDiv(parent : HTMLElement) { + // TODO: what if symbol list changes? + if (platform.debugSymbols && platform.debugSymbols.symbolmap) { + this.keys = Array.from(Object.keys(platform.debugSymbols.symbolmap).filter(sym => !ignoreSymbol(sym))); + } else { + this.keys = ['no symbols defined']; + } + this.vlist = new VirtualTextScroller(parent); + this.vlist.create(parent, this.keys.length + 1, this.getMemoryLineAt.bind(this)); + return this.vlist.maindiv; + } + + getMemoryLineAt(row : number) : VirtualTextLine { + // header line + if (row == 0) { + return {text: lpad("Symbol",35)+lpad("Reads",8)+lpad("Writes",8)}; + } + var sym = this.keys[row-1]; + var line = this.dumplines && this.dumplines[sym]; + function getop(op) { + var n = line[op] | 0; + return lpad(n ? n.toString() : "", 8); + } + var s : string; + var c : string; + if (line != null) { + s = lpad(sym, 35) + + getop(ProbeFlags.MEM_READ) + + getop(ProbeFlags.MEM_WRITE); + if (line[ProbeFlags.EXECUTE]) + c = 'seg_code'; + else if (line[ProbeFlags.IO_READ] || line[ProbeFlags.IO_WRITE]) + c = 'seg_io'; + else + c = 'seg_data'; + } else { + s = lpad(sym, 35); + c = 'seg_unknown'; + } + return {text:s, clas:c}; + } + + refresh() { + this.tick(); + } + + tick() { + // cache each line in frame + this.dumplines = {}; + this.redraw((op,addr,col,row,clk,value) => { + var sym = platform.debugSymbols.addr2symbol[addr]; + if (sym != null) { + var line = this.dumplines[sym]; + if (line == null) { + line = {}; + this.dumplines[sym] = line; + } + line[op] = (line[op] | 0) + 1; + } + }); + this.vlist.refresh(); + if (this.probe) this.probe.clear(); // clear cumulative data (TODO: doesnt work with seeking or debugging) + } +} + +/// + diff --git a/src/ide/views/editors.ts b/src/ide/views/editors.ts new file mode 100644 index 00000000..a1aa2fdf --- /dev/null +++ b/src/ide/views/editors.ts @@ -0,0 +1,583 @@ + +import { isMobileDevice, ProjectView } from "./baseviews"; +import { SourceFile, WorkerError, SourceLocation } from "../../common/workertypes"; +import { CodeAnalyzer } from "../../common/analysis"; +import { platform, current_project, lastDebugState, runToPC, qs } from "../ui"; +import { hex, rpad } from "../../common/util"; + +declare var CodeMirror; + +// helper function for editor +function jumpToLine(ed, i:number) { + var t = ed.charCoords({line: i, ch: 0}, "local").top; + var middleHeight = ed.getScrollerElement().offsetHeight / 2; + ed.scrollTo(null, t - middleHeight - 5); +} + +function createTextSpan(text:string, className:string) : HTMLElement { + var span = document.createElement("span"); + span.setAttribute("class", className); + span.appendChild(document.createTextNode(text)); + return span; +} + +///// + +const MAX_ERRORS = 200; + +const MODEDEFS = { + default: { theme: 'mbo' }, // NOTE: Not merged w/ other modes + '6502': { isAsm: true }, + z80: { isAsm: true }, + jsasm: { isAsm: true }, + gas: { isAsm: true }, + vasm: { isAsm: true }, + inform6: { theme: 'cobalt' }, + markdown: { lineWrap: true }, + fastbasic: { noGutters: true }, + basic: { noLineNumbers: true, noGutters: true }, // TODO: not used? +} + +export var textMapFunctions = { + input: null +}; + +export class SourceEditor implements ProjectView { + constructor(path:string, mode:string) { + this.path = path; + this.mode = mode; + } + path : string; + mode : string; + editor; + dirtylisting = true; + sourcefile : SourceFile; + currentDebugLine : SourceLocation; + markCurrentPC; // TextMarker + errormsgs = []; + errorwidgets = []; + errormarks = []; + inspectWidget; + + createDiv(parent:HTMLElement) { + var div = document.createElement('div'); + div.setAttribute("class", "editor"); + parent.appendChild(div); + var text = current_project.getFile(this.path) as string; + var asmOverride = text && this.mode=='verilog' && /__asm\b([\s\S]+?)\b__endasm\b/.test(text); + this.newEditor(div, asmOverride); + if (text) { + this.setText(text); // TODO: this calls setCode() and builds... it shouldn't + this.editor.setSelection({line:0,ch:0}, {line:0,ch:0}, {scroll:true}); // move cursor to start + } + this.setupEditor(); + return div; + } + + newEditor(parent:HTMLElement, isAsmOverride?:boolean) { + var modedef = MODEDEFS[this.mode] || MODEDEFS.default; + var isAsm = isAsmOverride || modedef.isAsm; + var lineWrap = !!modedef.lineWrap; + var theme = modedef.theme || MODEDEFS.default.theme; + var lineNums = !modedef.noLineNumbers && !isMobileDevice; + if (qs['embed']) { + lineNums = false; // no line numbers while embedded + isAsm = false; // no opcode bytes either + } + var gutters = ["CodeMirror-linenumbers", "gutter-offset", "gutter-info"]; + if (isAsm) gutters = ["CodeMirror-linenumbers", "gutter-offset", "gutter-bytes", "gutter-clock", "gutter-info"]; + if (modedef.noGutters || isMobileDevice) gutters = ["gutter-info"]; + this.editor = CodeMirror(parent, { + theme: theme, + lineNumbers: lineNums, + matchBrackets: true, + tabSize: 8, + indentAuto: true, + lineWrapping: lineWrap, + gutters: gutters + }); + } + + setupEditor() { + var timer; + // update file in project (and recompile) when edits made + this.editor.on('changes', (ed, changeobj) => { + clearTimeout(timer); + timer = setTimeout( () => { + current_project.updateFile(this.path, this.editor.getValue()); + }, 300); + }); + // inspect symbol when it's highlighted (double-click) + this.editor.on('cursorActivity', (ed) => { + var start = this.editor.getCursor(true); + var end = this.editor.getCursor(false); + if (start.line == end.line && start.ch < end.ch && end.ch-start.ch < 80) { + var name = this.editor.getSelection(); + this.inspect(name); + } else { + this.inspect(null); + } + }); + // gutter clicked + this.editor.on("gutterClick", (cm, n) => { + this.toggleBreakpoint(n); + }); + // set editor mode for highlighting, etc + this.editor.setOption("mode", this.mode); + // change text? + this.editor.on('beforeChange', (cm, chgobj) => { + if (textMapFunctions.input && chgobj.text) chgobj.text = chgobj.text.map(textMapFunctions.input); + }); + } + + + inspect(ident : string) : void { + var result; + if (platform.inspect) { + result = platform.inspect(ident); + } + if (this.inspectWidget) { + this.inspectWidget.clear(); + this.inspectWidget = null; + } + if (result) { + var infospan = createTextSpan(result, "tooltipinfoline"); + var line = this.editor.getCursor().line; + this.inspectWidget = this.editor.addLineWidget(line, infospan, {above:false}); + } + } + + setText(text:string) { + var i,j; + var oldtext = this.editor.getValue(); + if (oldtext != text) { + this.editor.setValue(text); + /* + // find minimum range to undo + for (i=0; i= numLines) line = 0; + this.addErrorMarker(line, info.msg); + if (info.start != null) { + var markOpts = {className:"mark-error", inclusiveLeft:true}; + var start = {line:line, ch:info.end?info.start:info.start-1}; + var end = {line:line, ch:info.end?info.end:info.start}; + var mark = this.editor.markText(start, end, markOpts); + this.errormarks.push(mark); + } + } + } + + addErrorMarker(line:number, msg:string) { + var div = document.createElement("div"); + div.setAttribute("class", "tooltipbox tooltiperror"); + div.appendChild(document.createTextNode("\u24cd")); + this.editor.setGutterMarker(line, "gutter-info", div); + this.errormsgs.push({line:line, msg:msg}); + // expand line widgets when mousing over errors + $(div).mouseover((e) => { + this.expandErrors(); + }); + } + + addErrorLine(line:number, msg:string) { + var errspan = createTextSpan(msg, "tooltiperrorline"); + this.errorwidgets.push(this.editor.addLineWidget(line, errspan)); + } + + expandErrors() { + var e; + while (e = this.errormsgs.shift()) { + this.addErrorLine(e.line, e.msg); + } + } + + markErrors(errors:WorkerError[]) { + // TODO: move cursor to error line if offscreen? + this.clearErrors(); + errors = errors.slice(0, MAX_ERRORS); + for (var info of errors) { + this.addError(info); + } + } + + clearErrors() { + this.dirtylisting = true; + // clear line widgets + this.editor.clearGutter("gutter-info"); + this.errormsgs = []; + while (this.errorwidgets.length) this.errorwidgets.shift().clear(); + while (this.errormarks.length) this.errormarks.shift().clear(); + } + + getSourceFile() : SourceFile { return this.sourcefile; } + + updateListing() { + // update editor annotations + // TODO: recreate editor if gutter-bytes is used (verilog) + this.clearErrors(); + this.editor.clearGutter("gutter-bytes"); + this.editor.clearGutter("gutter-offset"); + this.editor.clearGutter("gutter-clock"); + var lstlines = this.sourcefile.lines || []; + for (var info of lstlines) { + if (info.offset >= 0) { + this.setGutter("gutter-offset", info.line-1, hex(info.offset&0xffff,4)); + } + if (info.insns) { + var insnstr = info.insns.length > 9 ? ("...") : info.insns; + this.setGutter("gutter-bytes", info.line-1, insnstr); + if (info.iscode) { + // TODO: labels trick this part? + if (info.cycles) { + this.setGutter("gutter-clock", info.line-1, info.cycles+""); + } else if (platform.getOpcodeMetadata) { + var opcode = parseInt(info.insns.split(" ")[0], 16); + var meta = platform.getOpcodeMetadata(opcode, info.offset); + if (meta && meta.minCycles) { + var clockstr = meta.minCycles+""; + this.setGutter("gutter-clock", info.line-1, clockstr); + } + } + } + } + } + } + + setGutter(type:string, line:number, text:string) { + var lineinfo = this.editor.lineInfo(line); + if (lineinfo && lineinfo.gutterMarkers && lineinfo.gutterMarkers[type]) { + // do not replace existing marker + } else { + var textel = document.createTextNode(text); + this.editor.setGutterMarker(line, type, textel); + } + } + + setGutterBytes(line:number, s:string) { + this.setGutter("gutter-bytes", line-1, s); + } + + setTimingResult(result:CodeAnalyzer) : void { + this.editor.clearGutter("gutter-bytes"); + if (this.sourcefile == null) return; + // show the lines + for (const line of Object.keys(this.sourcefile.line2offset)) { + var pc = this.sourcefile.line2offset[line]; + var minclocks = result.pc2minclocks[pc]; + var maxclocks = result.pc2maxclocks[pc]; + if (minclocks>=0 && maxclocks>=0) { + var s; + if (maxclocks == minclocks) + s = minclocks + ""; + else + s = minclocks + "-" + maxclocks; + if (maxclocks == result.MAX_CLOCKS) + s += "+"; + this.setGutterBytes(parseInt(line), s); + } + } + } + + setCurrentLine(line:SourceLocation, moveCursor:boolean) { + var blocked = platform.isBlocked && platform.isBlocked(); + + var addCurrentMarker = (line:SourceLocation) => { + var div = document.createElement("div"); + var cls = blocked ? 'currentpc-marker-blocked' : 'currentpc-marker'; + div.classList.add(cls); + div.appendChild(document.createTextNode("\u25b6")); + this.editor.setGutterMarker(line.line-1, "gutter-info", div); + } + + this.clearCurrentLine(moveCursor); + if (line) { + addCurrentMarker(line); + if (moveCursor) { + this.editor.setCursor({line:line.line-1,ch:line.start||0}, {scroll:true}); + } + var cls = blocked ? 'currentpc-span-blocked' : 'currentpc-span'; + var markOpts = {className:cls, inclusiveLeft:true}; + if (line.start || line.end) + this.markCurrentPC = this.editor.markText({line:line.line-1,ch:line.start}, {line:line.line-1,ch:line.end||line.start+1}, markOpts); + else + this.markCurrentPC = this.editor.markText({line:line.line-1,ch:0}, {line:line.line,ch:0}, markOpts); + this.currentDebugLine = line; + } + } + + clearCurrentLine(moveCursor:boolean) { + if (this.currentDebugLine) { + this.editor.clearGutter("gutter-info"); + if (moveCursor) this.editor.setSelection(this.editor.getCursor()); + this.currentDebugLine = null; + } + if (this.markCurrentPC) { + this.markCurrentPC.clear(); + this.markCurrentPC = null; + } + } + + getActiveLine() : SourceLocation { + if (this.sourcefile) { + var cpustate = lastDebugState && lastDebugState.c; + if (!cpustate && platform.getCPUState && !platform.isRunning()) + cpustate = platform.getCPUState(); + if (cpustate) { + var EPC = (cpustate && (cpustate.EPC || cpustate.PC)); + var res = this.sourcefile.findLineForOffset(EPC, 15); + return res; + } + } + } + + refreshDebugState(moveCursor:boolean) { + // TODO: only if line changed + // TODO: remove after compilation + this.clearCurrentLine(moveCursor); + var line = this.getActiveLine(); + if (line) { + this.setCurrentLine(line, moveCursor); + } + } + + refreshListing() { + // lookup corresponding sourcefile for this file, using listing + var lst = current_project.getListingForFile(this.path); + if (lst && lst.sourcefile && lst.sourcefile !== this.sourcefile) { + this.sourcefile = lst.sourcefile; + this.dirtylisting = true; + } + if (!this.sourcefile || !this.dirtylisting) return; + this.updateListing(); + this.dirtylisting = false; + } + + refresh(moveCursor: boolean) { + this.refreshListing(); + this.refreshDebugState(moveCursor); + } + + tick() { + this.refreshDebugState(false); + } + + getLine(line : number) { + return this.editor.getLine(line-1); + } + + getCurrentLine() : number { + return this.editor.getCursor().line+1; + } + + getCursorPC() : number { + var line = this.getCurrentLine(); + while (this.sourcefile && line >= 0) { + var pc = this.sourcefile.line2offset[line]; + if (pc >= 0) return pc; + line--; + } + return -1; + } + + undoStep() { + this.editor.execCommand('undo'); + } + + toggleBreakpoint(lineno: number) { + // TODO: we have to always start at beginning of frame + if (this.sourcefile != null) { + var targetPC = this.sourcefile.line2offset[lineno+1]; + /* + var bpid = "pc" + targetPC; + if (platform.hasBreakpoint(bpid)) { + platform.clearBreakpoint(bpid); + } else { + platform.setBreakpoint(bpid, () => { + return platform.getPC() == targetPC; + }); + } + */ + runToPC(targetPC); + } + } +} + +/// + +const disasmWindow = 1024; // disassemble this many bytes around cursor + +export class DisassemblerView implements ProjectView { + disasmview; + + getDisasmView() { return this.disasmview; } + + createDiv(parent : HTMLElement) { + var div = document.createElement('div'); + div.setAttribute("class", "editor"); + parent.appendChild(div); + this.newEditor(div); + return div; + } + + newEditor(parent : HTMLElement) { + this.disasmview = CodeMirror(parent, { + mode: 'z80', // TODO: pick correct one + theme: 'cobalt', + tabSize: 8, + readOnly: true, + styleActiveLine: true + }); + } + + // TODO: too many globals + refresh(moveCursor: boolean) { + let state = lastDebugState || platform.saveState(); // TODO? + let pc = state.c ? state.c.PC : 0; + let curline = 0; + let selline = 0; + let addr2symbol = (platform.debugSymbols && platform.debugSymbols.addr2symbol) || {}; + // TODO: not perfect disassembler + let disassemble = (start, len) => { + // TODO: use pc2visits + let s = ""; + let ofs = 0; + while (ofs < len) { + let a = (start + ofs) | 0; + let disasm = platform.disassemble(a, platform.readAddress.bind(platform)); + /* TODO: look thru all source files + let srclinenum = sourcefile && this.sourcefile.offset2line[a]; + if (srclinenum) { + let srcline = getActiveEditor().getLine(srclinenum); + if (srcline && srcline.trim().length) { + s += "; " + srclinenum + ":\t" + srcline + "\n"; + curline++; + } + } + */ + let bytes = ""; + let comment = ""; + for (let i=0; i { + let addr = parseInt(args[1], 16); + let sym = addr2symbol[addr]; + if (sym) return (args[0] + sym); + sym = addr2symbol[addr-1]; + if (sym) return (args[0] + sym + "+1"); + return substr; + }); + } + if (addr2symbol) { + let sym = addr2symbol[a]; + if (sym) { + comment = "; " + sym; + } + } + let dline = hex(a, 4) + "\t" + rpad(bytes,14) + "\t" + rpad(dstr,30) + comment + "\n"; + s += dline; + if (a == pc) selline = curline; + curline++; + ofs += disasm.nbytes || 1; + } + return s; + } + var startpc = pc < 0 ? pc-disasmWindow : Math.max(0, pc-disasmWindow); // for 32-bit PCs w/ hi bit set + let text = disassemble(startpc, pc-startpc) + disassemble(pc, disasmWindow); + this.disasmview.setValue(text); + if (moveCursor) { + this.disasmview.setCursor(selline, 0); + } + jumpToLine(this.disasmview, selline); + } + + getCursorPC() : number { + var line = this.disasmview.getCursor().line; + if (line >= 0) { + var toks = this.disasmview.getLine(line).trim().split(/\s+/); + if (toks && toks.length >= 1) { + var pc = parseInt(toks[0], 16); + if (pc >= 0) return pc; + } + } + return -1; + } +} + +/// + +export class ListingView extends DisassemblerView implements ProjectView { + assemblyfile : SourceFile; + path : string; + + constructor(lstfn : string) { + super(); + this.path = lstfn; + } + + refreshListing() { + // lookup corresponding assemblyfile for this file, using listing + var lst = current_project.getListingForFile(this.path); + // TODO? + this.assemblyfile = lst && (lst.assemblyfile || lst.sourcefile); + } + + refresh(moveCursor: boolean) { + this.refreshListing(); + // load listing text into editor + if (!this.assemblyfile) return; + var asmtext = this.assemblyfile.text; + var disasmview = this.getDisasmView(); + disasmview.setValue(asmtext); + // go to PC + if (!platform.saveState) return; + var state = lastDebugState || platform.saveState(); + var pc = state.c ? (state.c.EPC || state.c.PC) : 0; + if (pc >= 0 && this.assemblyfile) { + var res = this.assemblyfile.findLineForOffset(pc, 15); + if (res) { + // set cursor while debugging + if (moveCursor) { + disasmview.setCursor(res.line-1, 0); + } + jumpToLine(disasmview, res.line-1); + } + } + } +} + diff --git a/src/ide/views/treeviews.ts b/src/ide/views/treeviews.ts new file mode 100644 index 00000000..b6c54a36 --- /dev/null +++ b/src/ide/views/treeviews.ts @@ -0,0 +1,350 @@ +import { dumpRAM } from "../../common/emu"; +import { ProbeFlags } from "../../common/recorder"; +import { hex } from "../../common/util"; +import { platform } from "../ui"; +import { ProjectView } from "./baseviews"; +import { ProbeViewBaseBase } from "./debugviews"; + +const MAX_CHILDREN = 256; +const MAX_STRING_LEN = 100; + +var TREE_SHOW_DOLLAR_IDENTS = false; + +class TreeNode { + parent : TreeNode; + name : string; + _div : HTMLElement; + _header : HTMLElement; + _inline : HTMLElement; + _content : HTMLElement; + children : Map; + expanded = false; + level : number; + view : ProjectView; + + constructor(parent : TreeNode, name : string) { + this.parent = parent; + this.name = name; + this.children = new Map(); + this.level = parent ? (parent.level+1) : -1; + this.view = parent ? parent.view : null; + } + getDiv() { + if (this._div == null) { + this._div = document.createElement("div"); + this._div.classList.add("vertical-scroll"); + this._div.classList.add("tree-content"); + this._header = document.createElement("div"); + this._header.classList.add("tree-header"); + this._header.classList.add("tree-level-" + this.level); + this._header.append(this.name); + this._inline = document.createElement("span"); + this._inline.classList.add("tree-value"); + this._header.append(this._inline); + this._div.append(this._header); + this.parent._content.append(this._div); + this._header.onclick = (e) => { + this.toggleExpanded(); + }; + } + if (this.expanded && this._content == null) { + this._content = document.createElement("div"); + this._div.append(this._content); + } + else if (!this.expanded && this._content != null) { + this._content.remove(); + this._content = null; + this.children.clear(); + } + return this._div; + } + toggleExpanded() { + this.expanded = !this.expanded; + this.view.tick(); + } + remove() { + this._div.remove(); + this._div = null; + } + update(obj : any) { + this.getDiv(); + var text = ""; + // is it a function? call it first, if we are expanded + // TODO: only call functions w/ signature + if (obj && obj.$$ && typeof obj.$$ == 'function' && this._content != null) { + obj = obj.$$(); + } + // check null first + if (obj == null) { + text = obj+""; + // primitive types + } else if (typeof obj == 'number') { + if (obj != (obj|0)) text = obj.toString(); // must be a float + else text = obj + "\t($" + hex(obj) + ")"; + } else if (typeof obj == 'boolean') { + text = obj.toString(); + } else if (typeof obj == 'string') { + if (obj.length < MAX_STRING_LEN) + text = obj; + else + text = obj.substring(0, MAX_STRING_LEN) + "..."; + // typed byte array (TODO: other kinds) + } else if (obj.buffer && obj.length <= MAX_CHILDREN) { + text = dumpRAM(obj, 0, obj.length); + // recurse into object? (or function) + } else if (typeof obj == 'object' || typeof obj == 'function') { + // only if expanded + if (this._content != null) { + // split big arrays + if (obj.slice && obj.length > MAX_CHILDREN) { + let newobj = {}; + let oldobj = obj; + var slicelen = MAX_CHILDREN; + while (obj.length / slicelen > MAX_CHILDREN) slicelen *= 2; + for (let ofs=0; ofs { return oldobj.slice(ofs, ofs+slicelen); }} + } + obj = newobj; + } + // get object keys + let names = obj instanceof Array ? Array.from(obj.keys()) : Object.getOwnPropertyNames(obj); + if (names.length > MAX_CHILDREN) { // max # of child objects + let newobj = {}; + let oldobj = obj; + var slicelen = 100; + while (names.length / slicelen > 100) slicelen *= 2; + for (let ofs=0; ofs { + // hide $xxx idents? + var hidden = !TREE_SHOW_DOLLAR_IDENTS && typeof name === 'string' && name.startsWith("$$"); + if (!hidden) { + let childnode = this.children.get(name); + if (childnode == null) { + childnode = new TreeNode(this, name); + this.children.set(name, childnode); + } + childnode.update(obj[name]); + } + orphans.delete(name); + }); + // remove orphans + orphans.forEach((delname) => { + let childnode = this.children.get(delname); + childnode.remove(); + this.children.delete(delname); + }); + this._header.classList.add("tree-expanded"); + this._header.classList.remove("tree-collapsed"); + } else { + this._header.classList.add("tree-collapsed"); + this._header.classList.remove("tree-expanded"); + } + } else { + text = typeof obj; // fallthrough + } + // change DOM object if needed + if (this._inline.innerText != text) { + this._inline.innerText = text; + } + } +} + +function createTreeRootNode(parent : HTMLElement, view : ProjectView) : TreeNode { + var mainnode = new TreeNode(null, null); + mainnode.view = view; + mainnode._content = parent; + var root = new TreeNode(mainnode, "/"); + root.expanded = true; + root.getDiv(); // create it + root._div.style.padding = '0px'; + return root; // should be cached +} + +export abstract class TreeViewBase implements ProjectView { + root : TreeNode; + + createDiv(parent : HTMLElement) : HTMLElement { + this.root = createTreeRootNode(parent, this); + return this.root.getDiv(); + } + + refresh() { + this.tick(); + } + + tick() { + this.root.update(this.getRootObject()); + } + + abstract getRootObject() : Object; +} + +export class StateBrowserView extends TreeViewBase implements ProjectView { + getRootObject() { return platform.saveState(); } +} + +export class DebugBrowserView extends TreeViewBase implements ProjectView { + getRootObject() { return platform.getDebugTree(); } +} + +interface CallGraphNode { + $$SP : number; + $$PC : number; + count : number; + startLine : number; + endLine : number; + calls : {[id:string] : CallGraphNode}; +} + +// TODO: clear stack data when reset? +export class CallStackView extends ProbeViewBaseBase implements ProjectView { + treeroot : TreeNode; + graph : CallGraphNode; + stack : CallGraphNode[]; + lastsp : number; + lastpc : number; + jsr : boolean; + rts : boolean; + cumulativeData = true; + + createDiv(parent : HTMLElement) : HTMLElement { + this.clear(); + this.treeroot = createTreeRootNode(parent, this); + return this.treeroot.getDiv(); + } + + refresh() { + this.tick(); + } + + tick() { + this.treeroot.update(this.getRootObject()); + if (this.probe) this.probe.clear(); // clear cumulative data (TODO: doesnt work with seeking or debugging) + } + + clear() { + this.graph = null; + this.reset(); + } + + reset() { + this.stack = []; + this.lastsp = -1; + this.lastpc = 0; + this.jsr = false; + this.rts = false; + } + + newNode(pc : number, sp : number) { + return {$$SP:sp, $$PC:pc, count:0, startLine:null, endLine:null, calls:{}}; + } + + newRoot(pc : number, sp : number) { + if (this.stack.length == 0) { + this.graph = this.newNode(null, sp); + this.stack.unshift(this.graph); + } else if (sp > this.stack[0].$$SP) { + this.graph = this.newNode(null, sp); + this.graph.calls[this.addr2str(pc)] = this.stack[0]; + this.stack.unshift(this.graph); + } + } + + getRootObject() : Object { + // TODO: we don't capture every frame, so if we don't start @ the top frame we may have problems + this.redraw((op,addr,col,row,clk,value) => { + switch (op) { + case ProbeFlags.SP_POP: + this.newRoot(this.lastpc, this.lastsp); + case ProbeFlags.SP_PUSH: + if (this.stack.length) { + let top = this.stack[this.stack.length-1]; + var delta = this.lastsp - addr; + if ((delta == 2 || delta == 3) && addr < top.$$SP) { // TODO: look for opcode? + this.jsr = true; + } + if ((delta == -2 || delta == -3) && this.stack.length > 1 && addr > top.$$SP) { + this.rts = true; + } + } + this.lastsp = addr; + break; + case ProbeFlags.EXECUTE: + // TODO: better check for CALL/RET opcodes + if (Math.abs(addr - this.lastpc) >= 4) { // make sure we're jumping a distance (TODO) + if (this.jsr && this.stack.length) { + let top = this.stack[this.stack.length-1]; + let sym = this.addr2str(addr); + let child = top.calls[sym]; + if (child == null) { + child = top.calls[sym] = this.newNode(addr, this.lastsp); + } + else if (child.$$PC == null) child.$$PC = addr; + //this.stack.forEach((node) => node.count++); + this.stack.push(child); + child.count++; + child.startLine = row; + } + this.jsr = false; + if (this.rts && this.stack.length) { + this.stack.pop().endLine = row; + } + this.rts = false; + } + this.lastpc = addr; + break; + } + }); + if (this.graph) this.graph['$$Stack'] = this.stack; + return TREE_SHOW_DOLLAR_IDENTS ? this.graph : this.graph && this.graph.calls; + } +} + +export class FrameCallsView extends ProbeViewBaseBase implements ProjectView { + treeroot : TreeNode; + + createDiv(parent : HTMLElement) : HTMLElement { + this.treeroot = createTreeRootNode(parent, this); + return this.treeroot.getDiv(); + } + + refresh() { + this.tick(); + } + + tick() { + this.treeroot.update(this.getRootObject()); + } + + getRootObject() : Object { + var frame = {}; + this.redraw((op,addr,col,row,clk,value) => { + switch (op) { + case ProbeFlags.EXECUTE: + let sym = this.addr2symbol(addr); + if (sym) { + if (!frame[sym]) { + frame[sym] = row; + } + } + break; + } + }); + return frame; + } +} + + +/// + diff --git a/src/ide/windows.ts b/src/ide/windows.ts index e5efc1b3..6da4bb47 100644 --- a/src/ide/windows.ts +++ b/src/ide/windows.ts @@ -3,8 +3,8 @@ import $ = require("jquery"); import { CodeProject } from "./project"; import { WorkerError, FileData } from "../common/workertypes"; -import { ProjectView, SourceEditor } from "./views"; import { getFilenamePrefix, getFilenameForPath } from "../common/util"; +import { ProjectView } from "./views/baseviews"; type WindowCreateFunction = (id:string) => ProjectView; type WindowShowFunction = (id:string, view:ProjectView) => void; diff --git a/src/platform/basic.ts b/src/platform/basic.ts index c3673b9d..f220ba20 100644 --- a/src/platform/basic.ts +++ b/src/platform/basic.ts @@ -1,7 +1,7 @@ import { Platform, BreakpointCallback, DebugCondition, DebugEvalCondition } from "../common/baseplatform"; import { PLATFORMS, AnimationTimer, EmuHalt } from "../common/emu"; -import * as views from "../ide/views"; +import * as editors from "../ide/views/editors"; import { BASICRuntime } from "../common/basic/runtime"; import { BASICProgram } from "../common/basic/compiler"; import { TeleTypeWithKeyboard } from "../common/teletype"; @@ -134,7 +134,7 @@ class BASICPlatform implements Platform { var resumePC = this.runtime.load(data); this.tty.uppercaseOnly = true; // this.program.opts.uppercaseOnly; //TODO? // map editor to uppercase-only if need be - views.textMapFunctions.input = this.program.opts.uppercaseOnly ? (s) => s.toUpperCase() : null; + editors.textMapFunctions.input = this.program.opts.uppercaseOnly ? (s) => s.toUpperCase() : null; // only reset if we exited, or couldn't restart at label (PC reset to 0) if (!this.hotReload || didExit || !resumePC) this.reset();