diff --git a/src/ide/project.ts b/src/ide/project.ts index c40c1376..509ce7c7 100644 --- a/src/ide/project.ts +++ b/src/ide/project.ts @@ -1,32 +1,36 @@ -import { FileData, Dependency, SourceLine, SourceFile, CodeListing, CodeListingMap, WorkerError, Segment, WorkerResult, WorkerOutputResult, isUnchanged, isOutputResult, WorkerMessage, WorkerItemUpdate, WorkerErrorResult, isErrorResult } from "../common/workertypes"; -import { getFilenamePrefix, getFolderForPath, isProbablyBinary, getBasePlatform, getWithBinary } from "../common/util"; -import { Platform } from "../common/baseplatform"; import localforage from "localforage"; +import { Platform } from "../common/baseplatform"; +import { getBasePlatform, getFilenamePrefix, getFolderForPath, getWithBinary, isProbablyBinary } from "../common/util"; +import { CodeListing, CodeListingMap, Dependency, FileData, Segment, SourceFile, WorkerErrorResult, WorkerItemUpdate, WorkerMessage, WorkerOutputResult, WorkerResult, isErrorResult, isOutputResult } from "../common/workertypes"; export interface ProjectFilesystem { - getFileData(path: string) : Promise; - setFileData(path: string, data: FileData) : Promise; + getFileData(path: string): Promise; + setFileData(path: string, data: FileData): Promise; + onFileSystemUpdate(callback: (path: string) => void): void; } export class WebPresetsFileSystem implements ProjectFilesystem { - preset_id : string; + preset_id: string; constructor(platform_id: string) { this.preset_id = getBasePlatform(platform_id); // remove .suffix from preset name } async getRemoteFile(path: string): Promise { - return new Promise( (yes,no)=> { + return new Promise((yes, no) => { return getWithBinary(path, yes, isProbablyBinary(path) ? 'arraybuffer' : 'text'); }); } - async getFileData(path: string) : Promise { + async getFileData(path: string): Promise { // found on remote fetch? var webpath = "presets/" + this.preset_id + "/" + path; var data = await this.getRemoteFile(webpath); - if (data) console.log("read",webpath,data.length,'bytes'); + if (data) console.log("read", webpath, data.length, 'bytes'); return data; } - async setFileData(path: string, data: FileData) : Promise { + async setFileData(path: string, data: FileData): Promise { + // not implemented + } + onFileSystemUpdate(callback: (path: string) => void): void { // not implemented } } @@ -42,7 +46,9 @@ export class NullFilesystem implements ProjectFilesystem { this.sets.push(path); return; } - + onFileSystemUpdate(callback: (path: string) => void): void { + // not implemented + } } export class OverlayFilesystem implements ProjectFilesystem { @@ -64,9 +70,13 @@ export class OverlayFilesystem implements ProjectFilesystem { await this.overlayfs.setFileData(path, data); return this.basefs.setFileData(path, data); } + onFileSystemUpdate(callback: (path: string) => void): void { + this.overlayfs.onFileSystemUpdate(callback); + this.basefs.onFileSystemUpdate(callback); + } } -export class LocalForageFilesystem { +export class LocalForageFilesystem implements ProjectFilesystem { store: any; constructor(store: any) { this.store = store; @@ -77,36 +87,40 @@ export class LocalForageFilesystem { async setFileData(path: string, data: FileData): Promise { return this.store.setItem(path, data); } + onFileSystemUpdate(callback: (path: string) => void): void { + // not implemented + } } -type BuildResultCallback = (result:WorkerResult) => void; -type BuildStatusCallback = (busy:boolean) => void; -type IterateFilesCallback = (path:string, data:FileData) => void; +type BuildResultCallback = (result: WorkerResult) => void; +type BuildStatusCallback = (busy: boolean) => void; +type IterateFilesCallback = (path: string, data: FileData) => void; -function isEmptyString(text : FileData) { +function isEmptyString(text: FileData) { return typeof text == 'string' && text.trim && text.trim().length == 0; } export class CodeProject { - filedata : {[path:string]:FileData} = {}; - listings : CodeListingMap; - segments : Segment[]; - mainPath : string; + filedata: { [path: string]: FileData } = {}; + listings: CodeListingMap; + segments: Segment[]; + mainPath: string; pendingWorkerMessages = 0; tools_preloaded = {}; - worker : Worker; - platform_id : string; - platform : Platform; - isCompiling : boolean = false; + worker: Worker; + platform_id: string; + platform: Platform; + isCompiling: boolean = false; filename2path = {}; // map stripped paths to full paths - filesystem : ProjectFilesystem; - dataItems : WorkerItemUpdate[]; - remoteTool? : string; + filesystem: ProjectFilesystem; + dataItems: WorkerItemUpdate[]; + remoteTool?: string; - callbackBuildResult : BuildResultCallback; - callbackBuildStatus : BuildStatusCallback; + callbackBuildResult: BuildResultCallback; + callbackBuildStatus: BuildStatusCallback; + onFileChanged: (path: string, data: FileData) => void; - constructor(worker, platform_id:string, platform, filesystem: ProjectFilesystem) { + constructor(worker, platform_id: string, platform, filesystem: ProjectFilesystem) { this.worker = worker; this.platform_id = platform_id; this.platform = platform; @@ -115,9 +129,19 @@ export class CodeProject { worker.onmessage = (e) => { this.receiveWorkerMessage(e.data); }; + + filesystem.onFileSystemUpdate(async (path: string) => { + if (path in this.filedata) { + var data = await this.filesystem.getFileData(path); + if (data) { + this.updateFile(path, data); + if (this.onFileChanged) this.onFileChanged(path, data); + } + } + }); } - receiveWorkerMessage(data : WorkerResult) { + receiveWorkerMessage(data: WorkerResult) { var notfinal = this.pendingWorkerMessages > 1; if (notfinal) { this.sendBuild(); @@ -144,15 +168,15 @@ export class CodeProject { } } - preloadWorker(path:string) { + preloadWorker(path: string) { var tool = this.getToolForFilename(path); if (tool && !this.tools_preloaded[tool]) { - this.worker.postMessage({preload:tool, platform:this.platform_id}); + this.worker.postMessage({ preload: tool, platform: this.platform_id }); this.tools_preloaded[tool] = true; } } - pushAllFiles(files:string[], fn:string) { + pushAllFiles(files: string[], fn: string) { // look for local and preset files files.push(fn); // look for files in current (main file) folder @@ -162,7 +186,7 @@ export class CodeProject { } // TODO: use tool id to parse files, not platform - parseIncludeDependencies(text:string):string[] { + parseIncludeDependencies(text: string): string[] { let files = []; let m; if (this.platform_id.startsWith('verilog')) { @@ -179,7 +203,7 @@ export class CodeProject { // include .arch (json) statements let re2 = /^\s*([.]arch)\s+(\w+)/gmi; while (m = re2.exec(text)) { - this.pushAllFiles(files, m[2]+".json"); + this.pushAllFiles(files, m[2] + ".json"); } // include $readmem[bh] (TODO) let re3 = /\$readmem[bh]\("(.+?)"/gmi; @@ -226,7 +250,7 @@ export class CodeProject { return files; } - parseLinkDependencies(text:string):string[] { + parseLinkDependencies(text: string): string[] { let files = []; let m; if (this.platform_id.startsWith('verilog')) { @@ -240,8 +264,8 @@ export class CodeProject { } return files; } - - loadFileDependencies(text:string) : Promise { + + loadFileDependencies(text: string): Promise { let includes = this.parseIncludeDependencies(text); let linkfiles = this.parseLinkDependencies(text); let allfiles = includes.concat(linkfiles); @@ -256,49 +280,51 @@ export class CodeProject { }); } - okToSend():boolean { + okToSend(): boolean { return this.pendingWorkerMessages++ == 0 && this.mainPath != null; } - updateFileInStore(path:string, text:FileData) { + updateFileInStore(path: string, text: FileData) { this.filesystem.setFileData(path, text); } // TODO: test duplicate files, local paths mixed with presets - buildWorkerMessage(depends:Dependency[]) : WorkerMessage { + buildWorkerMessage(depends: Dependency[]): WorkerMessage { this.preloadWorker(this.mainPath); - var msg : WorkerMessage = {updates:[], buildsteps:[]}; + var msg: WorkerMessage = { updates: [], buildsteps: [] }; // TODO: add preproc directive for __MAINFILE__ var mainfilename = this.stripLocalPath(this.mainPath); var maintext = this.getFile(this.mainPath); var depfiles = []; - msg.updates.push({path:mainfilename, data:maintext}); + msg.updates.push({ path: mainfilename, data: maintext }); this.filename2path[mainfilename] = this.mainPath; const tool = this.getToolForFilename(this.mainPath); let usesRemoteTool = tool.startsWith('remote:'); for (var dep of depends) { // remote tools send both includes and linked files in one build step if (!dep.link || usesRemoteTool) { - msg.updates.push({path:dep.filename, data:dep.data}); + msg.updates.push({ path: dep.filename, data: dep.data }); depfiles.push(dep.filename); } this.filename2path[dep.filename] = dep.path; } msg.buildsteps.push({ - path:mainfilename, - files:[mainfilename].concat(depfiles), - platform:this.platform_id, - tool:this.getToolForFilename(this.mainPath), - mainfile:true}); + path: mainfilename, + files: [mainfilename].concat(depfiles), + platform: this.platform_id, + tool: this.getToolForFilename(this.mainPath), + mainfile: true + }); for (var dep of depends) { if (dep.data && dep.link) { this.preloadWorker(dep.filename); - msg.updates.push({path:dep.filename, data:dep.data}); + msg.updates.push({ path: dep.filename, data: dep.data }); msg.buildsteps.push({ - path:dep.filename, - files:[dep.filename].concat(depfiles), - platform:this.platform_id, - tool:this.getToolForFilename(dep.path)}); + path: dep.filename, + files: [dep.filename].concat(depfiles), + platform: this.platform_id, + tool: this.getToolForFilename(dep.path) + }); } } if (this.dataItems) msg.setitems = this.dataItems; @@ -306,14 +332,14 @@ export class CodeProject { } // TODO: get local file as well as presets? - async loadFiles(paths:string[]) : Promise { - var result : Dependency[] = []; - var addResult = (path:string, data:FileData) => { + async loadFiles(paths: string[]): Promise { + var result: Dependency[] = []; + var addResult = (path: string, data: FileData) => { result.push({ - path:path, - filename:this.stripLocalPath(path), - link:true, - data:data + path: path, + filename: this.stripLocalPath(path), + link: true, + data: data }); } for (var path of paths) { @@ -336,12 +362,12 @@ export class CodeProject { return result; } - getFile(path:string):FileData { + getFile(path: string): FileData { return this.filedata[path]; } // TODO: purge files not included in latest build? - iterateFiles(callback:IterateFilesCallback) { + iterateFiles(callback: IterateFilesCallback) { for (var path in this.filedata) { callback(path, this.getFile(path)); } @@ -354,18 +380,18 @@ export class CodeProject { if (maindata instanceof Uint8Array) { this.isCompiling = true; this.receiveWorkerMessage({ - output:maindata, - errors:[], - listings:null, - symbolmap:null, - params:{} + output: maindata, + errors: [], + listings: null, + symbolmap: null, + params: {} }); return; } // otherwise, make it a string var text = typeof maindata === "string" ? maindata : ''; // TODO: load dependencies of non-main files - return this.loadFileDependencies(text).then( (depends) => { + return this.loadFileDependencies(text).then((depends) => { if (!depends) depends = []; var workermsg = this.buildWorkerMessage(depends); this.worker.postMessage(workermsg); @@ -373,7 +399,7 @@ export class CodeProject { }); } - updateFile(path:string, text:FileData) { + updateFile(path: string, text: FileData) { if (this.filedata[path] == text) return; // unchanged, don't update this.updateFileInStore(path, text); // TODO: isBinary this.filedata[path] = text; @@ -383,7 +409,7 @@ export class CodeProject { } }; - setMainFile(path:string) { + setMainFile(path: string) { this.mainPath = path; if (this.callbackBuildStatus) this.callbackBuildStatus(true); this.sendBuild(); @@ -410,25 +436,25 @@ export class CodeProject { processBuildSegments(data: WorkerOutputResult) { // save and sort segment list - var segs : Segment[] = (this.platform.getMemoryMap && this.platform.getMemoryMap()["main"]) || []; + var segs: Segment[] = (this.platform.getMemoryMap && this.platform.getMemoryMap()["main"]) || []; if (segs?.length) { segs.forEach(seg => seg.source = 'native'); } if (data.segments) { data.segments.forEach(seg => seg.source = 'linker'); segs = segs.concat(data.segments || []); } - segs.sort((a,b) => {return a.start-b.start}); + segs.sort((a, b) => { return a.start - b.start }); this.segments = segs; } - getListings() : CodeListingMap { + getListings(): CodeListingMap { return this.listings; } // returns first listing in format [prefix].lst (TODO: could be better) - getListingForFile(path: string) : CodeListing { + getListingForFile(path: string): CodeListing { // ignore include files (TODO) //if (path.toLowerCase().endsWith('.h') || path.toLowerCase().endsWith('.inc')) - //return; + //return; var fnprefix = getFilenamePrefix(this.stripLocalPath(path)); // find listing with matching prefix var listings = this.getListings(); @@ -442,13 +468,13 @@ export class CodeProject { } } } - - stripLocalPath(path : string) : string { + + stripLocalPath(path: string): string { if (this.mainPath) { var folder = getFolderForPath(this.mainPath); // TODO: kinda weird if folder is same name as file prefix - if (folder != '' && path.startsWith(folder+'/')) { - path = path.substring(folder.length+1); + if (folder != '' && path.startsWith(folder + '/')) { + path = path.substring(folder.length + 1); } } return path; @@ -463,7 +489,7 @@ export class CodeProject { } -export function createNewPersistentStore(storeid:string) : LocalForage { +export function createNewPersistentStore(storeid: string): LocalForage { var store = localforage.createInstance({ name: "__" + storeid, version: 2.0 diff --git a/src/ide/services.ts b/src/ide/services.ts index 236bada8..5290570c 100644 --- a/src/ide/services.ts +++ b/src/ide/services.ts @@ -1,5 +1,5 @@ -import { getFolderForPath, isProbablyBinary, stringToByteArray, byteArrayToString, byteArrayToUTF8 } from "../common/util"; +import { byteArrayToString, byteArrayToUTF8, isProbablyBinary, stringToByteArray } from "../common/util"; import { FileData } from "../common/workertypes"; import { CodeProject, ProjectFilesystem } from "./project"; @@ -10,34 +10,34 @@ declare var firebase; // https://github.com/philschatz/octokat.js/tree/master/examples export interface GHRepoMetadata { - url : string; // github url - platform_id : string; // e.g. "vcs" - sha? : string; // head commit sha + url: string; // github url + platform_id: string; // e.g. "vcs" + sha?: string; // head commit sha mainPath?: string; // main file path - branch? : string; // "master" was default, now fetched from GH + branch?: string; // "master" was default, now fetched from GH } export interface GHSession extends GHRepoMetadata { - user : string; // user name - reponame : string; // repo name - repopath : string; // "user/repo" - subtreepath : string; // tree/[branch]/[...] - prefix : string; // file prefix, "local/" or "" - repo : any; // [repo object] - tree? : any; // [tree object] - head? : any; // [head ref] + user: string; // user name + reponame: string; // repo name + repopath: string; // "user/repo" + subtreepath: string; // tree/[branch]/[...] + prefix: string; // file prefix, "local/" or "" + repo: any; // [repo object] + tree?: any; // [tree object] + head?: any; // [head ref] commit?: any; // after commit() - paths? : string[]; + paths?: string[]; } const README_md_template = "$NAME\n=====\n\n[Open this project in 8bitworkshop](http://8bitworkshop.com/redir.html?platform=$PLATFORM&githubURL=$GITHUBURL&file=$MAINFILE).\n"; -export function getRepos() : {[key:string]:GHRepoMetadata} { +export function getRepos(): { [key: string]: GHRepoMetadata } { var repos = {}; - for (var i=0; i any, githubToken:string, store, project : CodeProject) { + constructor(githubCons: () => any, githubToken: string, store, project: CodeProject) { this.githubCons = githubCons; this.githubToken = githubToken; this.store = store; this.project = project; this.recreateGithub(); } - + recreateGithub() { - this.github = new this.githubCons({token:this.githubToken}); + this.github = new this.githubCons({ token: this.githubToken }); } - - login() : Promise { + + login(): Promise { // already logged in? return immediately if (this.githubToken && this.githubToken.length) { - return new Promise( (yes,no) => { + return new Promise((yes, no) => { yes(); }); } // login via popup var provider = new firebase.auth.GithubAuthProvider(); provider.addScope('repo'); - return firebase.auth().signInWithPopup(provider).then( (result) => { + return firebase.auth().signInWithPopup(provider).then((result) => { this.githubToken = result.credential.accessToken; var user = result.user; this.recreateGithub(); @@ -92,11 +92,11 @@ export class GithubService { console.log("Stored GitHub OAUTH key"); }); } - - logout() : Promise { + + logout(): Promise { // already logged out? return immediately if (!(this.githubToken && this.githubToken.length)) { - return new Promise( (yes,no) => { + return new Promise((yes, no) => { yes(); }); } @@ -107,8 +107,8 @@ export class GithubService { this.recreateGithub(); }); } - - isFileIgnored(s : string) : boolean { + + isFileIgnored(s: string): boolean { s = s.toUpperCase(); if (s.startsWith("LICENSE")) return true; if (s.startsWith("README")) return true; @@ -116,7 +116,7 @@ export class GithubService { return false; } - async getGithubSession(ghurl:string) : Promise { + async getGithubSession(ghurl: string): Promise { var urlparse = parseGithubURL(ghurl); if (!urlparse) { throw new Error("Please enter a valid GitHub URL."); @@ -149,103 +149,104 @@ export class GithubService { return sess; } - getGithubHEADTree(ghurl:string) : Promise { + getGithubHEADTree(ghurl: string): Promise { var sess; - return this.getGithubSession(ghurl).then( (session) => { + return this.getGithubSession(ghurl).then((session) => { sess = session; return sess.repo.git.refs.heads(sess.branch).fetch(); }) - .then( (head) => { - sess.head = head; - sess.sha = head.object.sha; - return sess.repo.git.trees(sess.sha).fetch(); - }) - .then( (tree) => { - if (sess.subtreepath) { - for (let subtree of tree.tree) { - if (subtree.type == 'tree' && subtree.path == sess.subtreepath && subtree.sha) { - return sess.repo.git.trees(subtree.sha).fetch(); + .then((head) => { + sess.head = head; + sess.sha = head.object.sha; + return sess.repo.git.trees(sess.sha).fetch(); + }) + .then((tree) => { + if (sess.subtreepath) { + for (let subtree of tree.tree) { + if (subtree.type == 'tree' && subtree.path == sess.subtreepath && subtree.sha) { + return sess.repo.git.trees(subtree.sha).fetch(); + } } + throw Error("Cannot find subtree '" + sess.subtreepath + "' in tree " + tree.sha); } - throw Error("Cannot find subtree '" + sess.subtreepath + "' in tree " + tree.sha); - } - return tree; - }) - .then( (tree) => { - sess.tree = tree; - return sess; - }); + return tree; + }) + .then((tree) => { + sess.tree = tree; + return sess; + }); } - - bind(sess:GHSession, dobind:boolean) { + + bind(sess: GHSession, dobind: boolean) { var key = '__repo__' + sess.repopath; if (dobind) { - var repodata : GHRepoMetadata = { - url:sess.url, - branch:sess.branch, - platform_id:sess.platform_id, - mainPath:sess.mainPath, - sha:sess.sha}; + var repodata: GHRepoMetadata = { + url: sess.url, + branch: sess.branch, + platform_id: sess.platform_id, + mainPath: sess.mainPath, + sha: sess.sha + }; console.log('storing', repodata); localStorage.setItem(key, JSON.stringify(repodata)); } else { localStorage.removeItem(key); } } - - import(ghurl:string) : Promise { - var sess : GHSession; - return this.getGithubSession(ghurl).then( (session) => { + + import(ghurl: string): Promise { + var sess: GHSession; + return this.getGithubSession(ghurl).then((session) => { sess = session; // load README return sess.repo.contents('README.md').read(); }) - .catch( (e) => { - console.log(e); - console.log('no README.md found') - // make user repo exists - return sess.repo.fetch().then( (_repo) => { - return ''; // empty README + .catch((e) => { + console.log(e); + console.log('no README.md found') + // make user repo exists + return sess.repo.fetch().then((_repo) => { + return ''; // empty README + }) }) - }) - .then( (readme) => { - var m; - // check README for main file - const re8main = /8bitworkshop.com[^)]+file=([^)&]+)/; - m = re8main.exec(readme); - if (m && m[1]) { - console.log("main path: '" + m[1] + "'"); - sess.mainPath = m[1]; - } - // check README for proper platform - // unless we use githubURL= - // TODO: cannot handle multiple URLs in README - const re8plat = /8bitworkshop.com[^)]+platform=([A-Za-z0-9._\-]+)/; - m = re8plat.exec(readme); - if (m) { - console.log("platform id: '" + m[1] + "'"); - if (sess.platform_id && !sess.platform_id.startsWith(m[1])) - throw Error("Platform mismatch: Repository is " + m[1] + ", you have " + sess.platform_id + " selected."); - sess.platform_id = m[1]; - } - // bind to repository - this.bind(sess, true); - // get head commit - return sess; - }); + .then((readme) => { + var m; + // check README for main file + const re8main = /8bitworkshop.com[^)]+file=([^)&]+)/; + m = re8main.exec(readme); + if (m && m[1]) { + console.log("main path: '" + m[1] + "'"); + sess.mainPath = m[1]; + } + // check README for proper platform + // unless we use githubURL= + // TODO: cannot handle multiple URLs in README + const re8plat = /8bitworkshop.com[^)]+platform=([A-Za-z0-9._\-]+)/; + m = re8plat.exec(readme); + if (m) { + console.log("platform id: '" + m[1] + "'"); + if (sess.platform_id && !sess.platform_id.startsWith(m[1])) + throw Error("Platform mismatch: Repository is " + m[1] + ", you have " + sess.platform_id + " selected."); + sess.platform_id = m[1]; + } + // bind to repository + this.bind(sess, true); + // get head commit + return sess; + }); } - - pull(ghurl:string, deststore?) : Promise { - var sess : GHSession; - return this.getGithubHEADTree(ghurl).then( (session) => { + + pull(ghurl: string, deststore?): Promise { + var sess: GHSession; + return this.getGithubHEADTree(ghurl).then((session) => { sess = session; let blobreads = []; sess.paths = []; - sess.tree.tree.forEach( (item) => { + sess.tree.tree.forEach((item) => { console.log(item.path, item.type, item.size); sess.paths.push(item.path); if (item.type == 'blob' && !this.isFileIgnored(item.path)) { - var read = sess.repo.git.blobs(item.sha).fetch().then( (blob) => { + var read = sess.repo.git.blobs(item.sha).fetch().then((blob) => { var path = sess.prefix + item.path; var size = item.size; var encoding = blob.encoding; @@ -267,18 +268,18 @@ export class GithubService { }); return Promise.all(blobreads); }) - .then( (blobs) => { - return sess; - }); + .then((blobs) => { + return sess; + }); } - - importAndPull(ghurl:string) { + + importAndPull(ghurl: string) { return this.import(ghurl).then((sess) => { return this.pull(ghurl); }); } - publish(reponame:string, desc:string, license:string, isprivate:boolean) : Promise { + publish(reponame: string, desc: string, license: string, isprivate: boolean): Promise { var repo; var platform_id = this.project.platform_id; var mainPath = this.project.stripLocalPath(this.project.mainPath); @@ -289,33 +290,33 @@ export class GithubService { auto_init: false, license_template: license }) - .then( (_repo) => { - repo = _repo; - // create README.md - var s = README_md_template; - s = s.replace(/\$NAME/g, encodeURIComponent(reponame)); - s = s.replace(/\$PLATFORM/g, encodeURIComponent(platform_id)); - s = s.replace(/\$GITHUBURL/g, encodeURIComponent(repo.htmlUrl)); - s = s.replace(/\$MAINFILE/g, encodeURIComponent(mainPath)); - var config = { - message: '8bitworkshop: updated metadata in README.md', - content: btoa(s) - } - return repo.contents('README.md').add(config); - }).then( () => { - return this.getGithubSession(repo.htmlUrl); - }); + .then((_repo) => { + repo = _repo; + // create README.md + var s = README_md_template; + s = s.replace(/\$NAME/g, encodeURIComponent(reponame)); + s = s.replace(/\$PLATFORM/g, encodeURIComponent(platform_id)); + s = s.replace(/\$GITHUBURL/g, encodeURIComponent(repo.htmlUrl)); + s = s.replace(/\$MAINFILE/g, encodeURIComponent(mainPath)); + var config = { + message: '8bitworkshop: updated metadata in README.md', + content: btoa(s) + } + return repo.contents('README.md').add(config); + }).then(() => { + return this.getGithubSession(repo.htmlUrl); + }); } - - commit( ghurl:string, message:string, files:{path:string,data:FileData}[] ) : Promise { - var sess : GHSession; + + commit(ghurl: string, message: string, files: { path: string, data: FileData }[]): Promise { + var sess: GHSession; if (!message) { message = "updated from 8bitworkshop.com"; } - return this.getGithubHEADTree(ghurl).then( (session) => { + return this.getGithubHEADTree(ghurl).then((session) => { sess = session; if (sess.subtreepath) { throw Error("Sorry, right now you can only commit files to the root directory of a repository."); } - return Promise.all(files.map( (file) => { + return Promise.all(files.map((file) => { if (typeof file.data === 'string') { return sess.repo.git.blobs.create({ content: file.data, @@ -328,9 +329,9 @@ export class GithubService { }); } })); - }).then( (blobs) => { + }).then((blobs) => { return sess.repo.git.trees.create({ - tree: files.map( (file, index) => { + tree: files.map((file, index) => { return { path: file.path, mode: '100644', @@ -340,7 +341,7 @@ export class GithubService { }), base_tree: sess.tree.sha }); - }).then( (newtree) => { + }).then((newtree) => { return sess.repo.git.commits.create({ message: message, tree: newtree.sha, @@ -348,24 +349,24 @@ export class GithubService { sess.head.object.sha ] }); - }).then( (commit1) => { + }).then((commit1) => { return sess.repo.commits(commit1.sha).fetch(); - }).then( (commit) => { + }).then((commit) => { sess.commit = commit; return sess; }); } - push(sess:GHSession) : Promise { + push(sess: GHSession): Promise { return sess.head.update({ sha: sess.commit.sha - }).then( (update) => { + }).then((update) => { return sess; }); } - - deleteRepository(ghurl:string) { - return this.getGithubSession(ghurl).then( (session) => { + + deleteRepository(ghurl: string) { + return this.getGithubSession(ghurl).then((session) => { return session.repo.remove(); }); } @@ -380,8 +381,8 @@ export class FirebaseProjectFilesystem implements ProjectFilesystem { var database = firebase.database(); this.ref = database.ref('users/' + user_id + "/" + store_id); } - getChildForPath(path:string) { - var encodedPath = encodeURIComponent(path).replace('-','%2D').replace('.','%2E'); + getChildForPath(path: string) { + var encodedPath = encodeURIComponent(path).replace('-', '%2D').replace('.', '%2E'); return this.ref.child(encodedPath); } async getFileData(path: string): Promise { @@ -394,4 +395,7 @@ export class FirebaseProjectFilesystem implements ProjectFilesystem { filedata: data }); } + onFileSystemUpdate(callback: (path: string) => void): void { + // not implemented + } } diff --git a/src/ide/ui.ts b/src/ide/ui.ts index 3e016724..9ff0cd4d 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -262,6 +262,24 @@ async function initProject() { current_project.callbackBuildStatus = (busy: boolean) => { setBusyStatus(busy); }; + // Update file views when file contents change. + current_project.onFileChanged = (path: string, data: FileData) => { + var wnd = projectWindows.id2window[path]; + if (wnd) { + if (wnd instanceof SourceEditor && typeof data === 'string') { + wnd.setText(data); + } else if (wnd instanceof BinaryFileView && data instanceof Uint8Array) { + wnd.setData(data); + } else { + console.warn('onFileChanged: unknown view or data type'); + } + } + // Also refresh the asset editor if it's the active view. + var assetWnd = projectWindows.id2window['#asseteditor']; + if (assetWnd && assetWnd === projectWindows.getActive()) { + assetWnd.refresh(true); + } + }; } function setBusyStatus(busy: boolean) { @@ -679,19 +697,46 @@ async function getLocalFilesystem(repoid: string): Promise { alertError(`Could not get permission to access filesystem.`); return; } + const lastWriteTime: { [path: string]: number } = {}; 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 (isProbablyBinary(path) ? file.binary() : file.text()); - console.log(fileHandle, file, contents); + let contents = await (isProbablyBinary(path) ? file.arrayBuffer() : file.text()); + if (contents instanceof ArrayBuffer) { + return new Uint8Array(contents); + } return contents; }, setFileData: async (path, data) => { - //let vh = await dirHandle.getFileHandle(path, { create: true }); + lastWriteTime[path] = Date.now(); + const fileHandle = await dirHandle.getFileHandle(path, { create: true }); + const writable = await fileHandle.createWritable(); + await writable.write(data); + await writable.close(); + }, + onFileSystemUpdate: (callback: (path: string) => void) => { + // Experimental API: + // https://developer.mozilla.org/docs/Web/API/FileSystemObserver + if (typeof (window as any).FileSystemObserver === 'undefined') { + return; + } + const observer = new (window as any).FileSystemObserver((records, observer) => { + for (const record of records) { + // TODO Handle different types of changes intelligently. + // https://developer.mozilla.org/docs/Web/API/FileSystemChangeRecord#type + if (record.changedHandle) { + const path = record.changedHandle.name; + // Ignore filesystem notifications that are likely from our own recent writes, + // so that onFileChanged in ui.ts doesn't call assets editor's refresh(true). + // TODO: Consider better options than a time based threshold. + if (Date.now() - (lastWriteTime[path] || 0) < 2000) continue; + callback(path); + } + } + }); + observer.observe(dirHandle, { recursive: true }); } } } diff --git a/src/ide/views/debugviews.ts b/src/ide/views/debugviews.ts index a344f4c7..d118700f 100644 --- a/src/ide/views/debugviews.ts +++ b/src/ide/views/debugviews.ts @@ -202,6 +202,7 @@ export class VRAMMemoryView extends MemoryView { export class BinaryFileView implements ProjectView { vlist: VirtualTextScroller; + parent: HTMLElement; maindiv: HTMLElement; path: string; data: Uint8Array; @@ -212,12 +213,23 @@ export class BinaryFileView implements ProjectView { this.data = data; } + private populateVlist() { + $(this.vlist.maindiv).empty(); + this.vlist.create(this.parent, ((this.data.length + 15) >> 4), this.getMemoryLineAt.bind(this)); + } + createDiv(parent: HTMLElement) { - this.vlist = new VirtualTextScroller(parent); - this.vlist.create(parent, ((this.data.length + 15) >> 4), this.getMemoryLineAt.bind(this)); + this.parent = parent; + this.vlist = new VirtualTextScroller(this.parent); + this.populateVlist(); return this.vlist.maindiv; } + setData(data: Uint8Array) { + this.data = data; + this.populateVlist(); + } + getMemoryLineAt(row: number): VirtualTextLine { var offset = row * 16; var n1 = 0;