diff --git a/.travis.yml b/.travis.yml index 5c26caf6..455fb9b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,3 +8,7 @@ before_install: language: node_js node_js: - "6.5.0" +script: + - npm run build + - npm test + \ No newline at end of file diff --git a/Makefile b/Makefile index a182cfad..a6d7b3f3 100644 --- a/Makefile +++ b/Makefile @@ -27,4 +27,6 @@ web: tsweb: ifconfig | grep inet - node ../nodejs-typescript-webserver/bin/FileServer.js . + tsc -w & + python2 -m SimpleHTTPServer 2>> http.out + #node ../nodejs-typescript-webserver/bin/FileServer.js . diff --git a/doc/notes.txt b/doc/notes.txt index 2c943109..4781c0ad 100644 --- a/doc/notes.txt +++ b/doc/notes.txt @@ -43,6 +43,7 @@ TODO: (https://developers.google.com/web/updates/2018/04/loading-wasm) - unify versioning - more UI tests +- base-36 encoding for LZG WEB WORKER FORMAT diff --git a/index.html b/index.html index f57f8027..7ec150a4 100644 --- a/index.html +++ b/index.html @@ -207,6 +207,13 @@ ga('send', 'pageview'); + + @@ -232,9 +239,11 @@ ga('send', 'pageview'); - + - + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..c4c1b360 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,292 @@ +{ + "name": "8bitworkshop", + "version": "2.2.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/jquery": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.4.tgz", + "integrity": "sha512-KqgLNDh8oTl43/2B78S7tjwPnOxPtP9wQsLptbuQCmMwqH5QuPZOk36RsNgbs3mnq/3SCyG1l/GJmhKk5Dg68g==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true, + "requires": { + "graceful-readlink": "1.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "diff": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", + "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", + "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "growl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "3.0.1", + "lodash.keys": "3.1.2" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basecreate": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", + "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash.create": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", + "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", + "dev": true, + "requires": { + "lodash._baseassign": "3.2.0", + "lodash._basecreate": "3.0.3", + "lodash._isiterateecall": "3.0.9" + } + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "3.9.1", + "lodash.isarguments": "3.1.0", + "lodash.isarray": "3.0.4" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.3.tgz", + "integrity": "sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg==", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.9.0", + "debug": "2.6.8", + "diff": "3.2.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.1", + "growl": "1.9.2", + "he": "1.1.1", + "json3": "3.3.2", + "lodash.create": "3.1.1", + "mkdirp": "0.5.1", + "supports-color": "3.1.2" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "supports-color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", + "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + }, + "wavedrom-cli": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wavedrom-cli/-/wavedrom-cli-0.5.0.tgz", + "integrity": "sha1-h+lb+naRpbaTmPJzL13QycKkiCE=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } +} diff --git a/package.json b/package.json index ee484f90..1d0bbf61 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "author": "Steven Hugg", "dependencies": {}, "devDependencies": { + "@types/jquery": "^3.3.4", "mocha": "^3.2.0", "mocha-phantomjs": "^4.1.0", "wavedrom-cli": "^0.5.0" @@ -15,6 +16,7 @@ "test": "tests" }, "scripts": { + "build": "tsc", "test": "npm run test-node && npm run test-browser", "test-one": "mocha --recursive --timeout 20000", "test-node": "mocha --recursive --timeout 20000 test/cli", diff --git a/src/project.js b/src/project.js deleted file mode 100644 index 099bf204..00000000 --- a/src/project.js +++ /dev/null @@ -1,237 +0,0 @@ -"use strict"; - -function SourceFile(lines, text) { - lines = lines || []; - this.lines = lines; - this.text = text; - this.offset2line = {}; - this.line2offset = {}; - for (var info of lines) { - if (info.offset >= 0) { - this.offset2line[info.offset] = info.line; - this.line2offset[info.line] = info.offset; - } - } - this.findLineForOffset = function(PC) { - if (this.offset2line) { - for (var i=0; i<16; i++) { - var line = this.offset2line[PC]; - if (line >= 0) { - return line; - } - PC--; - } - } - return 0; - } - this.lineCount = function() { return lines.length; } -} - -function CodeProject(worker, platform_id, platform, store, mainpath) { - var self = this; - - var filedata = {}; - var listings; - - self.callbackBuildResult = function(result) { }; - self.callbackBuildStatus = function(busy) { }; - - var pendingWorkerMessages = 0; - - var tools_preloaded = {}; - function preloadWorker(path) { - var tool = platform.getToolForFilename(path); - if (tool && !tools_preloaded[tool]) { - worker.postMessage({preload:tool, platform:platform_id}); - tools_preloaded[tool] = true; - } - } - - self.setMainPath = function(path) { - mainpath = path; - } - - // TODO: get local file as well as presets? - self.loadFiles = function(paths, callback) { - var result = []; - function loadNext() { - var path = paths.shift(); - if (!path) { - callback(null, result); // TODO? - } else { - store.getItem(path, function(err, value) { - if (err) { - callback(err); - } else if (value) { - result.push({ - path:path, - filename:getFilenameForPath(path), - data:value - }); - filedata[path] = value; - loadNext(); - } else { - var webpath = "presets/" + platform_id + "/" + path; - if (platform_id == 'vcs' && path.indexOf('.') <= 0) - webpath += ".a"; // legacy stuff - // TODO: cache files - $.get( webpath, function( text ) { - console.log("GET",webpath,text.length,'bytes'); - result.push({ - path:path, - filename:getFilenameForPath(path), - data:text - }); - filedata[path] = text; - loadNext(); - }, 'text') - .fail(function() { - callback("Could not load preset " + path); - }); - } - }); - } - } - loadNext(); // load first file - } - - function parseFileDependencies(text) { - var files = []; - if (platform_id == 'verilog') { - var re = /^(`include|[.]include)\s+"(.+?)"/gm; - var m; - while (m = re.exec(text)) { - files.push(m[2]); - } - } else { - var re = /^([;#]|[/][/][#])link\s+"(.+?)"/gm; - var m; - while (m = re.exec(text)) { - files.push(m[2]); - } - } - return files; - } - - function loadFileDependencies(text, callback) { - var paths = parseFileDependencies(text); - self.loadFiles(paths, callback); - } - - function okToSend() { - return pendingWorkerMessages++ == 0; - } - - function updateFileInStore(path, text) { - // protect against accidential whole-file deletion - if (text.trim().length) { - // TODO? (originalFileID != path || text != originalText)) { - store.setItem(path, text); - } - } - - // TODO: test duplicate files, local paths mixed with presets - function buildWorkerMessage(depends) { - preloadWorker(mainpath); - var msg = {updates:[], buildsteps:[]}; - // TODO: add preproc directive for __MAINFILE__ - var mainfilename = getFilenameForPath(mainpath); - var maintext = self.getFile(mainpath); - msg.updates.push({path:mainfilename, data:maintext}); - msg.buildsteps.push({path:mainfilename, platform:platform_id, tool:platform.getToolForFilename(mainpath), mainfile:true}); - for (var i=0; i 1) { - self.sendBuild(); - pendingWorkerMessages = 0; - } else { - pendingWorkerMessages = 0; - } - self.callbackBuildStatus(false); - if (e.data && !e.data.unchanged) { - self.processBuildResult(e.data); - self.callbackBuildResult(e.data); // call with data when changed - } - }; -} - diff --git a/src/project.ts b/src/project.ts new file mode 100644 index 00000000..c70f3aae --- /dev/null +++ b/src/project.ts @@ -0,0 +1,301 @@ +"use strict"; + +type FileData = string | Uint8Array; + +interface SourceLine { + offset:number; + line:number; +} + +interface Dependency { + path:string, + filename:string, + data:FileData // TODO: or binary? +} + +interface WorkerFileUpdate { path:string, data:FileData }; +interface WorkerBuildStep { path:string, platform:string, tool:string, mainfile?:boolean }; + +interface WorkerMessage { + updates:WorkerFileUpdate[], + buildsteps:WorkerBuildStep[] +} + +interface WorkerError { + line:number, + msg:string, + //TODO +} + +export class SourceFile { + lines: SourceLine[]; + text: string; + offset2line: {[offset:number]:number}; + line2offset: {[line:number]:number}; + + constructor(lines:SourceLine[], text?:string) { + lines = lines || []; + this.lines = lines; + this.text = text; + this.offset2line = {}; + this.line2offset = {}; + for (var info of lines) { + if (info.offset >= 0) { + this.offset2line[info.offset] = info.line; + this.line2offset[info.line] = info.offset; + } + } + } + findLineForOffset(PC:number):number { + if (this.offset2line) { + for (var i=0; i<16; i++) { + var line = this.offset2line[PC]; + if (line >= 0) { + return line; + } + PC--; + } + } + return null; + } + lineCount():number { return this.lines.length; } +} + +interface CodeListing { + lines:SourceLine[], + asmlines:SourceLine[], + text:string, + sourcefile:SourceFile, + assemblyfile:SourceFile +} + +type CodeListingMap = {[path:string]:CodeListing}; + +interface WorkerResult { + output:Uint8Array, //TODO + errors:WorkerError[], + listings:CodeListingMap, +} + +type BuildResultCallback = (result:WorkerResult) => void; +type BuildStatusCallback = (busy:boolean) => void; +type LoadFilesCallback = (err:string, result?:Dependency[]) => void; +type IterateFilesCallback = (path:string, data:FileData) => void; +type GetRemoteCallback = any; // TODO (path:string, (text:string) => FileData) => void; + +export class CodeProject { + filedata : {[path:string]:FileData} = {}; + listings : CodeListingMap; + mainpath : string; + pendingWorkerMessages = 0; + tools_preloaded = {}; + callbackBuildResult : BuildResultCallback; + callbackBuildStatus : BuildStatusCallback; + worker: any; + platform_id : string; + platform: any; + store: any; + callbackGetRemote : GetRemoteCallback; + mainPath: string; + + constructor(worker, platform_id:string, platform, store) { + this.worker = worker; + this.platform_id = platform_id; + this.platform = platform; + this.store = store; + + worker.onmessage = (e) => { + if (this.pendingWorkerMessages > 1) { + this.sendBuild(); + this.pendingWorkerMessages = 0; + } else { + this.pendingWorkerMessages = 0; + } + if (this.callbackBuildStatus) this.callbackBuildStatus(false); + if (e.data && !e.data.unchanged) { + this.processBuildResult(e.data); + if (this.callbackBuildResult) this.callbackBuildResult(e.data); // call with data when changed + } + }; + } + + preloadWorker(path:string) { + var tool = this.platform.getToolForFilename(path); + if (tool && !this.tools_preloaded[tool]) { + this.worker.postMessage({preload:tool, platform:this.platform_id}); + this.tools_preloaded[tool] = true; + } + } + + parseFileDependencies(text:string):string[] { + var files = []; + if (this.platform_id == 'verilog') { + var re = /^(`include|[.]include)\s+"(.+?)"/gm; + var m; + while (m = re.exec(text)) { + files.push(m[2]); + } + } else { + var re = /^([;#]|[/][/][#])link\s+"(.+?)"/gm; + var m; + while (m = re.exec(text)) { + files.push(m[2]); + } + } + return files; + } + + loadFileDependencies(text:string, callback:LoadFilesCallback) { + var paths = this.parseFileDependencies(text); + this.loadFiles(paths, callback); + } + + okToSend():boolean { + return this.pendingWorkerMessages++ == 0; + } + + updateFileInStore(path:string, text:FileData) { + // protect against accidential whole-file deletion + if ((text).trim && (text).trim().length) { + // TODO? (originalFileID != path || text != originalText)) { + this.store.setItem(path, text); + } + } + + // TODO: test duplicate files, local paths mixed with presets + buildWorkerMessage(depends:Dependency[]) { + this.preloadWorker(this.mainpath); + var msg = {updates:[], buildsteps:[]}; + // TODO: add preproc directive for __MAINFILE__ + var mainfilename = getFilenameForPath(this.mainpath); + var maintext = this.getFile(this.mainpath); + msg.updates.push({path:mainfilename, data:maintext}); + msg.buildsteps.push({path:mainfilename, platform:this.platform_id, tool:this.platform.getToolForFilename(this.mainpath), mainfile:true}); + for (var i=0; i { + var path = paths.shift(); + if (!path) { + callback(null, result); // TODO? + } else { + this.store.getItem(path, (err, value) => { + if (err) { + callback(err); + } else if (value) { + result.push({ + path:path, + filename:getFilenameForPath(path), + data:value + }); + this.filedata[path] = value; + loadNext(); + } else { + var webpath = "presets/" + this.platform_id + "/" + path; + if (this.platform_id == 'vcs' && path.indexOf('.') <= 0) + webpath += ".a"; // legacy stuff + // TODO: cache files + this.callbackGetRemote( webpath, (text:string) => { + console.log("GET",webpath,text.length,'bytes'); + result.push({ + path:path, + filename:getFilenameForPath(path), + data:text + }); + this.filedata[path] = text; + loadNext(); + }, 'text') + .fail(function() { + callback("Could not load preset " + path); + }); + } + }); + } + } + loadNext(); // load first file + } + + getFile(path:string):FileData { + return this.filedata[path]; + } + + iterateFiles(callback:IterateFilesCallback) { + for (var path in this.filedata) { + callback(path, this.getFile(path)); + } + } + + sendBuild() { + var self = this; + var maindata = this.getFile(this.mainpath); + var text = typeof maindata === "string" ? maindata : ''; + this.loadFileDependencies(text, (err, depends) => { + if (err) { + console.log(err); // TODO? + } + if (this.platform_id == 'verilog') { + // TODO: should get rid of this msg format + this.worker.postMessage({ + code:text, + dependencies:depends, + platform:this.platform_id, + tool:this.platform.getToolForFilename(this.mainpath) + }); + } else { + var workermsg = this.buildWorkerMessage(depends); + this.worker.postMessage(workermsg); + } + }); + } + + updateFile(path:string, text:FileData) { + this.updateFileInStore(path, text); // TODO: isBinary + this.filedata[path] = text; + if (this.okToSend()) { + if (!this.mainpath) this.mainpath = path; + if (this.callbackBuildStatus) this.callbackBuildStatus(true); + this.sendBuild(); + } + }; + + processBuildResult(data:WorkerResult) { + // TODO: link listings with source files + this.listings = data.listings; + if (this.listings) { + for (var lstname in this.listings) { + var lst = this.listings[lstname]; + if (lst.lines) + lst.sourcefile = new SourceFile(lst.lines); + if (lst.asmlines) + lst.assemblyfile = new SourceFile(lst.asmlines, lst.text); + } + } + } + + getListings() : CodeListingMap { + return this.listings; + } + + // returns first listing in format [prefix].lst (TODO: could be better) + getListingForFile(path) : CodeListing { + var fnprefix = getFilenamePrefix(getFilenameForPath(path)); + var listings = this.getListings(); + for (var lstfn in listings) { + if (getFilenamePrefix(lstfn) == fnprefix) { + return listings[lstfn]; + } + } + } +} + diff --git a/src/ui.js b/src/ui.js index c7c623ef..34822c9d 100644 --- a/src/ui.js +++ b/src/ui.js @@ -13,6 +13,9 @@ var toolbar = $("#controls_top"); var current_project; // current CodeProject object +var projectWindows; // window manager + + // TODO: codemirror multiplex support? var TOOL_TO_SOURCE_STYLE = { 'dasm': '6502', @@ -66,6 +69,8 @@ function setLastPreset(id) { function initProject() { current_project = new CodeProject(newWorker(), platform_id, platform, store); + projectWindows = new ProjectWindows($("#workspace")[0], current_project); + current_project.callbackGetRemote = $.get; current_project.callbackBuildResult = function(result) { setCompileOutput(result); refreshWindowList(); @@ -82,622 +87,6 @@ function initProject() { }; } -// 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; @@ -781,7 +170,7 @@ function loadProject(preset_id) { // set current file ID main_file_id = preset_id; setLastPreset(preset_id); - current_project.setMainPath(preset_id); + current_project.mainPath = preset_id; // load files from storage or web URLs current_project.loadFiles([preset_id], function(err, result) { if (err) { diff --git a/src/util.js b/src/util.ts similarity index 91% rename from src/util.js rename to src/util.ts index f539873c..63bdb189 100644 --- a/src/util.js +++ b/src/util.ts @@ -1,23 +1,23 @@ "use strict"; -function lpad(s,n) { while(s.length 0) ? s.substr(0, pos) : s; } -function hex(v, nd) { +function hex(v:number, nd?:number) { try { if (!nd) nd = 2; var s = v.toString(16).toUpperCase(); @@ -29,7 +29,7 @@ function hex(v, nd) { } } -function arrayCompare(a,b) { +function arrayCompare(a:any[], b:any[]):boolean { if (a == null && b == null) return true; if (a == null) return false; if (b == null) return false; @@ -40,7 +40,7 @@ function arrayCompare(a,b) { return true; } -function invertMap(m) { +function invertMap(m:{}):{} { var r = {}; if (m) { for (var k in m) r[m[k]] = k; @@ -48,7 +48,7 @@ function invertMap(m) { return r; } -function highlightDifferences(s1, s2) { +function highlightDifferences(s1:string, s2:string):string { var split1 = s1.split(/(\S+\s+)/).filter(function(n) {return n}); var split2 = s2.split(/(\S+\s+)/).filter(function(n) {return n}); var i = 0; @@ -104,7 +104,7 @@ function lzgmini() { // Decode LZG coded data. The function returns the size of the decoded data. // Use any of the get* methods to retrieve the decoded data. - this.decode = function(data) { + this.decode = function(data:number[]):number[] { // Start by clearing the decompressed array in this object outdata = null; @@ -112,7 +112,7 @@ function lzgmini() { if ((data.length < LZG_HEADER_SIZE) || (data[0] != 76) || (data[1] != 90) || (data[2] != 71)) { - return 0; + return null; } // Calculate & check the checksum @@ -122,7 +122,7 @@ function lzgmini() { (data[14] & 0xff); if (calcChecksum(data) != checksum) { - return 0; + return null; } // Check which method to use @@ -223,13 +223,13 @@ function lzgmini() { } // Get the decoded byte array - this.getByteArray = function() + this.getByteArray = function():number[] { return outdata; } // Get the decoded string from a Latin 1 (or ASCII) encoded array - this.getStringLatin1 = function() + this.getStringLatin1 = function():string { var str = ""; if (outdata != null) @@ -245,7 +245,7 @@ function lzgmini() { } // Get the decoded string from an UTF-8 encoded array - this.getStringUTF8 = function() + this.getStringUTF8 = function():string { var str = ""; if (outdata != null) diff --git a/src/views.ts b/src/views.ts new file mode 100644 index 00000000..0226dece --- /dev/null +++ b/src/views.ts @@ -0,0 +1,547 @@ +"use strict"; + +// TODO: move to different namespace +var CodeMirror; +var platform; +var platform_id : string; +var compparams; +var addr2symbol : {[addr:number]:string}; +var current_project; +var VirtualList; +var lastDebugState; +var pixeditframe; + +// TODO: functions +var inspectVariable; +var jumpToLine; + +// 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 (const _nextofs of Object.keys(addr2symbol)) { + var nextofs = parseInt(_nextofs); // convert from string (stupid JS) + 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; + } +} + diff --git a/src/windows.ts b/src/windows.ts new file mode 100644 index 00000000..68d322a7 --- /dev/null +++ b/src/windows.ts @@ -0,0 +1,78 @@ +"use strict"; + +import $ = require("jquery"); +import { CodeProject } from "./project"; + +function ProjectWindows(containerdiv:HTMLElement, project:CodeProject) { + 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, 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) { + id2window[id] = window; + } + + 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."); + } +}; + diff --git a/test/cli/testpixelconvert.js b/test/cli/testpixelconvert.js index a170b401..b83ac275 100644 --- a/test/cli/testpixelconvert.js +++ b/test/cli/testpixelconvert.js @@ -7,7 +7,7 @@ var includeInThisContext = function(path) { vm.runInThisContext(code, path); }; -includeInThisContext("src/util.js"); +includeInThisContext("gen/util.js"); includeInThisContext("src/pixed/pixeleditor.js"); describe('Pixel editor', function() { diff --git a/test/cli/teststore.js b/test/cli/teststore.js index c061e026..c1dbf891 100644 --- a/test/cli/teststore.js +++ b/test/cli/teststore.js @@ -45,9 +45,9 @@ global.localStorage = { }; includeInThisContext("localForage/dist/localforage.js"); -includeInThisContext("src/util.js"); +includeInThisContext("gen/util.js"); includeInThisContext("src/store.js"); -includeInThisContext("src/project.js"); +var prj = require("../../gen/project.js"); var test_platform_id = "_TEST"; @@ -74,7 +74,7 @@ describe('Store', function() { var store = createNewPersistentStore(test_platform_id, function() { var worker = {}; var platform = {}; - var project = new CodeProject(worker, test_platform_id, platform, store); + var project = new prj.CodeProject(worker, test_platform_id, platform, store); project.loadFiles(['test'], function(err, result) { assert.equal(null, err); assert.deepEqual([ { path: 'test', filename: 'test', data: 'a' } ], result); @@ -106,7 +106,7 @@ describe('Store', function() { var platform = { getToolForFilename: function(fn) { return 'dasm'; }, }; - var project = new CodeProject(worker, test_platform_id, platform, store); + var project = new prj.CodeProject(worker, test_platform_id, platform, store); project.callbackBuildStatus = function(b) { msgs.push(b) }; project.updateFile('test.a', ' lda #0'); project.updateFile('test.a', ' lda #1'); // don't send twice (yet) @@ -129,7 +129,7 @@ describe('Store', function() { }; var platform = { }; - var project = new CodeProject(worker, test_platform_id, platform, store); + var project = new prj.CodeProject(worker, test_platform_id, platform, store); project.callbackBuildStatus = function(b) { msgs.push(b) }; var buildresult = { listings: { @@ -143,8 +143,8 @@ describe('Store', function() { var lst = buildresult.listings.test; console.log(lst); assert.equal(3, lst.sourcefile.findLineForOffset(61440+15)); - assert.equal(0, lst.sourcefile.findLineForOffset(61440+16)); - assert.equal(0, lst.sourcefile.findLineForOffset(61440-1)); + assert.equal(null, lst.sourcefile.findLineForOffset(61440+16)); + assert.equal(null, lst.sourcefile.findLineForOffset(61440-1)); assert.equal(1, lst.sourcefile.lineCount()); done(); }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..4a390bf5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "outDir": "./gen", + "allowJs": true, + "sourceMap": true, + "target": "es5", + "lib": [ + "es2016", + "dom" + ] + }, + "include": [ + "./src/*.ts" + ] +}