mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2024-11-29 14:51:17 +00:00
split ide/views.ts into ide/views/*
This commit is contained in:
parent
8f6432f0d4
commit
91d0031366
@ -7,13 +7,17 @@ import { WorkerResult, WorkerOutput, VerilogOutput, SourceFile, WorkerError, Fil
|
|||||||
import { ProjectWindows } from "./windows";
|
import { ProjectWindows } from "./windows";
|
||||||
import { Platform, Preset, DebugSymbols, DebugEvalCondition, isDebuggable, EmuState, BasePlatform } from "../common/baseplatform";
|
import { Platform, Preset, DebugSymbols, DebugEvalCondition, isDebuggable, EmuState, BasePlatform } from "../common/baseplatform";
|
||||||
import { PLATFORMS, EmuHalt, Toolbar } from "../common/emu";
|
import { PLATFORMS, EmuHalt, Toolbar } from "../common/emu";
|
||||||
import * as Views from "./views";
|
|
||||||
import { getFilenameForPath, getFilenamePrefix, highlightDifferences, invertMap, byteArrayToString, compressLZG, stringToByteArray,
|
import { getFilenameForPath, getFilenamePrefix, highlightDifferences, invertMap, byteArrayToString, compressLZG, stringToByteArray,
|
||||||
byteArrayToUTF8, isProbablyBinary, getWithBinary, getBasePlatform, getRootBasePlatform, hex, loadScript, decodeQueryString, parseBool } from "../common/util";
|
byteArrayToUTF8, isProbablyBinary, getWithBinary, getBasePlatform, getRootBasePlatform, hex, loadScript, decodeQueryString, parseBool } from "../common/util";
|
||||||
import { StateRecorderImpl } from "../common/recorder";
|
import { StateRecorderImpl } from "../common/recorder";
|
||||||
import { GHSession, GithubService, getRepos, parseGithubURL, FirebaseProjectFilesystem } from "./services";
|
import { GHSession, GithubService, getRepos, parseGithubURL, FirebaseProjectFilesystem } from "./services";
|
||||||
import Split = require('split.js');
|
import Split = require('split.js');
|
||||||
import { importPlatform } from "../platform/_index";
|
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)
|
// external libs (TODO)
|
||||||
declare var Tour, GIF, firebase;
|
declare var Tour, GIF, firebase;
|
||||||
@ -286,7 +290,7 @@ function refreshWindowList() {
|
|||||||
function loadEditor(path:string) {
|
function loadEditor(path:string) {
|
||||||
var tool = platform.getToolForFilename(path);
|
var tool = platform.getToolForFilename(path);
|
||||||
var mode = tool && TOOL_TO_SOURCE_STYLE[tool];
|
var mode = tool && TOOL_TO_SOURCE_STYLE[tool];
|
||||||
return new Views.SourceEditor(path, mode);
|
return new SourceEditor(path, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addEditorItem(id:string) {
|
function addEditorItem(id:string) {
|
||||||
@ -295,7 +299,7 @@ function refreshWindowList() {
|
|||||||
if (typeof data === 'string')
|
if (typeof data === 'string')
|
||||||
return loadEditor(id);
|
return loadEditor(id);
|
||||||
else if (data instanceof Uint8Array)
|
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
|
// add listing if source/assembly file exists and has text
|
||||||
if ((lst.assemblyfile && lst.assemblyfile.text) || (lst.sourcefile && lst.sourcefile.text)) {
|
if ((lst.assemblyfile && lst.assemblyfile.text) || (lst.sourcefile && lst.sourcefile.text)) {
|
||||||
addWindowItem(lstfn, getFilenameForPath(lstfn), (path) => {
|
addWindowItem(lstfn, getFilenameForPath(lstfn), (path) => {
|
||||||
return new Views.ListingView(path);
|
return new ListingView(path);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -328,57 +332,57 @@ function refreshWindowList() {
|
|||||||
separate = true;
|
separate = true;
|
||||||
if (platform.disassemble && platform.saveState) {
|
if (platform.disassemble && platform.saveState) {
|
||||||
addWindowItem("#disasm", "Disassembly", () => {
|
addWindowItem("#disasm", "Disassembly", () => {
|
||||||
return new Views.DisassemblerView();
|
return new DisassemblerView();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (platform.readAddress) {
|
if (platform.readAddress) {
|
||||||
addWindowItem("#memory", "Memory Browser", () => {
|
addWindowItem("#memory", "Memory Browser", () => {
|
||||||
return new Views.MemoryView();
|
return new MemoryView();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (current_project.segments && current_project.segments.length) {
|
if (current_project.segments && current_project.segments.length) {
|
||||||
addWindowItem("#memmap", "Memory Map", () => {
|
addWindowItem("#memmap", "Memory Map", () => {
|
||||||
return new Views.MemoryMapView();
|
return new MemoryMapView();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (platform.readVRAMAddress) {
|
if (platform.readVRAMAddress) {
|
||||||
addWindowItem("#memvram", "VRAM Browser", () => {
|
addWindowItem("#memvram", "VRAM Browser", () => {
|
||||||
return new Views.VRAMMemoryView();
|
return new VRAMMemoryView();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (platform.startProbing) {
|
if (platform.startProbing) {
|
||||||
addWindowItem("#memheatmap", "Memory Probe", () => {
|
addWindowItem("#memheatmap", "Memory Probe", () => {
|
||||||
return new Views.AddressHeatMapView();
|
return new AddressHeatMapView();
|
||||||
});
|
});
|
||||||
// TODO: only if raster
|
// TODO: only if raster
|
||||||
addWindowItem("#crtheatmap", "CRT Probe", () => {
|
addWindowItem("#crtheatmap", "CRT Probe", () => {
|
||||||
return new Views.RasterPCHeatMapView();
|
return new RasterPCHeatMapView();
|
||||||
});
|
});
|
||||||
addWindowItem("#probelog", "Probe Log", () => {
|
addWindowItem("#probelog", "Probe Log", () => {
|
||||||
return new Views.ProbeLogView();
|
return new ProbeLogView();
|
||||||
});
|
});
|
||||||
addWindowItem("#scanlineio", "Scanline I/O", () => {
|
addWindowItem("#scanlineio", "Scanline I/O", () => {
|
||||||
return new Views.ScanlineIOView();
|
return new ScanlineIOView();
|
||||||
});
|
});
|
||||||
addWindowItem("#symbolprobe", "Symbol Profiler", () => {
|
addWindowItem("#symbolprobe", "Symbol Profiler", () => {
|
||||||
return new Views.ProbeSymbolView();
|
return new ProbeSymbolView();
|
||||||
});
|
});
|
||||||
addWindowItem("#callstack", "Call Stack", () => {
|
addWindowItem("#callstack", "Call Stack", () => {
|
||||||
return new Views.CallStackView();
|
return new CallStackView();
|
||||||
});
|
});
|
||||||
/*
|
/*
|
||||||
addWindowItem("#framecalls", "Frame Profiler", () => {
|
addWindowItem("#framecalls", "Frame Profiler", () => {
|
||||||
return new Views.FrameCallsView();
|
return new FrameCallsView();
|
||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
if (platform.getDebugTree) {
|
if (platform.getDebugTree) {
|
||||||
addWindowItem("#debugview", "Debug Tree", () => {
|
addWindowItem("#debugview", "Debug Tree", () => {
|
||||||
return new Views.DebugBrowserView();
|
return new DebugBrowserView();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
addWindowItem('#asseteditor', 'Asset Editor', () => {
|
addWindowItem('#asseteditor', 'Asset Editor', () => {
|
||||||
return new Views.AssetEditorView();
|
return new AssetEditorView();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1213,7 +1217,7 @@ function getErrorElement(err : WorkerError) {
|
|||||||
if (projectWindows.isWindow(path)) {
|
if (projectWindows.isWindow(path)) {
|
||||||
link.click((ev) => {
|
link.click((ev) => {
|
||||||
var wnd = projectWindows.createOrShow(path);
|
var wnd = projectWindows.createOrShow(path);
|
||||||
if (wnd instanceof Views.SourceEditor) {
|
if (wnd instanceof SourceEditor) {
|
||||||
wnd.setCurrentLine(err, true);
|
wnd.setCurrentLine(err, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -2233,7 +2237,7 @@ export function setupSplits() {
|
|||||||
var sizes;
|
var sizes;
|
||||||
if (platform_id.startsWith('vcs'))
|
if (platform_id.startsWith('vcs'))
|
||||||
sizes = [0, 50, 50];
|
sizes = [0, 50, 50];
|
||||||
else if (isEmbed || Views.isMobileDevice)
|
else if (isEmbed || isMobileDevice)
|
||||||
sizes = [0, 60, 40];
|
sizes = [0, 60, 40];
|
||||||
else
|
else
|
||||||
sizes = [12, 44, 44];
|
sizes = [12, 44, 44];
|
||||||
@ -2483,7 +2487,7 @@ function writeOutputROMFile() {
|
|||||||
}
|
}
|
||||||
export function highlightSearch(query: string) { // TODO: filename?
|
export function highlightSearch(query: string) { // TODO: filename?
|
||||||
var wnd = projectWindows.getActive();
|
var wnd = projectWindows.getActive();
|
||||||
if (wnd instanceof Views.SourceEditor) {
|
if (wnd instanceof SourceEditor) {
|
||||||
var sc = wnd.editor.getSearchCursor(query);
|
var sc = wnd.editor.getSearchCursor(query);
|
||||||
if (sc.findNext()) {
|
if (sc.findNext()) {
|
||||||
wnd.editor.setSelection(sc.pos.to, sc.pos.from);
|
wnd.editor.setSelection(sc.pos.to, sc.pos.from);
|
||||||
|
2127
src/ide/views.ts
2127
src/ide/views.ts
File diff suppressed because it is too large
Load Diff
389
src/ide/views/asseteditor.ts
Normal file
389
src/ide/views/asseteditor.ts
Normal file
@ -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<width; x++) {
|
||||||
|
for (var y=0; y<height; y++) {
|
||||||
|
metadefs.push({x:x*8, y:y*8, tile:tile, attr:attr});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var meta = {defs:metadefs,width:width*8,height:height*8};
|
||||||
|
result.push({fileid:id,label:ident,meta:meta});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move to pixeleditor.ts?
|
||||||
|
addPaletteEditorViews(parentdiv:JQuery, pal2rgb:pixed.PaletteFormatToRGB, callback) {
|
||||||
|
var adual = $('<div class="asset_dual"/>').appendTo(parentdiv);
|
||||||
|
var aeditor = $('<div class="asset_editor"/>').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 = $('<table/>').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<len; i+=imgsperline) {
|
||||||
|
layout.push(["", i, Math.min(len-i,imgsperline)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function updateCell(cell, j) {
|
||||||
|
var val = pal2rgb.words[j];
|
||||||
|
var rgb = pal2rgb.palette[j];
|
||||||
|
var hexcol = '#'+hex(rgb2bgr(rgb),6);
|
||||||
|
var textcol = (rgb & 0x008000) ? 'black' : 'white';
|
||||||
|
cell.text(hex(val,2)).css('background-color',hexcol).css('color',textcol);
|
||||||
|
}
|
||||||
|
// iterate over each row of the layout
|
||||||
|
layout.forEach( ([name, start, len]) => {
|
||||||
|
if (start < pal2rgb.palette.length) { // skip row if out of range
|
||||||
|
var arow = $('<tr/>').appendTo(atable);
|
||||||
|
$('<td/>').text(name).appendTo(arow);
|
||||||
|
var inds = [];
|
||||||
|
for (var k=start; k<start+Math.abs(len); k++)
|
||||||
|
inds.push(k);
|
||||||
|
if (len < 0)
|
||||||
|
inds.reverse();
|
||||||
|
inds.forEach( (i) => {
|
||||||
|
var cell = $('<td/>').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<HTMLElement> {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
33
src/ide/views/baseviews.ts
Normal file
33
src/ide/views/baseviews.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
796
src/ide/views/debugviews.ts
Normal file
796
src/ide/views/debugviews.ts
Normal file
@ -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<n1; i++) s += ' ';
|
||||||
|
if (n1 > 8) s += ' ';
|
||||||
|
for (var i=n1; i<n2; i++) {
|
||||||
|
var read = this.readAddress(offset+i);
|
||||||
|
if (i==8) s += ' ';
|
||||||
|
s += ' ' + (typeof read == 'number' ? hex(read,2) : '??');
|
||||||
|
}
|
||||||
|
for (var i=n2; i<16; i++) s += ' ';
|
||||||
|
if (sym) s += ' ' + sym;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
readAddress(n : number) {
|
||||||
|
return platform.readAddress(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDumpLineAt(line : number) {
|
||||||
|
var d = this.dumplines[line];
|
||||||
|
if (d) {
|
||||||
|
return d.a + " " + d.s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: addr2symbol for ca65; and make it work without symbols
|
||||||
|
getDumpLines() {
|
||||||
|
var addr2sym = (platform.debugSymbols && platform.debugSymbols.addr2symbol) || {};
|
||||||
|
if (this.dumplines == null) {
|
||||||
|
this.dumplines = [];
|
||||||
|
var ofs = 0;
|
||||||
|
var sym;
|
||||||
|
for (const _nextofs of Object.keys(addr2sym)) {
|
||||||
|
var nextofs = parseInt(_nextofs); // convert from string (stupid JS)
|
||||||
|
var nextsym = addr2sym[nextofs];
|
||||||
|
if (sym) {
|
||||||
|
// ignore certain symbols
|
||||||
|
if (ignoreSymbol(sym)) {
|
||||||
|
sym = '';
|
||||||
|
}
|
||||||
|
while (ofs < nextofs && this.dumplines.length < 0x10000) {
|
||||||
|
var ofs2 = (ofs + 16) & 0xffff0;
|
||||||
|
if (ofs2 > 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<this.dumplines.length; i++)
|
||||||
|
if (this.dumplines[i].a >= 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<n1; i++) s += ' ';
|
||||||
|
if (n1 > 8) s += ' ';
|
||||||
|
for (var i=n1; i<n2; i++) {
|
||||||
|
var read = this.data[offset+i];
|
||||||
|
if (i==8) s += ' ';
|
||||||
|
s += ' ' + (read>=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 = $('<div class="segment-offset" style="grid-column-start:1"/>');
|
||||||
|
offset.text('$'+hex(seg.start,4));
|
||||||
|
this.maindiv.append(offset);
|
||||||
|
}
|
||||||
|
var segdiv = $('<div class="segment"/>');
|
||||||
|
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 = $('<div class="row"/>').append(offset, segdiv);
|
||||||
|
//var container = $('<div class="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<p.idx; i++) {
|
||||||
|
var word = p.buf[i];
|
||||||
|
var addr = word & 0xffff;
|
||||||
|
var value = (word >> 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<this.cyclesPerLine; i++) {
|
||||||
|
var opaddr = line[i];
|
||||||
|
if (opaddr !== undefined) {
|
||||||
|
var addr = opaddr & 0xffff;
|
||||||
|
var op = op & 0xff000000;
|
||||||
|
if (op == ProbeFlags.EXECUTE) {
|
||||||
|
s += ',';
|
||||||
|
} else {
|
||||||
|
var v = hex(addr);
|
||||||
|
s += v;
|
||||||
|
i += v.length - 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s += (i==hblankCycle) ? '|' : '.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (line[-1]) s += ' ' + line[-1]; // executing symbol
|
||||||
|
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) => {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
|
583
src/ide/views/editors.ts
Normal file
583
src/ide/views/editors.ts
Normal file
@ -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<oldtext.length && i<text.length && text[i] == oldtext[i]; i++) { }
|
||||||
|
for (j=0; j<oldtext.length && j<text.length && text[text.length-1-j] == oldtext[oldtext.length-1-j]; j++) { }
|
||||||
|
//console.log(i,j,oldtext.substring(i,oldtext.length-j));
|
||||||
|
this.replaceSelection(i, oldtext.length-j, text.substring(i, text.length-j)); // calls setCode()
|
||||||
|
*/
|
||||||
|
// clear history if setting empty editor
|
||||||
|
if (oldtext == '') {
|
||||||
|
this.editor.clearHistory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
insertText(text:string) {
|
||||||
|
var cur = this.editor.getCursor();
|
||||||
|
this.editor.replaceRange(text, cur, cur);
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceSelection(start:number, end:number, text:string) {
|
||||||
|
this.editor.setSelection(this.editor.posFromIndex(start), this.editor.posFromIndex(end));
|
||||||
|
this.editor.replaceSelection(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue() : string {
|
||||||
|
return this.editor.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
getPath() : string { return this.path; }
|
||||||
|
|
||||||
|
addError(info: WorkerError) {
|
||||||
|
// only mark errors with this filename, or without any filename
|
||||||
|
if (!info.path || this.path.endsWith(info.path)) {
|
||||||
|
var numLines = this.editor.lineCount();
|
||||||
|
var line = info.line-1;
|
||||||
|
if (line < 0 || line >= 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<disasm.nbytes; i++)
|
||||||
|
bytes += hex(platform.readAddress(a+i));
|
||||||
|
while (bytes.length < 14)
|
||||||
|
bytes += ' ';
|
||||||
|
let dstr = disasm.line;
|
||||||
|
if (addr2symbol && disasm.isaddr) { // TODO: move out
|
||||||
|
dstr = dstr.replace(/([^#])[$]([0-9A-F]+)/, (substr:string, ...args:any[]):string => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
350
src/ide/views/treeviews.ts
Normal file
350
src/ide/views/treeviews.ts
Normal file
@ -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<string,TreeNode>;
|
||||||
|
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<oldobj.length; ofs+=slicelen) {
|
||||||
|
newobj["$"+hex(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<names.length; ofs+=slicelen) {
|
||||||
|
var newdict = {};
|
||||||
|
for (var i=ofs; i<ofs+slicelen; i++)
|
||||||
|
newdict[names[i]] = oldobj[names[i]];
|
||||||
|
newobj["["+ofs+"...]"] = newdict;
|
||||||
|
}
|
||||||
|
obj = newobj;
|
||||||
|
names = Object.getOwnPropertyNames(obj);
|
||||||
|
}
|
||||||
|
// track deletions
|
||||||
|
let orphans = new Set(this.children.keys());
|
||||||
|
// visit all children
|
||||||
|
names.forEach((name) => {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///
|
||||||
|
|
@ -3,8 +3,8 @@
|
|||||||
import $ = require("jquery");
|
import $ = require("jquery");
|
||||||
import { CodeProject } from "./project";
|
import { CodeProject } from "./project";
|
||||||
import { WorkerError, FileData } from "../common/workertypes";
|
import { WorkerError, FileData } from "../common/workertypes";
|
||||||
import { ProjectView, SourceEditor } from "./views";
|
|
||||||
import { getFilenamePrefix, getFilenameForPath } from "../common/util";
|
import { getFilenamePrefix, getFilenameForPath } from "../common/util";
|
||||||
|
import { ProjectView } from "./views/baseviews";
|
||||||
|
|
||||||
type WindowCreateFunction = (id:string) => ProjectView;
|
type WindowCreateFunction = (id:string) => ProjectView;
|
||||||
type WindowShowFunction = (id:string, view:ProjectView) => void;
|
type WindowShowFunction = (id:string, view:ProjectView) => void;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import { Platform, BreakpointCallback, DebugCondition, DebugEvalCondition } from "../common/baseplatform";
|
import { Platform, BreakpointCallback, DebugCondition, DebugEvalCondition } from "../common/baseplatform";
|
||||||
import { PLATFORMS, AnimationTimer, EmuHalt } from "../common/emu";
|
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 { BASICRuntime } from "../common/basic/runtime";
|
||||||
import { BASICProgram } from "../common/basic/compiler";
|
import { BASICProgram } from "../common/basic/compiler";
|
||||||
import { TeleTypeWithKeyboard } from "../common/teletype";
|
import { TeleTypeWithKeyboard } from "../common/teletype";
|
||||||
@ -134,7 +134,7 @@ class BASICPlatform implements Platform {
|
|||||||
var resumePC = this.runtime.load(data);
|
var resumePC = this.runtime.load(data);
|
||||||
this.tty.uppercaseOnly = true; // this.program.opts.uppercaseOnly; //TODO?
|
this.tty.uppercaseOnly = true; // this.program.opts.uppercaseOnly; //TODO?
|
||||||
// map editor to uppercase-only if need be
|
// 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)
|
// only reset if we exited, or couldn't restart at label (PC reset to 0)
|
||||||
if (!this.hotReload || didExit || !resumePC)
|
if (!this.hotReload || didExit || !resumePC)
|
||||||
this.reset();
|
this.reset();
|
||||||
|
Loading…
Reference in New Issue
Block a user