Report compiler errors in the UI.

Also, indentation changes.
This commit is contained in:
Jonathan Protzenko 2015-04-24 12:48:08 +01:00
Родитель 6b16ee4924
Коммит 7a073d8a95
4 изменённых файлов: 638 добавлений и 630 удалений

Просмотреть файл

@ -2092,6 +2092,8 @@ module TDev
}, json => {
ModalDialog.info(lf("Compilation error"), lf("Unknown early compilation error"));
});
}, (error: any) => {
ModalDialog.info("Compilation error", error.message);
});
}

Просмотреть файл

@ -1,336 +1,340 @@
///<reference path='refs.ts'/>
module TDev {
export interface ExternalEditor {
// Both these two fields are for our UI
name: string;
description: string;
// Unique
id: string;
// The domain root for the external editor.
origin: string;
// The path from the domain root to the editor main document.
path: string;
export interface ExternalEditor {
// Both these two fields are for our UI
name: string;
description: string;
// Unique
id: string;
// The domain root for the external editor.
origin: string;
// The path from the domain root to the editor main document.
path: string;
}
var externalEditorsCache: ExternalEditor[] = null;
export function getExternalEditors(): ExternalEditor[] {
if (!externalEditorsCache) {
// Detect at run-time where we're running from!
var url = Ticker.mainJsName.replace(/main.js$/, "");
var match = url.match(/(https?:\/\/[^\/]+)(.*)/);
var origin = match[1];
var path = match[2];
externalEditorsCache = [ {
name: "C++ Editor",
description: "Directly write C++ code using Ace (OUTDATED)",
id: "ace",
origin: origin,
path: path+"ace/editor.html"
}, {
name: "Blockly editor",
description: "Great block-based environment!",
id: "blockly",
origin: origin,
path: path+"blockly/editor.html"
} ];
}
return externalEditorsCache;
}
// Assumes that [id] is a valid external editor id.
export function editorById(id: string): ExternalEditor {
var r = getExternalEditors().filter(x => x.id == id);
Util.assert(r.length == 1);
return r[0];
}
export module External {
export var TheChannel: Channel = null;
import J = AST.Json;
export function wrapCpp(cpp: string) {
return ("// version = 1\n#include \"prelude.h\"\n" + cpp);
}
var externalEditorsCache: ExternalEditor[] = null;
export function getExternalEditors(): ExternalEditor[] {
if (!externalEditorsCache) {
// Detect at run-time where we're running from!
var url = Ticker.mainJsName.replace(/main.js$/, "");
var match = url.match(/(https?:\/\/[^\/]+)(.*)/);
var origin = match[1];
var path = match[2];
externalEditorsCache = [ {
name: "C++ Editor",
description: "Directly write C++ code using Ace (OUTDATED)",
id: "ace",
origin: origin,
path: path+"ace/editor.html"
}, {
name: "Blockly editor",
description: "Great block-based environment!",
id: "blockly",
origin: origin,
path: path+"blockly/editor.html"
} ];
}
return externalEditorsCache;
export function makeOutMbedErrorMsg(json: any) {
var errorMsg = "unknown error";
// This JSON format is *very* unstructured...
if (json.mbedresponse) {
var messages = json.messages.filter(m =>
m.severity == "error" || m.type == "Error"
);
errorMsg = messages.map(m => m.message + "\n" + m.text).join("\n");
}
return errorMsg;
}
// Assumes that [id] is a valid external editor id.
export function editorById(id: string): ExternalEditor {
var r = getExternalEditors().filter(x => x.id == id);
Util.assert(r.length == 1);
return r[0];
// This function modifies its argument by adding an extra [J.JLibrary]
// to its [decls] field that references the Microbit library.
function addMicrobitLibrary(app: J.JApp) {
var lib = <AST.LibraryRef> AST.Parser.parseDecl(
'meta import microbit {'+
' pub "hrgbjn"'+
'}'
);
var jLib = <J.JLibrary> J.addIdsAndDumpNode(lib);
app.decls.push(jLib);
}
export module External {
export var TheChannel: Channel = null;
// Takes a [JApp] and runs its through various hoops to make sure
// everything is type-checked and resolved properly.
function roundtrip(a: J.JApp): Promise { // of J.JApp
addMicrobitLibrary(a);
var text = J.serialize(a);
return AST.loadScriptAsync((id: string) => {
if (id == "")
return Promise.as(text);
else
return World.getAnyScriptAsync(id);
}, "").then((resp: AST.LoadScriptResult) => {
// Otherwise, eventually, this will result in our script being
// saved in the TouchDevelop format...
var s = Script;
Script = null;
// The function writes its result in a global
return Promise.as(J.dump(s));
});
}
import J = AST.Json;
export class Channel {
constructor(
private editor: ExternalEditor,
private iframe: HTMLIFrameElement,
public guid: string) {
}
export function wrapCpp(cpp: string) {
return ("// version = 1\n#include \"prelude.h\"\n" + cpp);
public post(message: Message) {
// The notification that the script has been successfully saved
// to cloud may take a while to arrive; the user may have
// discarded the editor in the meanwhile.
if (!this.iframe || !this.iframe.contentWindow)
return;
this.iframe.contentWindow.postMessage(message, this.editor.origin);
}
public receive(event) {
console.log("[outer message]", event);
if (event.origin != this.editor.origin) {
console.error("[outer message] not from the right origin!", event.origin, this.editor.origin);
return;
}
export function makeOutMbedErrorMsg(json: any) {
var errorMsg = "unknown error";
// This JSON format is *very* unstructured...
if (json.mbedresponse) {
var messages = json.messages.filter(m =>
m.severity == "error" || m.type == "Error"
);
errorMsg = messages.map(m => m.message + "\n" + m.text).join("\n");
}
return errorMsg;
}
switch ((<Message> event.data).type) {
case MessageType.Save: {
var message = <Message_Save> event.data;
World.getInstalledHeaderAsync(this.guid).then((header: Cloud.Header) => {
var scriptText = message.script.scriptText;
var editorState = message.script.editorState;
header.scriptVersion.baseSnapshot = message.script.baseSnapshot;
// This function modifies its argument by adding an extra [J.JLibrary]
// to its [decls] field that references the Microbit library.
function addMicrobitLibrary(app: J.JApp) {
var lib = <AST.LibraryRef> AST.Parser.parseDecl(
'meta import microbit {'+
' pub "hrgbjn"'+
'}'
);
var jLib = <J.JLibrary> J.addIdsAndDumpNode(lib);
app.decls.push(jLib);
}
var metadata = message.script.metadata;
Object.keys(metadata).forEach(k => {
var v = metadata[k];
if (k == "name")
v = v || "unnamed";
header.meta[k] = v;
});
// [name] deserves a special treatment because it
// appears both on the header and in the metadata.
header.name = metadata.name;
// Takes a [JApp] and runs its through various hoops to make sure
// everything is type-checked and resolved properly.
function roundtrip(a: J.JApp): Promise { // of J.JApp
addMicrobitLibrary(a);
var text = J.serialize(a);
return AST.loadScriptAsync((id: string) => {
if (id == "")
return Promise.as(text);
else
return World.getAnyScriptAsync(id);
}, "").then((resp: AST.LoadScriptResult) => {
// Otherwise, eventually, this will result in our script being
// saved in the TouchDevelop format...
var s = Script;
Script = null;
// The function writes its result in a global
return Promise.as(J.dump(s));
});
}
export class Channel {
constructor(
private editor: ExternalEditor,
private iframe: HTMLIFrameElement,
public guid: string) {
}
public post(message: Message) {
// The notification that the script has been successfully saved
// to cloud may take a while to arrive; the user may have
// discarded the editor in the meanwhile.
if (!this.iframe || !this.iframe.contentWindow)
return;
this.iframe.contentWindow.postMessage(message, this.editor.origin);
}
public receive(event) {
console.log("[outer message]", event);
if (event.origin != this.editor.origin) {
console.error("[outer message] not from the right origin!", event.origin, this.editor.origin);
return;
}
switch ((<Message> event.data).type) {
case MessageType.Save: {
var message = <Message_Save> event.data;
World.getInstalledHeaderAsync(this.guid).then((header: Cloud.Header) => {
var scriptText = message.script.scriptText;
var editorState = message.script.editorState;
header.scriptVersion.baseSnapshot = message.script.baseSnapshot;
var metadata = message.script.metadata;
Object.keys(metadata).forEach(k => {
var v = metadata[k];
if (k == "name")
v = v || "unnamed";
header.meta[k] = v;
});
// [name] deserves a special treatment because it
// appears both on the header and in the metadata.
header.name = metadata.name;
// Writes into local storage.
World.updateInstalledScriptAsync(header, scriptText, editorState, false, "").then(() => {
console.log("[external] script saved properly");
this.post(<Message_SaveAck>{
type: MessageType.SaveAck,
where: SaveLocation.Local,
status: Status.Ok,
});
});
// Schedules a cloud sync; set the right state so
// that [scheduleSaveToCloudAsync] writes the
// baseSnapshot where we can read it back.
localStorage["editorScriptToSaveDirty"] = this.guid;
TheEditor.scheduleSaveToCloudAsync().then((response: Cloud.PostUserInstalledResponse) => {
// Reading the code of [scheduleSaveToCloudAsync], an early falsy return
// means that a sync is already scheduled.
if (!response)
return;
if (response.numErrors) {
this.post(<Message_SaveAck>{
type: MessageType.SaveAck,
where: SaveLocation.Cloud,
status: Status.Error,
error: (<any> response.headers[0]).error,
});
// Couldn't sync! Chances are high that we need to do a merge.
// Because [syncAsync] is not called on a regular basis when an
// external editor is open, we need to trigger the download of
// the newer version from the cloud *now*.
World.syncAsync().then(() => {
World.getInstalledScriptVersionInCloud(this.guid).then((json: string) => {
var m: PendingMerge = JSON.parse(json || "{}");
if ("theirs" in m) {
this.post(<Message_Merge>{
type: MessageType.Merge,
merge: m
});
} else {
console.log("[external] cloud error was not because of a due merge");
}
});
});
return;
}
var newCloudSnapshot = response.headers[0].scriptVersion.baseSnapshot;
console.log("[external] accepted, new cloud version ", newCloudSnapshot);
// Note: currently, [response.retry] is always false. The reason is,
// every call of us to [updateInstalledScriptAsync] is immediately
// followed by a call to [scheduleSaveToCloudAsync]. Furthermore,
// the latter function has its own tracking mechanism where updates
// are delayed, and it sort-of knows if it missed an update and
// should retry. In that case, it doesn't return until the second
// update has been processed, and we only get called after the cloud
// is, indeed, in sync. (If we were to offer external editors a way
// to decide whether to save to cloud or not, then this would no
// longer be true.)
this.post(<Message_SaveAck>{
type: MessageType.SaveAck,
where: SaveLocation.Cloud,
status: Status.Ok,
newBaseSnapshot: newCloudSnapshot,
cloudIsInSync: !response.retry,
});
});
});
break;
}
case MessageType.Quit:
TheEditor.goToHub("list:installed-scripts:script:"+this.guid+":overview");
TheChannel = null;
break;
case MessageType.Compile:
var message1 = <Message_Compile> event.data;
var cpp;
switch (message1.language) {
case Language.CPlusPlus:
cpp = Promise.as(message1.text);
break;
case Language.TouchDevelop:
// the guid is here only for testing; the real generation should be deterministic for best results
cpp = roundtrip(message1.text).then((a: J.JApp) => {
return Microbit.compile(a);
});
break;
}
cpp.then((cpp: string) => {
console.log(cpp);
Cloud.postUserInstalledCompileAsync(this.guid, wrapCpp(cpp)).then(json => {
// Success.
console.log(json);
if (json.success) {
this.post(<Message_CompileAck>{
type: MessageType.CompileAck,
status: Status.Ok
});
document.location.href = json.hexurl;
} else {
var errorMsg = makeOutMbedErrorMsg(json);
this.post(<Message_CompileAck>{
type: MessageType.CompileAck,
status: Status.Error,
error: errorMsg
});
}
}, (json: string) => {
// Failure
console.log(json);
this.post(<Message_CompileAck>{
type: MessageType.CompileAck,
status: Status.Error,
error: "early error"
});
});
});
break;
case MessageType.Upgrade:
var message2 = <Message_Upgrade> event.data;
var ast = message2.ast;
addMicrobitLibrary(ast);
console.log("Attempting to serialize", ast);
var text = J.serialize(ast);
console.log("Attempting to edit script text", text);
Browser.TheHost.openNewScriptAsync({
editorName: "touchdevelop",
scriptName: message2.name,
scriptText: text
});
break;
default:
console.error("[external] unexpected message type", message.type);
break;
}
}
}
export interface ScriptData {
guid: string;
scriptText: string;
editorState: string;
scriptVersionInCloud: string;
baseSnapshot: string;
metadata: Metadata;
};
// The [scriptVersionInCloud] name is the one that's used by [world.ts];
// actually, it hasn't much to do, really, with the script version
// that's in the cloud. It's more of an unused field (in the new "lite
// cloud" context) that we use to store extra information attached to
// the script.
export function loadAndSetup(editor: ExternalEditor, data: ScriptData) {
// The [scheduleSaveToCloudAsync] method on [Editor] needs the
// [guid] field of this global to match for us to read back the
// [baseSnapshot] field afterwards.
ScriptEditorWorldInfo = <EditorWorldInfo>{
guid: data.guid,
baseId: null,
baseUserId: null,
status: null,
version: null,
baseSnapshot: null,
};
// Clear leftover iframes.
var iframeDiv = document.getElementById("externalEditorFrame");
iframeDiv.setChildren([]);
// Load the editor; send the initial message.
var iframe = document.createElement("iframe");
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
iframe.addEventListener("load", function () {
TheChannel = new Channel(editor, iframe, data.guid);
var extra = JSON.parse(data.scriptVersionInCloud || "{}");
TheChannel.post(<Message_Init>{
type: MessageType.Init,
script: data,
merge: ("theirs" in extra) ? extra : null
// Writes into local storage.
World.updateInstalledScriptAsync(header, scriptText, editorState, false, "").then(() => {
console.log("[external] script saved properly");
this.post(<Message_SaveAck>{
type: MessageType.SaveAck,
where: SaveLocation.Local,
status: Status.Ok,
});
});
iframe.setAttribute("src", editor.origin + editor.path);
iframeDiv.appendChild(iframe);
});
// Change the hash and the window title.
TheEditor.historyMgr.setHash("edit:" + data.guid, editor.name);
// Schedules a cloud sync; set the right state so
// that [scheduleSaveToCloudAsync] writes the
// baseSnapshot where we can read it back.
localStorage["editorScriptToSaveDirty"] = this.guid;
TheEditor.scheduleSaveToCloudAsync().then((response: Cloud.PostUserInstalledResponse) => {
// Reading the code of [scheduleSaveToCloudAsync], an early falsy return
// means that a sync is already scheduled.
if (!response)
return;
if (response.numErrors) {
this.post(<Message_SaveAck>{
type: MessageType.SaveAck,
where: SaveLocation.Cloud,
status: Status.Error,
error: (<any> response.headers[0]).error,
});
// Couldn't sync! Chances are high that we need to do a merge.
// Because [syncAsync] is not called on a regular basis when an
// external editor is open, we need to trigger the download of
// the newer version from the cloud *now*.
World.syncAsync().then(() => {
World.getInstalledScriptVersionInCloud(this.guid).then((json: string) => {
var m: PendingMerge = JSON.parse(json || "{}");
if ("theirs" in m) {
this.post(<Message_Merge>{
type: MessageType.Merge,
merge: m
});
} else {
console.log("[external] cloud error was not because of a due merge");
}
});
});
return;
}
var newCloudSnapshot = response.headers[0].scriptVersion.baseSnapshot;
console.log("[external] accepted, new cloud version ", newCloudSnapshot);
// Note: currently, [response.retry] is always false. The reason is,
// every call of us to [updateInstalledScriptAsync] is immediately
// followed by a call to [scheduleSaveToCloudAsync]. Furthermore,
// the latter function has its own tracking mechanism where updates
// are delayed, and it sort-of knows if it missed an update and
// should retry. In that case, it doesn't return until the second
// update has been processed, and we only get called after the cloud
// is, indeed, in sync. (If we were to offer external editors a way
// to decide whether to save to cloud or not, then this would no
// longer be true.)
this.post(<Message_SaveAck>{
type: MessageType.SaveAck,
where: SaveLocation.Cloud,
status: Status.Ok,
newBaseSnapshot: newCloudSnapshot,
cloudIsInSync: !response.retry,
});
});
});
break;
}
case MessageType.Quit:
TheEditor.goToHub("list:installed-scripts:script:"+this.guid+":overview");
TheChannel = null;
break;
case MessageType.Compile:
var message1 = <Message_Compile> event.data;
var cpp;
switch (message1.language) {
case Language.CPlusPlus:
cpp = Promise.as(message1.text);
break;
case Language.TouchDevelop:
// the guid is here only for testing; the real generation should be deterministic for best results
cpp = roundtrip(message1.text).then((a: J.JApp) => {
return Microbit.compile(a);
});
break;
}
cpp.then((cpp: string) => {
console.log(cpp);
Cloud.postUserInstalledCompileAsync(this.guid, wrapCpp(cpp)).then(json => {
// Success.
console.log(json);
if (json.success) {
this.post(<Message_CompileAck>{
type: MessageType.CompileAck,
status: Status.Ok
});
document.location.href = json.hexurl;
} else {
var errorMsg = makeOutMbedErrorMsg(json);
this.post(<Message_CompileAck>{
type: MessageType.CompileAck,
status: Status.Error,
error: errorMsg
});
}
}, (json: string) => {
// Failure
console.log(json);
this.post(<Message_CompileAck>{
type: MessageType.CompileAck,
status: Status.Error,
error: "early error"
});
});
}, (error: any) => {
ModalDialog.info("Compilation error", error.message);
});
break;
case MessageType.Upgrade:
var message2 = <Message_Upgrade> event.data;
var ast = message2.ast;
addMicrobitLibrary(ast);
console.log("Attempting to serialize", ast);
var text = J.serialize(ast);
console.log("Attempting to edit script text", text);
Browser.TheHost.openNewScriptAsync({
editorName: "touchdevelop",
scriptName: message2.name,
scriptText: text
});
break;
default:
console.error("[external] unexpected message type", message.type);
break;
}
}
}
export interface ScriptData {
guid: string;
scriptText: string;
editorState: string;
scriptVersionInCloud: string;
baseSnapshot: string;
metadata: Metadata;
};
// The [scriptVersionInCloud] name is the one that's used by [world.ts];
// actually, it hasn't much to do, really, with the script version
// that's in the cloud. It's more of an unused field (in the new "lite
// cloud" context) that we use to store extra information attached to
// the script.
export function loadAndSetup(editor: ExternalEditor, data: ScriptData) {
// The [scheduleSaveToCloudAsync] method on [Editor] needs the
// [guid] field of this global to match for us to read back the
// [baseSnapshot] field afterwards.
ScriptEditorWorldInfo = <EditorWorldInfo>{
guid: data.guid,
baseId: null,
baseUserId: null,
status: null,
version: null,
baseSnapshot: null,
};
// Clear leftover iframes.
var iframeDiv = document.getElementById("externalEditorFrame");
iframeDiv.setChildren([]);
// Load the editor; send the initial message.
var iframe = document.createElement("iframe");
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
iframe.addEventListener("load", function () {
TheChannel = new Channel(editor, iframe, data.guid);
var extra = JSON.parse(data.scriptVersionInCloud || "{}");
TheChannel.post(<Message_Init>{
type: MessageType.Init,
script: data,
merge: ("theirs" in extra) ? extra : null
});
});
iframe.setAttribute("src", editor.origin + editor.path);
iframeDiv.appendChild(iframe);
// Change the hash and the window title.
TheEditor.historyMgr.setHash("edit:" + data.guid, editor.name);
}
}
}
// vim: set ts=2 sw=2 sts=2:

Просмотреть файл

@ -37,6 +37,7 @@ module TDev {
return e;
} catch (e) {
console.error("Compilation error", e);
throw e;
}
}

Просмотреть файл

@ -4,335 +4,336 @@
module TDev {
// ---------- Communication protocol
// ---------- Communication protocol
var allowedOrigins: { [index: string]: any } = {
"http://localhost:4242": null,
"https://www.touchdevelop.com": null,
"https://mbitmain.azurewebsites.net": null
var allowedOrigins: { [index: string]: any } = {
"http://localhost:4242": null,
"https://www.touchdevelop.com": null,
"https://mbitmain.azurewebsites.net": null
};
var $ = (s: string) => document.querySelector(s);
// Both of these are written once when we receive the first (trusted)
// message.
var outer: Window = null;
var origin: string = null;
// A global that remembers the current version we're editing
var currentVersion: string;
var inMerge: boolean = false;
window.addEventListener("message", (event) => {
if (!(event.origin in allowedOrigins)) {
console.error("[inner message] not from the right origin!", event.origin);
return;
}
if (!outer || !origin) {
outer = event.source;
origin = event.origin;
}
receive(<External.Message>event.data);
});
function receive(message: External.Message) {
console.log("[inner message]", message);
switch (message.type) {
case External.MessageType.Init:
setupEditor(<External.Message_Init> message);
setupButtons();
setupCurrentVersion(<External.Message_Init> message);
break;
case External.MessageType.SaveAck:
saveAck(<External.Message_SaveAck> message);
break;
case External.MessageType.Merge:
promptMerge((<External.Message_Merge> message).merge);
break;
case External.MessageType.CompileAck:
compileAck(<External.Message_CompileAck> message);
}
}
function post(message: External.Message) {
if (!outer)
console.error("Invalid state");
outer.postMessage(message, origin);
}
// ---------- Revisions
function prefix(where: External.SaveLocation) {
switch (where) {
case External.SaveLocation.Cloud:
return("☁ [cloud]");
case External.SaveLocation.Local:
return("⌂ [local]");
}
}
function saveAck(message: External.Message_SaveAck) {
switch (message.status) {
case External.Status.Error:
statusMsg(prefix(message.where)+" error: "+message.error, message.status);
break;
case External.Status.Ok:
if (message.where == External.SaveLocation.Cloud) {
statusMsg(prefix(message.where)+" successfully saved version (cloud in sync? "+
message.cloudIsInSync +", "+
"from "+currentVersion+" to "+message.newBaseSnapshot+")",
message.status);
currentVersion = message.newBaseSnapshot;
} else {
statusMsg(prefix(message.where)+" successfully saved", message.status);
}
break;
}
}
function compileAck(message: External.Message_CompileAck) {
switch (message.status) {
case External.Status.Error:
statusMsg("compilation error: "+message.error, message.status);
break;
case External.Status.Ok:
statusMsg("compilation successful", message.status);
break;
}
}
function promptMerge(merge: External.PendingMerge) {
console.log("[merge] merge request, base = "+merge.base.baseSnapshot +
", theirs = "+merge.theirs.baseSnapshot +
", mine = "+currentVersion);
var mkButton = function (symbol: string, label: string, f: () => void) {
var b = document.createElement("a");
b.classList.add("roundbutton");
b.setAttribute("href", "#");
var s = document.createElement("div");
s.classList.add("roundsymbol");
s.textContent = symbol;
b.appendChild(s);
var l = document.createElement("div");
l.classList.add("roundlabel");
l.textContent = label;
b.appendChild(l);
b.addEventListener("click", f);
return b;
};
var box = $("#merge-commands");
var clearMerge = () => {
while (box.firstChild)
box.removeChild(box.firstChild);
};
var mineText = saveBlockly();
var mineName = getName();
var mineDescription = getDescription();
var mineButton = mkButton("🔍", "see mine", () => {
loadBlockly(mineText);
setName(mineName);
setDescription(mineDescription);
});
var theirsButton = mkButton("🔍", "see theirs", () => {
loadBlockly(merge.theirs.scriptText);
setName(merge.theirs.metadata.name);
setDescription(merge.theirs.metadata.description);
});
var baseButton = mkButton("🔍", "see base", () => {
loadBlockly(merge.base.scriptText);
setName(merge.base.metadata.name);
setDescription(merge.base.metadata.description);
});
var mergeButton = mkButton("👍", "finish merge", () => {
inMerge = false;
currentVersion = merge.theirs.baseSnapshot;
clearMerge();
doSave();
});
clearMerge();
inMerge = true;
[ mineButton, theirsButton, baseButton, mergeButton ].forEach(button => {
box.appendChild(button);
box.appendChild(document.createTextNode(" "));
});
}
var $ = (s: string) => document.querySelector(s);
function setupCurrentVersion(message: External.Message_Init) {
currentVersion = message.script.baseSnapshot;
console.log("[revisions] current version is "+currentVersion);
// Both of these are written once when we receive the first (trusted)
// message.
var outer: Window = null;
var origin: string = null;
if (message.merge)
promptMerge(message.merge);
}
// A global that remembers the current version we're editing
var currentVersion: string;
var inMerge: boolean = false;
// ---------- UI functions
window.addEventListener("message", (event) => {
if (!(event.origin in allowedOrigins)) {
console.error("[inner message] not from the right origin!", event.origin);
return;
}
interface EditorState {
lastSave: Date;
}
if (!outer || !origin) {
outer = event.source;
origin = event.origin;
}
function statusMsg(s: string, st: External.Status) {
var box = <HTMLElement> $("#log");
var elt = document.createElement("div");
elt.classList.add("status");
if (st == External.Status.Error)
elt.classList.add("error");
else
elt.classList.remove("error");
elt.textContent = s;
box.appendChild(elt);
box.scrollTop = box.scrollHeight;
}
receive(<External.Message>event.data);
function loadEditorState(s: string): EditorState {
return JSON.parse(s || "{ \"lastSave\": null }");
}
function saveEditorState(s: EditorState): string {
return JSON.stringify(s);
}
function loadBlockly(s: string) {
var text = s || "<xml></xml>";
var xml = Blockly.Xml.textToDom(text);
Blockly.mainWorkspace.clear();
try {
Blockly.Xml.domToWorkspace(Blockly.mainWorkspace, xml);
} catch (e) {
console.error("Cannot load saved Blockly script. Too recent?");
console.error(e);
}
}
function saveBlockly(): string {
var xml = Blockly.Xml.workspaceToDom(Blockly.mainWorkspace);
var text = Blockly.Xml.domToPrettyText(xml);
return text;
}
function setDescription(x: string) {
(<HTMLInputElement> $("#script-description")).value = (x || "");
}
function setName(x: string) {
(<HTMLInputElement> $("#script-name")).value = x;
}
function getDescription() {
return (<HTMLInputElement> $("#script-description")).value;
}
function getName() {
return (<HTMLInputElement> $("#script-name")).value;
}
var dirty = false;
// Called once at startup
function setupEditor(message: External.Message_Init) {
var state = loadEditorState(message.script.editorState);
Blockly.inject($("#editor"), {
toolbox: $("#blockly-toolbox"),
scrollbars: false
});
loadBlockly(message.script.scriptText);
// Hack alert! Blockly's [fireUiEvent] function [setTimeout]'s (with a 0 delay) the actual
// firing of the event, meaning that the call to [inject] above schedule a change event to
// be fired immediately after the current function is done. To make sure our change handler
// does not receive that initial event, we schedule it for slightly later.
window.setTimeout(() => {
Blockly.addChangeListener(() => {
statusMsg("✎ local changes", External.Status.Ok);
dirty = true;
});
}, 1);
$("#script-name").addEventListener("input", () => {
statusMsg("✎ local changes", External.Status.Ok);
dirty = true;
});
$("#script-description").addEventListener("input", () => {
statusMsg("✎ local changes", External.Status.Ok);
dirty = true;
});
function receive(message: External.Message) {
console.log("[inner message]", message);
setName(message.script.metadata.name);
setDescription(message.script.metadata.description);
switch (message.type) {
case External.MessageType.Init:
setupEditor(<External.Message_Init> message);
setupButtons();
setupCurrentVersion(<External.Message_Init> message);
break;
// That's triggered when the user closes or reloads the whole page, but
// doesn't help if the user hits the "back" button in our UI.
window.addEventListener("beforeunload", function (e) {
if (dirty) {
var confirmationMessage = "Some of your changes have not been saved. Quit anyway?";
(e || window.event).returnValue = confirmationMessage;
return confirmationMessage;
}
});
case External.MessageType.SaveAck:
saveAck(<External.Message_SaveAck> message);
break;
window.setInterval(() => {
doSave();
}, 5000);
case External.MessageType.Merge:
promptMerge((<External.Message_Merge> message).merge);
break;
console.log("[loaded] cloud version " + message.script.baseSnapshot +
"(dated from: "+state.lastSave+")");
}
case External.MessageType.CompileAck:
compileAck(<External.Message_CompileAck> message);
function doSave(force = false) {
if (!dirty && !force)
return;
var text = saveBlockly();
console.log("[saving] on top of: ", currentVersion);
post(<External.Message_Save>{
type: External.MessageType.Save,
script: {
scriptText: text,
editorState: saveEditorState({
lastSave: new Date()
}),
baseSnapshot: currentVersion,
metadata: {
name: getName(),
description: getDescription()
}
}
},
});
dirty = false;
}
function post(message: External.Message) {
if (!outer)
console.error("Invalid state");
outer.postMessage(message, origin);
}
// ---------- Revisions
function prefix(where: External.SaveLocation) {
switch (where) {
case External.SaveLocation.Cloud:
return("☁ [cloud]");
case External.SaveLocation.Local:
return("⌂ [local]");
}
}
function saveAck(message: External.Message_SaveAck) {
switch (message.status) {
case External.Status.Error:
statusMsg(prefix(message.where)+" error: "+message.error, message.status);
break;
case External.Status.Ok:
if (message.where == External.SaveLocation.Cloud) {
statusMsg(prefix(message.where)+" successfully saved version (cloud in sync? "+
message.cloudIsInSync +", "+
"from "+currentVersion+" to "+message.newBaseSnapshot+")",
message.status);
currentVersion = message.newBaseSnapshot;
} else {
statusMsg(prefix(message.where)+" successfully saved", message.status);
}
break;
}
}
function compileAck(message: External.Message_CompileAck) {
switch (message.status) {
case External.Status.Error:
statusMsg("compilation error: "+message.error, message.status);
break;
case External.Status.Ok:
statusMsg("compilation successful", message.status);
break;
}
}
function promptMerge(merge: External.PendingMerge) {
console.log("[merge] merge request, base = "+merge.base.baseSnapshot +
", theirs = "+merge.theirs.baseSnapshot +
", mine = "+currentVersion);
var mkButton = function (symbol: string, label: string, f: () => void) {
var b = document.createElement("a");
b.classList.add("roundbutton");
b.setAttribute("href", "#");
var s = document.createElement("div");
s.classList.add("roundsymbol");
s.textContent = symbol;
b.appendChild(s);
var l = document.createElement("div");
l.classList.add("roundlabel");
l.textContent = label;
b.appendChild(l);
b.addEventListener("click", f);
return b;
};
var box = $("#merge-commands");
var clearMerge = () => {
while (box.firstChild)
box.removeChild(box.firstChild);
};
var mineText = saveBlockly();
var mineName = getName();
var mineDescription = getDescription();
var mineButton = mkButton("🔍", "see mine", () => {
loadBlockly(mineText);
setName(mineName);
setDescription(mineDescription);
});
var theirsButton = mkButton("🔍", "see theirs", () => {
loadBlockly(merge.theirs.scriptText);
setName(merge.theirs.metadata.name);
setDescription(merge.theirs.metadata.description);
});
var baseButton = mkButton("🔍", "see base", () => {
loadBlockly(merge.base.scriptText);
setName(merge.base.metadata.name);
setDescription(merge.base.metadata.description);
});
var mergeButton = mkButton("👍", "finish merge", () => {
inMerge = false;
currentVersion = merge.theirs.baseSnapshot;
clearMerge();
doSave();
});
clearMerge();
inMerge = true;
[ mineButton, theirsButton, baseButton, mergeButton ].forEach(button => {
box.appendChild(button);
box.appendChild(document.createTextNode(" "));
});
}
function setupCurrentVersion(message: External.Message_Init) {
currentVersion = message.script.baseSnapshot;
console.log("[revisions] current version is "+currentVersion);
if (message.merge)
promptMerge(message.merge);
}
// ---------- UI functions
interface EditorState {
lastSave: Date;
}
function statusMsg(s: string, st: External.Status) {
var box = <HTMLElement> $("#log");
var elt = document.createElement("div");
elt.classList.add("status");
if (st == External.Status.Error)
elt.classList.add("error");
else
elt.classList.remove("error");
elt.textContent = s;
box.appendChild(elt);
box.scrollTop = box.scrollHeight;
}
function loadEditorState(s: string): EditorState {
return JSON.parse(s || "{ \"lastSave\": null }");
}
function saveEditorState(s: EditorState): string {
return JSON.stringify(s);
}
function loadBlockly(s: string) {
var text = s || "<xml></xml>";
var xml = Blockly.Xml.textToDom(text);
Blockly.mainWorkspace.clear();
try {
Blockly.Xml.domToWorkspace(Blockly.mainWorkspace, xml);
} catch (e) {
console.error("Cannot load saved Blockly script. Too recent?");
console.error(e);
}
}
function saveBlockly(): string {
var xml = Blockly.Xml.workspaceToDom(Blockly.mainWorkspace);
var text = Blockly.Xml.domToPrettyText(xml);
return text;
}
function setDescription(x: string) {
(<HTMLInputElement> $("#script-description")).value = (x || "");
}
function setName(x: string) {
(<HTMLInputElement> $("#script-name")).value = x;
}
function getDescription() {
return (<HTMLInputElement> $("#script-description")).value;
}
function getName() {
return (<HTMLInputElement> $("#script-name")).value;
}
var dirty = false;
// Called once at startup
function setupEditor(message: External.Message_Init) {
var state = loadEditorState(message.script.editorState);
Blockly.inject($("#editor"), {
toolbox: $("#blockly-toolbox"),
scrollbars: false
});
loadBlockly(message.script.scriptText);
// Hack alert! Blockly's [fireUiEvent] function [setTimeout]'s (with a 0 delay) the actual
// firing of the event, meaning that the call to [inject] above schedule a change event to
// be fired immediately after the current function is done. To make sure our change handler
// does not receive that initial event, we schedule it for slightly later.
window.setTimeout(() => {
Blockly.addChangeListener(() => {
statusMsg("✎ local changes", External.Status.Ok);
dirty = true;
});
}, 1);
$("#script-name").addEventListener("input", () => {
statusMsg("✎ local changes", External.Status.Ok);
dirty = true;
});
$("#script-description").addEventListener("input", () => {
statusMsg("✎ local changes", External.Status.Ok);
dirty = true;
});
setName(message.script.metadata.name);
setDescription(message.script.metadata.description);
// That's triggered when the user closes or reloads the whole page, but
// doesn't help if the user hits the "back" button in our UI.
window.addEventListener("beforeunload", function (e) {
if (dirty) {
var confirmationMessage = "Some of your changes have not been saved. Quit anyway?";
(e || window.event).returnValue = confirmationMessage;
return confirmationMessage;
}
});
window.setInterval(() => {
doSave();
}, 5000);
console.log("[loaded] cloud version " + message.script.baseSnapshot +
"(dated from: "+state.lastSave+")");
}
function doSave(force = false) {
if (!dirty && !force)
return;
var text = saveBlockly();
console.log("[saving] on top of: ", currentVersion);
post(<External.Message_Save>{
type: External.MessageType.Save,
script: {
scriptText: text,
editorState: saveEditorState({
lastSave: new Date()
}),
baseSnapshot: currentVersion,
metadata: {
name: getName(),
description: getDescription()
}
},
});
dirty = false;
}
function setupButtons() {
$("#command-quit").addEventListener("click", () => {
doSave();
post({ type: External.MessageType.Quit });
});
$("#command-compile").addEventListener("click", () => {
post(<External.Message_Compile> {
type: External.MessageType.Compile,
text: compile(Blockly.mainWorkspace, {
name: getName(),
description: getDescription()
}),
language: External.Language.TouchDevelop
});
});
$("#command-graduate").addEventListener("click", () => {
post(<External.Message_Upgrade> {
type: External.MessageType.Upgrade,
ast: compile(Blockly.mainWorkspace, {
name: getName(),
description: getDescription()
}),
name: getName()+" (converted)",
});
});
$("#command-run").addEventListener("click", () => {
});
}
function setupButtons() {
$("#command-quit").addEventListener("click", () => {
doSave();
post({ type: External.MessageType.Quit });
});
$("#command-compile").addEventListener("click", () => {
post(<External.Message_Compile> {
type: External.MessageType.Compile,
text: compile(Blockly.mainWorkspace, {
name: getName(),
description: getDescription()
}),
language: External.Language.TouchDevelop
});
});
$("#command-graduate").addEventListener("click", () => {
post(<External.Message_Upgrade> {
type: External.MessageType.Upgrade,
ast: compile(Blockly.mainWorkspace, {
name: getName(),
description: getDescription()
}),
name: getName()+" (converted)",
});
});
$("#command-run").addEventListener("click", () => {
});
}
}
// vim: set ts=2 sw=2 sts=2: