C++ compiler.
Report compiler errors in the UI. Also, indentation changes.
This commit is contained in:
Родитель
6b16ee4924
Коммит
7a073d8a95
|
@ -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:
|
||||
|
|
Загрузка…
Ссылка в новой задаче