"use strict"; // 8bitworkshop IDE user interface // make sure VCS doesn't start if (window.Javatari) Javatari.AUTO_START = false; var PRESETS; // presets array var platform_id; // platform ID string var platform; // platform object var toolbar = $("#controls_top"); var current_project; // current CodeProject object // TODO: codemirror multiplex support? var TOOL_TO_SOURCE_STYLE = { 'dasm': '6502', 'acme': '6502', 'cc65': 'text/x-csrc', 'ca65': '6502', 'z80asm': 'z80', 'sdasz80': 'z80', 'sdcc': 'text/x-csrc', 'verilator': 'verilog', 'jsasm': 'z80' } function newWorker() { return new Worker("./src/worker/workermain.js"); } var userPaused; // did user explicitly pause? var current_output; // current ROM var current_preset_entry; // current preset object (if selected) var main_file_id; // main file ID var symbolmap; // symbol map var addr2symbol; // address to symbol name map var compparams; // received build params from worker var store; // persistent store var lastDebugInfo; // last debug info (CPU text) var lastDebugState; // last debug state (object) function inspectVariable(ed, name) { var val; if (platform.inspect) { platform.inspect(name); } } function getCurrentPresetTitle() { if (!current_preset_entry) return "ROM"; else return current_preset_entry.title || current_preset_entry.name || "ROM"; } function setLastPreset(id) { if (platform_id != 'base_z80') { // TODO localStorage.setItem("__lastplatform", platform_id); localStorage.setItem("__lastid_"+platform_id, id); } } function initProject() { current_project = new CodeProject(newWorker(), platform_id, platform, store); current_project.callbackBuildResult = function(result) { setCompileOutput(result); refreshWindowList(); }; current_project.callbackBuildStatus = function(busy) { if (busy) { toolbar.addClass("is-busy"); } else { toolbar.removeClass("is-busy"); toolbar.removeClass("has-errors"); // may be added in next callback projectWindows.setErrors(null); } $('#compile_spinner').css('visibility', busy ? 'visible' : 'hidden'); }; } // TODO: remove some calls of global functions function SourceEditor(path, mode) { var self = this; var editor; var dirtylisting = true; var sourcefile; var currentDebugLine; self.createDiv = function(parent, text) { var div = document.createElement('div'); div.setAttribute("class", "editor"); parent.appendChild(div); newEditor(div); if (text) self.setText(text); // TODO: this calls setCode() and builds... it shouldn't return div; } function newEditor(parent) { var isAsm = mode=='6502' || mode =='z80' || mode=='verilog' || mode=='gas'; // TODO editor = CodeMirror(parent, { theme: 'mbo', lineNumbers: true, matchBrackets: true, tabSize: 8, indentAuto: true, gutters: isAsm ? ["CodeMirror-linenumbers", "gutter-offset", "gutter-bytes", "gutter-clock", "gutter-info"] : ["CodeMirror-linenumbers", "gutter-offset", "gutter-info"], }); var timer; editor.on('changes', function(ed, changeobj) { clearTimeout(timer); timer = setTimeout(function() { current_project.updateFile(path, editor.getValue(), false); }, 200); }); editor.on('cursorActivity', function(ed) { var start = editor.getCursor(true); var end = editor.getCursor(false); if (start.line == end.line && start.ch < end.ch) { var name = editor.getSelection(); inspectVariable(editor, name); } else { inspectVariable(editor); } }); //scrollProfileView(editor); editor.setOption("mode", mode); } self.setText = function(text) { editor.setValue(text); // calls setCode() editor.clearHistory(); } self.getValue = function() { return editor.getValue(); } self.getPath = function() { return path; } var lines2errmsg = []; self.addErrorMarker = function(line, msg) { var div = document.createElement("div"); div.setAttribute("class", "tooltipbox tooltiperror"); div.appendChild(document.createTextNode("\u24cd")); var tooltip = document.createElement("span"); tooltip.setAttribute("class", "tooltiptext"); if (lines2errmsg[line]) msg = lines2errmsg[line] + "\n" + msg; tooltip.appendChild(document.createTextNode(msg)); lines2errmsg[line] = msg; div.appendChild(tooltip); editor.setGutterMarker(line, "gutter-info", div); } self.markErrors = function(errors) { // TODO: move cursor to error line if offscreen? self.clearErrors(); var numLines = editor.lineCount(); for (var info of errors) { // only mark errors with this filename, or without any filename if (!info.path || path.endsWith(info.path)) { var line = info.line-1; if (line < 0 || line >= numLines) line = 0; self.addErrorMarker(line, info.msg); } } } self.clearErrors = function() { editor.clearGutter("gutter-info"); refreshDebugState(); dirtylisting = true; } self.getSourceFile = function() { return sourcefile; } // TODO: update gutter only when refreshing this window self.updateListing = function(_sourcefile) { sourcefile = _sourcefile; // update editor annotations editor.clearGutter("gutter-info"); editor.clearGutter("gutter-bytes"); editor.clearGutter("gutter-offset"); editor.clearGutter("gutter-clock"); var lstlines = sourcefile.lines || []; for (var info of lstlines) { if (info.offset >= 0) { var textel = document.createTextNode(hex(info.offset,4)); editor.setGutterMarker(info.line-1, "gutter-offset", textel); } if (info.insns) { var insnstr = info.insns.length > 9 ? ("...") : info.insns; var textel = document.createTextNode(insnstr); editor.setGutterMarker(info.line-1, "gutter-bytes", textel); if (info.iscode) { var opcode = parseInt(info.insns.split()[0], 16); if (platform.getOpcodeMetadata) { var meta = platform.getOpcodeMetadata(opcode, info.offset); var clockstr = meta.minCycles+""; var textel = document.createTextNode(clockstr); editor.setGutterMarker(info.line-1, "gutter-clock", textel); } } } } } self.setGutterBytes = function(line, s) { var textel = document.createTextNode(s); editor.setGutterMarker(line-1, "gutter-bytes", textel); } self.setCurrentLine = function(line) { function addCurrentMarker(line) { var div = document.createElement("div"); div.style.color = '#66ffff'; div.appendChild(document.createTextNode("\u25b6")); editor.setGutterMarker(line, "gutter-info", div); } self.clearCurrentLine(); if (line>0) { addCurrentMarker(line-1); editor.setSelection({line:line,ch:0}, {line:line-1,ch:0}, {scroll:true}); currentDebugLine = line; } } self.clearCurrentLine = function() { if (currentDebugLine) { editor.clearGutter("gutter-info"); editor.setSelection(editor.getCursor()); currentDebugLine = 0; } } function refreshDebugState() { self.clearCurrentLine(); var state = lastDebugState; if (state && state.c) { var PC = state.c.PC; var line = sourcefile.findLineForOffset(PC); if (line >= 0) { self.setCurrentLine(line); // TODO: switch to disasm? } } } function refreshListing() { if (!dirtylisting) return; dirtylisting = false; var lst = current_project.getListingForFile(path); if (lst && lst.sourcefile) { self.updateListing(lst.sourcefile); // updates sourcefile variable } } self.refresh = function() { refreshListing(); refreshDebugState(); } self.getLine = function(line) { return editor.getLine(line-1); } self.getCurrentLine = function() { return editor.getCursor().line+1; } self.getCursorPC = function() { var line = self.getCurrentLine(); while (sourcefile && line >= 0) { var pc = sourcefile.line2offset[line]; if (pc >= 0) return pc; line--; } return -1; } // bitmap editor (TODO: refactor) function handleWindowMessage(e) { //console.log("window message", e.data); if (e.data.bytes) { editor.replaceSelection(e.data.bytestr); } if (e.data.close) { $("#pixeditback").hide(); } } function openBitmapEditorWithParams(fmt, bytestr, palfmt, palstr) { $("#pixeditback").show(); window.addEventListener("message", handleWindowMessage, false); // TODO: remove listener pixeditframe.contentWindow.postMessage({fmt:fmt, bytestr:bytestr, palfmt:palfmt, palstr:palstr}, '*'); } function lookBackwardsForJSONComment(line, req) { var re = /[/;][*;]([{].+[}])[*;][/;]/; while (--line >= 0) { var s = editor.getLine(line); var m = re.exec(s); if (m) { var jsontxt = m[1].replace(/([A-Za-z]+):/g, '"$1":'); // fix lenient JSON var obj = JSON.parse(jsontxt); if (obj[req]) { var start = {obj:obj, line:line, ch:s.indexOf(m[0])+m[0].length}; var line0 = line; var pos0 = start.ch; line--; while (++line < editor.lineCount()) { var l = editor.getLine(line); var endsection; if (platform_id == 'verilog') endsection = l.indexOf('end') >= pos0; else endsection = l.indexOf(';') >= pos0; if (endsection) { var end = {line:line, ch:editor.getLine(line).length}; return {obj:obj, start:start, end:end}; } pos0 = 0; } line = line0; } } } } self.openBitmapEditorAtCursor = function() { if ($("#pixeditback").is(":visible")) { $("#pixeditback").hide(250); return; } var line = editor.getCursor().line + 1; var data = lookBackwardsForJSONComment(self.getCurrentLine(), 'w'); if (data && data.obj && data.obj.w>0 && data.obj.h>0) { var paldata = lookBackwardsForJSONComment(data.start.line-1, 'pal'); var palbytestr; if (paldata) { palbytestr = editor.getRange(paldata.start, paldata.end); paldata = paldata.obj; } editor.setSelection(data.end, data.start); openBitmapEditorWithParams(data.obj, editor.getSelection(), paldata, palbytestr); } else { alert("To edit graphics, move cursor to a constant array preceded by a comment in the format:\n\n/*{w:,h:,bpp:,count:...}*/\n\n(See code examples)"); } } } /// function DisassemblerView() { var self = this; var disasmview; self.getDisasmView = function() { return disasmview; } self.createDiv = function(parent) { var div = document.createElement('div'); div.setAttribute("class", "editor"); parent.appendChild(div); newEditor(div); return div; } function newEditor(parent) { disasmview = CodeMirror(parent, { mode: 'z80', // TODO: pick correct one theme: 'cobalt', tabSize: 8, readOnly: true, styleActiveLine: true }); } // TODO: too many globals self.refresh = function() { var state = lastDebugState || platform.saveState(); var pc = state.c ? state.c.PC : 0; var curline = 0; var selline = 0; // TODO: not perfect disassembler function disassemble(start, end) { if (start < 0) start = 0; if (end > 0xffff) end = 0xffff; // TODO: use pc2visits var a = start; var s = ""; while (a < end) { var disasm = platform.disassemble(a, platform.readAddress); /* TODO: look thru all source files var srclinenum = sourcefile && sourcefile.offset2line[a]; if (srclinenum) { var srcline = getActiveEditor().getLine(srclinenum); if (srcline && srcline.trim().length) { s += "; " + srclinenum + ":\t" + srcline + "\n"; curline++; } } */ var bytes = ""; for (var i=0; i= 0) { var toks = disasmview.getLine(line).split(/\s+/); if (toks && toks.length >= 1) { var pc = parseInt(toks[0], 16); if (pc >= 0) return pc; } } return -1; } } /// function ListingView(assemblyfile) { var self = this; this.__proto__ = new DisassemblerView(); self.refresh = function() { var state = lastDebugState || platform.saveState(); var pc = state.c ? state.c.PC : 0; var asmtext = assemblyfile.text; var disasmview = self.getDisasmView(); if (platform_id == 'base_z80') { // TODO asmtext = asmtext.replace(/[ ]+\d+\s+;.+\n/g, ''); asmtext = asmtext.replace(/[ ]+\d+\s+.area .+\n/g, ''); } disasmview.setValue(asmtext); var findPC = platform.getDebugCallback() ? pc : -1; if (findPC >= 0) { var lineno = assemblyfile.findLineForOffset(findPC); if (lineno) { // set cursor while debugging if (platform.getDebugCallback()) disasmview.setCursor(lineno-1, 0); jumpToLine(disasmview, lineno-1); } } } } /// function MemoryView() { var self = this; var memorylist; var dumplines; var div; // TODO? function getVisibleEditorLineHeight() { return $(".CodeMirror-line:visible").first().height(); } self.createDiv = function(parent) { div = document.createElement('div'); div.setAttribute("class", "memdump"); parent.appendChild(div); showMemoryWindow(div); return div; } function showMemoryWindow(parent) { memorylist = new VirtualList({ w:$("#workspace").width(), h:$("#workspace").height(), itemHeight: getVisibleEditorLineHeight(), totalRows: 0x1000, generatorFn: function(row) { var s = getMemoryLineAt(row); var div = document.createElement("div"); if (dumplines) { var dlr = dumplines[row]; if (dlr) div.classList.add('seg_' + getMemorySegment(dumplines[row].a)); } div.appendChild(document.createTextNode(s)); return div; } }); $(parent).append(memorylist.container); self.tick(); if (compparams && dumplines) memorylist.scrollToItem(findMemoryWindowLine(compparams.data_start)); } self.tick = function() { if (memorylist) { $(div).find('[data-index]').each(function(i,e) { var div = $(e); var row = div.attr('data-index'); var oldtext = div.text(); var newtext = getMemoryLineAt(row); if (oldtext != newtext) div.text(newtext); }); } } function getMemoryLineAt(row) { var offset = row * 16; var n1 = 0; var n2 = 16; var sym; if (getDumpLines()) { var dl = 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,4) + ' '; for (var i=0; i 8) s += ' '; for (var i=n1; i=0?hex(read,2):'??'); } for (var i=n2; i<16; i++) s += ' '; if (sym) s += ' ' + sym; return s; } function getDumpLineAt(line) { var d = dumplines[line]; if (d) { return d.a + " " + d.s; } } var IGNORE_SYMS = {s__INITIALIZER:true, /* s__GSINIT:true, */ _color_prom:true}; // TODO: addr2symbol for ca65; and make it work without symbols function getDumpLines() { if (!dumplines && addr2symbol) { dumplines = []; var ofs = 0; var sym; for (var nextofs in addr2symbol) { nextofs |= 0; var nextsym = addr2symbol[nextofs]; if (sym) { if (IGNORE_SYMS[sym]) { ofs = nextofs; } else { while (ofs < nextofs) { var ofs2 = (ofs + 16) & 0xffff0; if (ofs2 > nextofs) ofs2 = nextofs; //if (ofs < 1000) console.log(ofs, ofs2, nextofs, sym); dumplines.push({a:ofs, l:ofs2-ofs, s:sym}); ofs = ofs2; } } } sym = nextsym; } } return dumplines; } function getMemorySegment(a) { if (!compparams) return 'unknown'; 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) return 'code'; else return 'unknown'; } function findMemoryWindowLine(a) { for (var i=0; i= a) return i; } } ///// function ProjectWindows(containerdiv) { var self = this; var id2window = {}; var id2createfn = {}; var id2div = {}; var activewnd; var activediv; var lasterrors; // TODO: delete windows ever? this.setCreateFunc = function(id, createfn) { id2createfn[id] = createfn; } this.createOrShow = function(id) { var wnd = id2window[id]; if (!wnd) { wnd = id2window[id] = id2createfn[id](id); } var div = id2div[id]; if (!div) { div = id2div[id] = wnd.createDiv(containerdiv, current_project.getFile(id)); } if (activewnd != wnd) { if (activediv) $(activediv).hide(); activediv = div; activewnd = wnd; $(div).show(); this.refresh(); this.refreshErrors(); } return wnd; } this.put = function(id, window, category) { id2window[id] = window; if (!categories[category]) categories[category] = []; // TODO: remove/replace window if (!(id in categories[category])) categories[category].push(id); } this.filesForCategory = function(id) { return categories[id] || []; } this.refresh = function() { if (activewnd && activewnd.refresh) activewnd.refresh(); } this.tick = function() { if (activewnd && activewnd.tick) activewnd.tick(); } this.setErrors = function(errors) { lasterrors = errors; this.refreshErrors(); } this.refreshErrors = function() { if (activewnd && activewnd.markErrors) { if (lasterrors && lasterrors.length) activewnd.markErrors(lasterrors); else activewnd.clearErrors(); } } this.getActive = function() { return activewnd; } this.getCurrentText = function() { if (activewnd && activewnd.getValue) return activewnd.getValue(); else alert("Please switch to an editor window."); } }; var projectWindows = new ProjectWindows($("#workspace")[0]); function refreshWindowList() { var ul = $("#windowMenuList").empty(); var separate = false; function addWindowItem(id, name, createfn) { if (separate) { ul.append(document.createElement("hr")); separate = false; } var li = document.createElement("li"); var a = document.createElement("a"); a.setAttribute("class", "dropdown-item"); a.setAttribute("href", "#"); a.appendChild(document.createTextNode(name)); li.appendChild(a); ul.append(li); if (createfn) { projectWindows.setCreateFunc(id, createfn); $(a).click(function() { projectWindows.createOrShow(id); }); } } function loadEditor(path) { var tool = platform.getToolForFilename(path); var mode = tool && TOOL_TO_SOURCE_STYLE[tool]; return new SourceEditor(path, mode, current_project.getFile(path)); } // add main file editor var id = main_file_id; addWindowItem(id, getFilenameForPath(id), loadEditor); // add other source files separate = true; current_project.iterateFiles(function(id, text) { if (id != main_file_id) addWindowItem(id, getFilenameForPath(id), loadEditor); }); // add listings var listings = current_project.getListings(); if (listings) { for (var lstfn in listings) { var lst = listings[lstfn]; if (lst.assemblyfile) { addWindowItem(lstfn, getFilenameForPath(lstfn), function(path) { return new ListingView(lst.assemblyfile); }); } } } // add other tools separate = true; if (platform.disassemble) { addWindowItem("#disasm", "Disassembly", function() { return new DisassemblerView(); }); } if (platform.readAddress && platform_id != 'vcs') { addWindowItem("#memory", "Memory Browser", function() { return new MemoryView(); }); } } // can pass integer or string id function loadProject(preset_id) { var index = parseInt(preset_id+""); // might fail -1 for (var i=0; i= 0) { // load the preset current_preset_entry = PRESETS[index]; preset_id = current_preset_entry.id; } // set current file ID main_file_id = preset_id; setLastPreset(preset_id); current_project.setMainPath(preset_id); // load files from storage or web URLs current_project.loadFiles([preset_id], function(err, result) { if (err) { alert(err); } else if (result && result.length) { // we need this to build create functions for the editor (TODO?) refreshWindowList(); // show main file projectWindows.createOrShow(preset_id); } }); } function reloadPresetNamed(id) { qs['platform'] = platform_id; qs['file'] = id; gotoNewLocation(); } function getSkeletonFile(fileid, callback) { var ext = platform.getToolForFilename(fileid); $.get( "presets/"+platform_id+"/skeleton."+ext, function( text ) { callback(null, text); }, 'text') .fail(function() { alert("Could not load skeleton for " + platform_id + "/" + ext + "; using blank file"); callback(null, '\n'); }); } function _createNewFile(e) { var filename = prompt("Create New File", "newfile" + platform.getDefaultExtension()); if (filename && filename.length) { if (filename.indexOf(".") < 0) { filename += platform.getDefaultExtension(); } var path = "local/" + filename; getSkeletonFile(path, function(err, result) { if (result) { store.setItem(path, result, function(err, result) { if (err) alert(err+""); if (result != null) reloadPresetNamed("local/" + filename); }); } }); } return true; } function _uploadNewFile(e) { $("#uploadFileElem").click(); } function handleFileUpload(files) { console.log(files); var index = 0; function uploadNextFile() { var f = files[index++]; if (!f) { console.log("Done uploading"); gotoNewLocation(); } else { var path = "local/" + f.name; var reader = new FileReader(); reader.onload = function(e) { var data = e.target.result; store.setItem(path, data, function(err, result) { if (err) console.log(err); else { console.log("Uploaded " + path + " " + data.length + " bytes"); if (index == 1) qs['file'] = path; uploadNextFile(); } }); } reader.readAsText(f); } } if (files) uploadNextFile(); } function getCurrentFilename() { var toks = main_file_id.split("/"); return toks[toks.length-1]; } function _shareFile(e) { if (current_output == null) { // TODO alert("Please fix errors before sharing."); return true; } var text = projectWindows.getCurrentText(); if (!text) return false; var github = new Octokat(); var files = {}; files[getCurrentFilename()] = {"content": text}; var gistdata = { "description": '8bitworkshop.com {"platform":"' + platform_id + '"}', "public": true, "files": files }; var gist = github.gists.create(gistdata).done(function(val) { var url = "http://8bitworkshop.com/?sharekey=" + val.id; window.prompt("Copy link to clipboard (Ctrl+C, Enter)", url); }).fail(function(err) { alert("Error sharing file: " + err.message); }); return true; } function _resetPreset(e) { if (!current_preset_entry) { alert("Can only reset built-in file examples.") } else if (confirm("Reset '" + current_preset_entry.name + "' to default?")) { qs['reset'] = '1'; gotoNewLocation(); } return true; } function _downloadROMImage(e) { if (current_output == null) { // TODO alert("Please fix errors before downloading ROM."); return true; } var blob = new Blob([current_output], {type: "application/octet-stream"}); saveAs(blob, getCurrentFilename()+".rom"); } function _downloadSourceFile(e) { var text = projectWindows.getCurrentText(); if (!text) return false; var blob = new Blob([text], {type: "text/plain;charset=utf-8"}); saveAs(blob, getCurrentFilename()); } function populateExamples(sel) { // make sure to use callback so it follows other sections store.length(function(err, len) { sel.append($("