8bitworkshop/src/ide/views/treeviews.ts

351 lines
10 KiB
TypeScript

import { dumpRAM } from "../../common/emu";
import { ProbeFlags } from "../../common/probe";
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;
}
}
///