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 => {
|
}, json => {
|
||||||
ModalDialog.info(lf("Compilation error"), lf("Unknown early compilation error"));
|
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'/>
|
///<reference path='refs.ts'/>
|
||||||
|
|
||||||
module TDev {
|
module TDev {
|
||||||
export interface ExternalEditor {
|
export interface ExternalEditor {
|
||||||
// Both these two fields are for our UI
|
// Both these two fields are for our UI
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
// Unique
|
// Unique
|
||||||
id: string;
|
id: string;
|
||||||
// The domain root for the external editor.
|
// The domain root for the external editor.
|
||||||
origin: string;
|
origin: string;
|
||||||
// The path from the domain root to the editor main document.
|
// The path from the domain root to the editor main document.
|
||||||
path: string;
|
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 makeOutMbedErrorMsg(json: any) {
|
||||||
|
var errorMsg = "unknown error";
|
||||||
export function getExternalEditors(): ExternalEditor[] {
|
// This JSON format is *very* unstructured...
|
||||||
if (!externalEditorsCache) {
|
if (json.mbedresponse) {
|
||||||
// Detect at run-time where we're running from!
|
var messages = json.messages.filter(m =>
|
||||||
var url = Ticker.mainJsName.replace(/main.js$/, "");
|
m.severity == "error" || m.type == "Error"
|
||||||
var match = url.match(/(https?:\/\/[^\/]+)(.*)/);
|
);
|
||||||
var origin = match[1];
|
errorMsg = messages.map(m => m.message + "\n" + m.text).join("\n");
|
||||||
var path = match[2];
|
}
|
||||||
externalEditorsCache = [ {
|
return errorMsg;
|
||||||
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.
|
// This function modifies its argument by adding an extra [J.JLibrary]
|
||||||
export function editorById(id: string): ExternalEditor {
|
// to its [decls] field that references the Microbit library.
|
||||||
var r = getExternalEditors().filter(x => x.id == id);
|
function addMicrobitLibrary(app: J.JApp) {
|
||||||
Util.assert(r.length == 1);
|
var lib = <AST.LibraryRef> AST.Parser.parseDecl(
|
||||||
return r[0];
|
'meta import microbit {'+
|
||||||
|
' pub "hrgbjn"'+
|
||||||
|
'}'
|
||||||
|
);
|
||||||
|
var jLib = <J.JLibrary> J.addIdsAndDumpNode(lib);
|
||||||
|
app.decls.push(jLib);
|
||||||
}
|
}
|
||||||
|
|
||||||
export module External {
|
// Takes a [JApp] and runs its through various hoops to make sure
|
||||||
export var TheChannel: Channel = null;
|
// 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) {
|
public post(message: Message) {
|
||||||
return ("// version = 1\n#include \"prelude.h\"\n" + cpp);
|
// 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) {
|
switch ((<Message> event.data).type) {
|
||||||
var errorMsg = "unknown error";
|
case MessageType.Save: {
|
||||||
// This JSON format is *very* unstructured...
|
var message = <Message_Save> event.data;
|
||||||
if (json.mbedresponse) {
|
World.getInstalledHeaderAsync(this.guid).then((header: Cloud.Header) => {
|
||||||
var messages = json.messages.filter(m =>
|
var scriptText = message.script.scriptText;
|
||||||
m.severity == "error" || m.type == "Error"
|
var editorState = message.script.editorState;
|
||||||
);
|
header.scriptVersion.baseSnapshot = message.script.baseSnapshot;
|
||||||
errorMsg = messages.map(m => m.message + "\n" + m.text).join("\n");
|
|
||||||
}
|
|
||||||
return errorMsg;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function modifies its argument by adding an extra [J.JLibrary]
|
var metadata = message.script.metadata;
|
||||||
// to its [decls] field that references the Microbit library.
|
Object.keys(metadata).forEach(k => {
|
||||||
function addMicrobitLibrary(app: J.JApp) {
|
var v = metadata[k];
|
||||||
var lib = <AST.LibraryRef> AST.Parser.parseDecl(
|
if (k == "name")
|
||||||
'meta import microbit {'+
|
v = v || "unnamed";
|
||||||
' pub "hrgbjn"'+
|
header.meta[k] = v;
|
||||||
'}'
|
});
|
||||||
);
|
// [name] deserves a special treatment because it
|
||||||
var jLib = <J.JLibrary> J.addIdsAndDumpNode(lib);
|
// appears both on the header and in the metadata.
|
||||||
app.decls.push(jLib);
|
header.name = metadata.name;
|
||||||
}
|
|
||||||
|
|
||||||
// Takes a [JApp] and runs its through various hoops to make sure
|
// Writes into local storage.
|
||||||
// everything is type-checked and resolved properly.
|
World.updateInstalledScriptAsync(header, scriptText, editorState, false, "").then(() => {
|
||||||
function roundtrip(a: J.JApp): Promise { // of J.JApp
|
console.log("[external] script saved properly");
|
||||||
addMicrobitLibrary(a);
|
this.post(<Message_SaveAck>{
|
||||||
var text = J.serialize(a);
|
type: MessageType.SaveAck,
|
||||||
return AST.loadScriptAsync((id: string) => {
|
where: SaveLocation.Local,
|
||||||
if (id == "")
|
status: Status.Ok,
|
||||||
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
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
iframe.setAttribute("src", editor.origin + editor.path);
|
|
||||||
iframeDiv.appendChild(iframe);
|
|
||||||
|
|
||||||
// Change the hash and the window title.
|
// Schedules a cloud sync; set the right state so
|
||||||
TheEditor.historyMgr.setHash("edit:" + data.guid, editor.name);
|
// 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;
|
return e;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Compilation error", e);
|
console.error("Compilation error", e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,335 +4,336 @@
|
||||||
|
|
||||||
module TDev {
|
module TDev {
|
||||||
|
|
||||||
// ---------- Communication protocol
|
// ---------- Communication protocol
|
||||||
|
|
||||||
var allowedOrigins: { [index: string]: any } = {
|
var allowedOrigins: { [index: string]: any } = {
|
||||||
"http://localhost:4242": null,
|
"http://localhost:4242": null,
|
||||||
"https://www.touchdevelop.com": null,
|
"https://www.touchdevelop.com": null,
|
||||||
"https://mbitmain.azurewebsites.net": 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)
|
if (message.merge)
|
||||||
// message.
|
promptMerge(message.merge);
|
||||||
var outer: Window = null;
|
}
|
||||||
var origin: string = null;
|
|
||||||
|
|
||||||
// A global that remembers the current version we're editing
|
// ---------- UI functions
|
||||||
var currentVersion: string;
|
|
||||||
var inMerge: boolean = false;
|
|
||||||
|
|
||||||
window.addEventListener("message", (event) => {
|
interface EditorState {
|
||||||
if (!(event.origin in allowedOrigins)) {
|
lastSave: Date;
|
||||||
console.error("[inner message] not from the right origin!", event.origin);
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!outer || !origin) {
|
function statusMsg(s: string, st: External.Status) {
|
||||||
outer = event.source;
|
var box = <HTMLElement> $("#log");
|
||||||
origin = event.origin;
|
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) {
|
setName(message.script.metadata.name);
|
||||||
console.log("[inner message]", message);
|
setDescription(message.script.metadata.description);
|
||||||
|
|
||||||
switch (message.type) {
|
// That's triggered when the user closes or reloads the whole page, but
|
||||||
case External.MessageType.Init:
|
// doesn't help if the user hits the "back" button in our UI.
|
||||||
setupEditor(<External.Message_Init> message);
|
window.addEventListener("beforeunload", function (e) {
|
||||||
setupButtons();
|
if (dirty) {
|
||||||
setupCurrentVersion(<External.Message_Init> message);
|
var confirmationMessage = "Some of your changes have not been saved. Quit anyway?";
|
||||||
break;
|
(e || window.event).returnValue = confirmationMessage;
|
||||||
|
return confirmationMessage;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
case External.MessageType.SaveAck:
|
window.setInterval(() => {
|
||||||
saveAck(<External.Message_SaveAck> message);
|
doSave();
|
||||||
break;
|
}, 5000);
|
||||||
|
|
||||||
case External.MessageType.Merge:
|
console.log("[loaded] cloud version " + message.script.baseSnapshot +
|
||||||
promptMerge((<External.Message_Merge> message).merge);
|
"(dated from: "+state.lastSave+")");
|
||||||
break;
|
}
|
||||||
|
|
||||||
case External.MessageType.CompileAck:
|
function doSave(force = false) {
|
||||||
compileAck(<External.Message_CompileAck> message);
|
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) {
|
function setupButtons() {
|
||||||
if (!outer)
|
$("#command-quit").addEventListener("click", () => {
|
||||||
console.error("Invalid state");
|
doSave();
|
||||||
outer.postMessage(message, origin);
|
post({ type: External.MessageType.Quit });
|
||||||
}
|
});
|
||||||
|
$("#command-compile").addEventListener("click", () => {
|
||||||
// ---------- Revisions
|
post(<External.Message_Compile> {
|
||||||
|
type: External.MessageType.Compile,
|
||||||
function prefix(where: External.SaveLocation) {
|
text: compile(Blockly.mainWorkspace, {
|
||||||
switch (where) {
|
name: getName(),
|
||||||
case External.SaveLocation.Cloud:
|
description: getDescription()
|
||||||
return("☁ [cloud]");
|
}),
|
||||||
case External.SaveLocation.Local:
|
language: External.Language.TouchDevelop
|
||||||
return("⌂ [local]");
|
});
|
||||||
}
|
});
|
||||||
}
|
$("#command-graduate").addEventListener("click", () => {
|
||||||
|
post(<External.Message_Upgrade> {
|
||||||
function saveAck(message: External.Message_SaveAck) {
|
type: External.MessageType.Upgrade,
|
||||||
switch (message.status) {
|
ast: compile(Blockly.mainWorkspace, {
|
||||||
case External.Status.Error:
|
name: getName(),
|
||||||
statusMsg(prefix(message.where)+" error: "+message.error, message.status);
|
description: getDescription()
|
||||||
break;
|
}),
|
||||||
case External.Status.Ok:
|
name: getName()+" (converted)",
|
||||||
if (message.where == External.SaveLocation.Cloud) {
|
});
|
||||||
statusMsg(prefix(message.where)+" successfully saved version (cloud in sync? "+
|
});
|
||||||
message.cloudIsInSync +", "+
|
$("#command-run").addEventListener("click", () => {
|
||||||
"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", () => {
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// vim: set ts=2 sw=2 sts=2:
|
||||||
|
|
Загрузка…
Ссылка в новой задаче