"use strict"; // 8bitworkshop IDE user interface var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.highlightSearch = exports.reloadWorkspaceFile = exports.emulationHalted = exports.getSaveState = exports.getTestOutput = exports.setTestInput = exports.startUI = exports.getPlatformAndRepo = exports.setupSplits = exports.gotoNewLocation = exports.haltEmulation = exports.setFrameRateUI = exports.clearBreakpoint = exports.runToPC = exports.setupBreakpoint = exports.getCurrentEditorFilename = exports.getCurrentMainFilename = exports.getWorkerParams = exports.getCurrentOutput = exports.getCurrentProject = exports.getPlatformStore = exports.lastDebugState = exports.projectWindows = exports.current_project = exports.platform = exports.repo_id = exports.store_id = exports.platform_id = exports.qs = void 0; const localforage = __importStar(require("localforage")); const project_1 = require("./project"); const windows_1 = require("./windows"); const baseplatform_1 = require("../common/baseplatform"); const emu_1 = require("../common/emu"); const toolbar_1 = require("./toolbar"); const util_1 = require("../common/util"); const recorder_1 = require("../common/recorder"); const services_1 = require("./services"); const Split = require("split.js"); const _index_1 = require("../platform/_index"); const editors_1 = require("./views/editors"); const debugviews_1 = require("./views/debugviews"); const asseteditor_1 = require("./views/asseteditor"); const baseviews_1 = require("./views/baseviews"); const treeviews_1 = require("./views/treeviews"); const DOMPurify = require("dompurify"); const dialogs_1 = require("./dialogs"); const sync_1 = require("./sync"); const analytics_1 = require("./analytics"); const shareexport_1 = require("./shareexport"); /// EXPORTED GLOBALS (TODO: remove) exports.qs = (0, util_1.decodeQueryString)(window.location.search || '?'); // private globals var compparams; // received build params from worker var platform_name; // platform name (after setPlatformUI) var toolbar = $("#controls_top"); var uitoolbar; var stateRecorder; var userPaused; // did user explicitly pause? var current_output; // current ROM (or other object) var current_preset; // current preset object (if selected) var store; // persistent store const isElectron = (0, util_1.parseBool)(exports.qs.electron); const isEmbed = (0, util_1.parseBool)(exports.qs.embed); var lastDebugInfo; // last debug info (CPU text) var debugCategory; // current debug category var debugTickPaused = false; var recorderActive = false; var lastViewClicked = null; var lastDebugCommand = null; var errorWasRuntime = false; var lastBreakExpr = "c.PC == 0x6000"; function getPlatformStore() { return store; } exports.getPlatformStore = getPlatformStore; function getCurrentProject() { return exports.current_project; } exports.getCurrentProject = getCurrentProject; function getCurrentOutput() { return current_output; } exports.getCurrentOutput = getCurrentOutput; function getWorkerParams() { return compparams; } exports.getWorkerParams = getWorkerParams; // TODO: codemirror multiplex support? // TODO: move to views.ts? const TOOL_TO_SOURCE_STYLE = { 'dasm': '6502', 'acme': '6502', 'cc65': 'text/x-csrc', 'ca65': '6502', 'nesasm': '6502', 'z80asm': 'z80', 'sdasz80': 'z80', 'sdcc': 'text/x-csrc', 'verilator': 'verilog', 'jsasm': 'z80', 'zmac': 'z80', 'bataribasic': 'bataribasic', 'markdown': 'markdown', 'js': 'javascript', 'xasm6809': 'z80', 'cmoc': 'text/x-csrc', 'yasm': 'gas', 'smlrc': 'text/x-csrc', 'inform6': 'inform6', 'fastbasic': 'fastbasic', 'basic': 'basic', 'silice': 'verilog', 'wiz': 'text/x-wiz', 'vasmarm': 'vasm', 'armips': 'vasm', 'ecs': 'ecs', 'remote:llvm-mos': 'text/x-csrc', 'cc7800': 'text/x-csrc', 'armtcc': 'text/x-csrc', }; // TODO: move into tool class const TOOL_TO_HELPURL = { 'dasm': 'https://raw.githubusercontent.com/sehugg/dasm/master/doc/dasm.txt', 'cc65': 'https://cc65.github.io/doc/cc65.html', 'ca65': 'https://cc65.github.io/doc/ca65.html', 'sdcc': 'http://sdcc.sourceforge.net/doc/sdccman.pdf', 'verilator': 'https://www.veripool.org/ftp/verilator_doc.pdf', 'fastbasic': 'https://github.com/dmsc/fastbasic/blob/master/manual.md', 'bataribasic': "help/bataribasic/manual.html", 'wiz': "https://github.com/wiz-lang/wiz/blob/master/readme.md#wiz", 'silice': "https://github.com/sylefeb/Silice", 'zmac': "https://raw.githubusercontent.com/sehugg/zmac/master/doc.txt", 'cmoc': "http://perso.b2b2c.ca/~sarrazip/dev/cmoc.html", 'remote:llvm-mos': 'https://llvm-mos.org/wiki/Welcome', 'acme': 'https://raw.githubusercontent.com/sehugg/acme/main/docs/QuickRef.txt', }; function newWorker() { // TODO: return new Worker("https://8bitworkshop.com.s3-website-us-east-1.amazonaws.com/dev/gen/worker/bundle.js"); return new Worker("./gen/worker/bundle.js"); } const hasLocalStorage = function () { try { const key = "__some_random_key_you_are_not_going_to_use__"; localStorage.setItem(key, key); var has = localStorage.getItem(key) == key; localStorage.removeItem(key); return has; } catch (e) { return false; } }(); // wrapper for localstorage class UserPrefs { setLastPreset(id) { if (hasLocalStorage && !isEmbed) { if (exports.repo_id && exports.platform_id && !isElectron) localStorage.setItem("__lastrepo_" + exports.platform_id, exports.repo_id); else localStorage.removeItem("__lastrepo_" + exports.platform_id); localStorage.setItem("__lastplatform", exports.platform_id); localStorage.setItem("__lastid_" + exports.store_id, id); } } unsetLastPreset() { if (hasLocalStorage && !isEmbed) { delete exports.qs.file; localStorage.removeItem("__lastid_" + exports.store_id); } } getLastPreset() { return hasLocalStorage && !isEmbed && localStorage.getItem("__lastid_" + exports.store_id); } getLastPlatformID() { return hasLocalStorage && !isEmbed && localStorage.getItem("__lastplatform"); } getLastRepoID(platform) { return hasLocalStorage && !isEmbed && platform && localStorage.getItem("__lastrepo_" + platform); } shouldCompleteTour() { return hasLocalStorage && !isEmbed && !localStorage.getItem("8bitworkshop.hello"); } completedTour() { if (hasLocalStorage && !isEmbed) localStorage.setItem("8bitworkshop.hello", "true"); } } var userPrefs = new UserPrefs(); // https://developers.google.com/web/updates/2016/06/persistent-storage function requestPersistPermission(interactive, failureonly) { if (navigator.storage && navigator.storage.persist) { navigator.storage.persist().then(persistent => { console.log("requestPersistPermission =", persistent); if (persistent) { interactive && !failureonly && (0, dialogs_1.alertInfo)("Your browser says it will persist your local file edits, but you may want to back up your work anyway."); } else { interactive && (0, dialogs_1.alertError)("Your browser refused to expand the peristent storage quota. Your edits may not be preserved after closing the page."); } }); } else { interactive && (0, dialogs_1.alertError)("Your browser may not persist edits after closing the page. Try a different browser."); } } function getCurrentPresetTitle() { if (!current_preset) return exports.current_project.mainPath || "ROM"; else return current_preset.title || current_preset.name || exports.current_project.mainPath || "ROM"; } async function newFilesystem() { var basefs = new project_1.WebPresetsFileSystem(exports.platform_id); if (isElectron) { console.log('using electron with local filesystem', alternateLocalFilesystem); return new project_1.OverlayFilesystem(basefs, alternateLocalFilesystem); } else if (exports.qs.localfs != null) { return new project_1.OverlayFilesystem(basefs, await getLocalFilesystem(exports.qs.localfs)); } else { return new project_1.OverlayFilesystem(basefs, new project_1.LocalForageFilesystem(store)); } } async function initProject() { var filesystem = await newFilesystem(); exports.current_project = new project_1.CodeProject(newWorker(), exports.platform_id, exports.platform, filesystem); exports.current_project.remoteTool = exports.qs.tool || null; exports.projectWindows = new windows_1.ProjectWindows($("#workspace")[0], exports.current_project); exports.current_project.callbackBuildResult = (result) => { setCompileOutput(result); }; exports.current_project.callbackBuildStatus = (busy) => { setBusyStatus(busy); }; } function setBusyStatus(busy) { if (busy) { toolbar.addClass("is-busy"); } else { toolbar.removeClass("is-busy"); } $('#compile_spinner').css('visibility', busy ? 'visible' : 'hidden'); } function newDropdownListItem(id, text) { var li = document.createElement("li"); var a = document.createElement("a"); a.setAttribute("class", "dropdown-item"); a.setAttribute("href", "#"); a.setAttribute("data-wndid", id); if (id == exports.projectWindows.getActiveID()) $(a).addClass("dropdown-item-checked"); a.appendChild(document.createTextNode(text)); li.appendChild(a); return { li, a }; } function refreshWindowList() { var ul = $("#windowMenuList").empty(); var separate = false; function addWindowItem(id, name, createfn) { if (separate) { ul.append(document.createElement("hr")); separate = false; } let { li, a } = newDropdownListItem(id, name); ul.append(li); if (createfn) { var onopen = (id, wnd) => { ul.find('a').removeClass("dropdown-item-checked"); $(a).addClass("dropdown-item-checked"); }; exports.projectWindows.setCreateFunc(id, createfn); exports.projectWindows.setShowFunc(id, onopen); $(a).click((e) => { exports.projectWindows.createOrShow(id); lastViewClicked = id; }); } } function loadEditor(path) { var tool = exports.platform.getToolForFilename(path); // hack because .h files can be DASM or CC65 if (tool == 'dasm' && path.endsWith(".h") && getCurrentMainFilename().endsWith(".c")) { tool = 'cc65'; } var mode = tool && TOOL_TO_SOURCE_STYLE[tool]; return new editors_1.SourceEditor(path, mode); } function addEditorItem(id) { addWindowItem(id, (0, util_1.getFilenameForPath)(id), () => { var data = exports.current_project.getFile(id); if (typeof data === 'string') return loadEditor(id); else if (data instanceof Uint8Array) return new debugviews_1.BinaryFileView(id, data); }); } // add main file editor addEditorItem(exports.current_project.mainPath); // add other source files exports.current_project.iterateFiles((id, text) => { if (text && id != exports.current_project.mainPath) { addEditorItem(id); } }); // add listings separate = true; var listings = exports.current_project.getListings(); if (listings) { for (var lstfn in listings) { var lst = listings[lstfn]; // add listing if source/assembly file exists and has text if ((lst.assemblyfile && lst.assemblyfile.text) || (lst.sourcefile && lst.sourcefile.text) || lst.text) { addWindowItem(lstfn, (0, util_1.getFilenameForPath)(lstfn), (path) => { return new editors_1.ListingView(path); }); } } } // add other tools separate = true; if (exports.platform.disassemble && exports.platform.saveState) { addWindowItem("#disasm", "Disassembly", () => { return new editors_1.DisassemblerView(); }); } if (exports.platform.readAddress) { addWindowItem("#memory", "Memory Browser", () => { return new debugviews_1.MemoryView(); }); } if (exports.current_project.segments && exports.current_project.segments.length) { addWindowItem("#memmap", "Memory Map", () => { return new debugviews_1.MemoryMapView(); }); } if (exports.platform.readVRAMAddress) { addWindowItem("#memvram", "VRAM Browser", () => { return new debugviews_1.VRAMMemoryView(); }); } if (exports.platform.startProbing) { addWindowItem("#memheatmap", "Memory Probe", () => { return new debugviews_1.AddressHeatMapView(); }); // TODO: only if raster addWindowItem("#crtheatmap", "CRT Probe", () => { //return new RasterPCHeatMapView(); return new debugviews_1.RasterStackMapView(); }); addWindowItem("#probelog", "Probe Log", () => { return new debugviews_1.ProbeLogView(); }); addWindowItem("#scanlineio", "Scanline I/O", () => { return new debugviews_1.ScanlineIOView(); }); addWindowItem("#symbolprobe", "Symbol Profiler", () => { return new debugviews_1.ProbeSymbolView(); }); addWindowItem("#callstack", "Call Stack", () => { return new treeviews_1.CallStackView(); }); /* addWindowItem("#framecalls", "Frame Profiler", () => { return new FrameCallsView(); }); */ } if (exports.platform.getDebugTree) { addWindowItem("#debugview", "Debug Tree", () => { return new treeviews_1.DebugBrowserView(); }); } addWindowItem('#asseteditor', 'Asset Editor', () => { return new asseteditor_1.AssetEditorView(); }); } function highlightLines(path, hispec) { if (hispec) { var toks = exports.qs.highlight.split(','); var start = parseInt(toks[0]) - 1; var end = parseInt(toks[1]) - 1; var editor = exports.projectWindows.createOrShow(path); editor.highlightLines(start, end); } } function loadMainWindow(preset_id) { // we need this to build create functions for the editor refreshWindowList(); // show main file exports.projectWindows.createOrShow(preset_id); // build project exports.current_project.setMainFile(preset_id); // highlighting? highlightLines(preset_id, exports.qs.highlight); } async function loadProject(preset_id) { // set current file ID // TODO: this is done twice (mainPath and mainpath!) exports.current_project.mainPath = preset_id; userPrefs.setLastPreset(preset_id); // load files from storage or web URLs var result = await exports.current_project.loadFiles([preset_id]); if (result && result.length) { // file found; continue loadMainWindow(preset_id); } else { var skel = await getSkeletonFile(preset_id); exports.current_project.filedata[preset_id] = skel || "\n"; loadMainWindow(preset_id); // don't alert if we selected "new file" if (!exports.qs.newfile) { (0, dialogs_1.alertInfo)("Could not find file \"" + preset_id + "\". Loading default file."); } else { requestPersistPermission(true, true); } delete exports.qs.newfile; replaceURLState(); } } function reloadProject(id) { // leave repository == '/' if (id == '/') { exports.qs = { repo: '/' }; } else if (id.indexOf('://') >= 0) { var urlparse = (0, services_1.parseGithubURL)(id); if (urlparse) { exports.qs = { repo: urlparse.repopath }; } } else { exports.qs.platform = exports.platform_id; exports.qs.file = id; } gotoNewLocation(); } async function getSkeletonFile(fileid) { var ext = exports.platform.getToolForFilename(fileid); try { return await $.get("presets/" + (0, util_1.getBasePlatform)(exports.platform_id) + "/skeleton." + ext, 'text'); } catch (e) { (0, dialogs_1.alertError)("Could not load skeleton for " + exports.platform_id + "/" + ext + "; using blank file"); } } function checkEnteredFilename(fn) { if (fn.indexOf(" ") >= 0) { (0, dialogs_1.alertError)("No spaces in filenames, please."); return false; } return true; } function _createNewFile(e) { // TODO: support spaces bootbox.prompt({ title: "Enter the name of your new main source file.", placeholder: "newfile" + exports.platform.getDefaultExtension(), callback: (filename) => { if (filename && filename.trim().length > 0) { if (!checkEnteredFilename(filename)) return; if (filename.indexOf(".") < 0) { filename += exports.platform.getDefaultExtension(); } var path = filename; (0, analytics_1.gaEvent)('workspace', 'file', 'new'); exports.qs.newfile = '1'; reloadProject(path); } } }); return true; } function _uploadNewFile(e) { const uploadFileElem = $(``); const file = uploadFileElem[0]; uploadFileElem.change((e) => { handleFileUpload(file.files); }); uploadFileElem.click(); } // called from index.html function handleFileUpload(files) { console.log(files); var index = 0; function uploadNextFile() { var f = files[index++]; if (!f) { console.log("Done uploading", index); if (index > 2) { (0, dialogs_1.alertInfo)("Files uploaded."); setTimeout(updateSelector, 1000); // TODO: wait for files to upload } else { exports.qs.file = files[0].name; bootbox.confirm({ message: "Open '" + DOMPurify.sanitize(exports.qs.file) + "' as main project file?", buttons: { confirm: { label: "Open As New Project" }, cancel: { label: "Include/Link With Project Later" }, }, callback: (result) => { if (result) gotoNewLocation(); else setTimeout(updateSelector, 1000); // TODO: wait for files to upload } }); } (0, analytics_1.gaEvent)('workspace', 'file', 'upload'); } else { var path = f.name; var reader = new FileReader(); reader.onload = function (e) { var arrbuf = e.target.result; var data = new Uint8Array(arrbuf); // convert to UTF8, unless it's a binary file if ((0, util_1.isProbablyBinary)(path, data)) { //gotoMainFile = false; } else { data = (0, util_1.byteArrayToUTF8)(data).replace('\r\n', '\n'); // convert CRLF to LF } // store in local forage exports.projectWindows.updateFile(path, data); console.log("Uploaded " + path + " " + data.length + " bytes"); uploadNextFile(); }; reader.readAsArrayBuffer(f); // read as binary } } if (files) uploadNextFile(); } async function _openLocalDirectory(e) { var pickerfn = window['showDirectoryPicker']; if (!pickerfn) { (0, dialogs_1.alertError)(`This browser can't open local files on your computer, yet. Try Chrome.`); } var dirHandle = await pickerfn(); var repoid = dirHandle.name; var storekey = '__localfs__' + repoid; var fsdata = { handle: dirHandle, }; var lstore = localforage.createInstance({ name: storekey, version: 2.0 }); await lstore.setItem(storekey, fsdata); exports.qs = { localfs: repoid }; gotoNewLocation(true); } async function promptUser(message) { return new Promise((resolve, reject) => { bootbox.prompt(DOMPurify.sanitize(message), (result) => { resolve(result); }); }); } async function getLocalFilesystem(repoid) { const options = { mode: 'readwrite' }; var storekey = '__localfs__' + repoid; var lstore = localforage.createInstance({ name: storekey, version: 2.0 }); var fsdata = await lstore.getItem(storekey); var dirHandle = fsdata.handle; console.log(fsdata, dirHandle); var granted = await dirHandle.queryPermission(options); console.log(granted); if (granted !== 'granted') { await promptUser(`Request permissions to access filesystem?`); granted = await dirHandle.requestPermission(options); } if (granted !== 'granted') { (0, dialogs_1.alertError)(`Could not get permission to access filesystem.`); return; } return { getFileData: async (path) => { console.log('getFileData', path); let fileHandle = await dirHandle.getFileHandle(path, { create: false }); console.log('getFileData', fileHandle); let file = await fileHandle.getFile(); console.log('getFileData', file); let contents = await ((0, util_1.isProbablyBinary)(path) ? file.binary() : file.text()); console.log(fileHandle, file, contents); return contents; }, setFileData: async (path, data) => { //let vh = await dirHandle.getFileHandle(path, { create: true }); } }; } function getCurrentMainFilename() { return (0, util_1.getFilenameForPath)(exports.current_project.mainPath); } exports.getCurrentMainFilename = getCurrentMainFilename; function getCurrentEditorFilename() { return (0, util_1.getFilenameForPath)(exports.projectWindows.getActiveID()); } exports.getCurrentEditorFilename = getCurrentEditorFilename; function _revertFile(e) { var wnd = exports.projectWindows.getActive(); if (wnd && wnd.setText) { var fn = exports.projectWindows.getActiveID(); $.get("presets/" + (0, util_1.getBasePlatform)(exports.platform_id) + "/" + fn, (text) => { bootbox.confirm("Reset '" + DOMPurify.sanitize(fn) + "' to default?", (ok) => { if (ok) { wnd.setText(text); } }); }, 'text') .fail(() => { if (exports.repo_id) (0, dialogs_1.alertError)("Can only revert built-in examples. If you want to revert all files, You can pull from the repository."); else (0, dialogs_1.alertError)("Can only revert built-in examples."); }); } else { (0, dialogs_1.alertError)("Cannot revert the active window. Please choose a text file."); } } function _deleteFile(e) { var wnd = exports.projectWindows.getActive(); if (wnd && wnd.getPath) { var fn = exports.projectWindows.getActiveID(); bootbox.confirm("Delete '" + DOMPurify.sanitize(fn) + "'?", (ok) => { if (ok) { store.removeItem(fn).then(() => { // if we delete what is selected if (exports.qs.file == fn) { userPrefs.unsetLastPreset(); gotoNewLocation(); } else { updateSelector(); (0, dialogs_1.alertInfo)("Deleted " + fn); } }); } }); } else { (0, dialogs_1.alertError)("Cannot delete the active window."); } } function _renameFile(e) { var wnd = exports.projectWindows.getActive(); if (wnd && wnd.getPath && exports.current_project.getFile(wnd.getPath())) { var fn = exports.projectWindows.getActiveID(); bootbox.prompt({ title: "Rename '" + DOMPurify.sanitize(fn) + "' to?", value: fn, callback: (newfn) => { var data = exports.current_project.getFile(wnd.getPath()); if (newfn && newfn != fn && data) { if (!checkEnteredFilename(newfn)) return; store.removeItem(fn).then(() => { return store.setItem(newfn, data); }).then(() => { updateSelector(); alert("Renamed " + fn + " to " + newfn); // need alert() so it pauses if (fn == exports.current_project.mainPath) { reloadProject(newfn); } }); } } }); } else { (0, dialogs_1.alertError)("Cannot rename the active window."); } } function populateExamples(sel) { let files = {}; let optgroup; const PRESETS = exports.platform.getPresets ? exports.platform.getPresets() : []; for (var i = 0; i < PRESETS.length; i++) { var preset = PRESETS[i]; var name = preset.chapter ? (preset.chapter + ". " + preset.name) : preset.name; var isCurrentPreset = preset.id == exports.current_project.mainPath; if (preset.category) { optgroup = $("").attr('label', 'Examples: ' + preset.category).appendTo(sel); } else if (!optgroup) { optgroup = $("").attr('label', 'Examples').appendTo(sel); } optgroup.append($("").attr('label', 'Repositories').appendTo(sel); for (let repopath in repos) { var repo = repos[repopath]; if (repo.platform_id && (0, util_1.getBasePlatform)(repo.platform_id) == (0, util_1.getBasePlatform)(exports.platform_id)) { optgroup.append($("").attr('label', category).appendTo(sel); let name = key.substring(prefix.length); optgroup.append($("