DuktapeDebugger.ts 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862
  1. import HostInteropType from "../interop";
  2. import * as debuggerProxy from "./HostDebuggerExtensionProxy";
  3. import {default as BreakpointDecoratorManager, Breakpoint} from "./BreakpointDecoratorManager";
  4. /*
  5. * Duktape debugger web client
  6. *
  7. * Talks to the NodeJS server using socket.io.
  8. *
  9. * http://unixpapa.com/js/key.html
  10. */
  11. export default class DuktapeDebugger {
  12. // Monaco editor
  13. editor: monaco.editor.IStandaloneCodeEditor;
  14. socket: SocketIOClient.Socket;
  15. breakpointDecorator: BreakpointDecoratorManager;
  16. // Update interval for custom source highlighting.
  17. SOURCE_UPDATE_INTERVAL = 350;
  18. // Source view
  19. activeFileName = null; // file that we want to be loaded in source view
  20. activeLine = null; // scroll to line once file has been loaded
  21. activeHighlight = null; // line that we want to highlight (if any)
  22. loadedFileName = null; // currently loaded (shown) file
  23. loadedLineCount = 0; // currently loaded file line count
  24. loadedFileExecuting = false; // true if currFileName (loosely) matches loadedFileName
  25. loadedLinePending = null; // if set, scroll loaded file to requested line
  26. highlightLine = null; // highlight line
  27. sourceEditedLines = []; // line numbers which have been modified
  28. // (added classes etc, tracked for removing)
  29. sourceUpdateInterval = null; // timer for updating source view
  30. sourceFetchXhr = null; // current AJAX request for fetching a source file (if any)
  31. forceButtonUpdate = false; // hack to reset button states
  32. bytecodeDialogOpen = false; // bytecode dialog active
  33. bytecodeIdxHighlight = null; // index of currently highlighted line (or null)
  34. bytecodeIdxInstr = 0; // index to first line of bytecode instructions
  35. // Execution state
  36. prevState = null; // previous execution state ("paused", "running", etc)
  37. prevAttached = null; // previous debugger attached state (true, false, null)
  38. currFileName = null; // current filename being executed
  39. currFuncName = null; // current function name being executed
  40. currLine = 0; // current line being executed
  41. currPc = 0; // current bytecode PC being executed
  42. // current execution state ("paused", "running", "detached", etc)
  43. currState: "connected" | "disconnected" | "running" | "paused" | "reconnecting";
  44. currAttached = false; // current debugger attached state (true or false)
  45. currLocals = []; // current local variables
  46. currCallstack = []; // current callstack (from top to bottom)
  47. currBreakpoints: Breakpoint[] = []; // current breakpoints
  48. startedRunning = 0; // timestamp when last started running (if running)
  49. // (used to grey out the source file if running for long enough)
  50. /*
  51. * Helpers
  52. */
  53. formatBytes(x) {
  54. if (x < 1024) {
  55. return String(x) + " bytes";
  56. } else if (x < 1024 * 1024) {
  57. return (x / 1024).toPrecision(3) + " kB";
  58. } else {
  59. return (x / (1024 * 1024)).toPrecision(3) + " MB";
  60. }
  61. }
  62. /*
  63. * Source view periodic update handling
  64. */
  65. doSourceUpdate() {
  66. var elem;
  67. // Remove previously added custom classes
  68. this.sourceEditedLines.forEach((linenum) => {
  69. elem = $("#source-code div")[linenum - 1];
  70. if (elem) {
  71. elem.classList.remove("breakpoint");
  72. elem.classList.remove("execution");
  73. elem.classList.remove("highlight");
  74. }
  75. });
  76. this.sourceEditedLines.length = 0;
  77. // If we"re executing the file shown, highlight current line
  78. if (this.loadedFileExecuting) {
  79. this.editor.revealLineInCenterIfOutsideViewport(this.currLine);
  80. this.editor.setPosition(new monaco.Position(this.currLine, 0));
  81. this.sourceEditedLines.push(this.currLine);
  82. elem = $("#source-code div")[this.currLine - 1];
  83. if (elem) {
  84. //sourceEditedLines.push(currLine);
  85. elem.classList.add("execution");
  86. }
  87. }
  88. // Add breakpoints
  89. this.breakpointDecorator.setCurrentFileName(this.loadedFileName);
  90. this.currBreakpoints.forEach((bp) => {
  91. if (bp.fileName === this.loadedFileName) {
  92. this.breakpointDecorator.addBreakpointDecoration(bp.fileName, bp.lineNumber);
  93. this.sourceEditedLines.push(bp.lineNumber);
  94. }
  95. });
  96. if (this.highlightLine !== null) {
  97. elem = $("#source-code div")[this.highlightLine - 1];
  98. if (elem) {
  99. this.sourceEditedLines.push(this.highlightLine);
  100. elem.classList.add("highlight");
  101. }
  102. }
  103. // Bytecode dialog highlight
  104. if (this.loadedFileExecuting && this.bytecodeDialogOpen && this.bytecodeIdxHighlight !== this.bytecodeIdxInstr + this.currPc) {
  105. if (typeof this.bytecodeIdxHighlight === "number") {
  106. $("#bytecode-preformatted div")[this.bytecodeIdxHighlight].classList.remove("highlight");
  107. }
  108. this.bytecodeIdxHighlight = this.bytecodeIdxInstr + this.currPc;
  109. $("#bytecode-preformatted div")[this.bytecodeIdxHighlight].classList.add("highlight");
  110. }
  111. // If no-one requested us to scroll to a specific line, finish.
  112. if (this.loadedLinePending == null) {
  113. return;
  114. }
  115. // Scroll to the requested line
  116. var reqLine = this.loadedLinePending;
  117. this.loadedLinePending = null;
  118. this.editor.revealLineInCenterIfOutsideViewport(reqLine);
  119. this.editor.setPosition(new monaco.Position(reqLine, 0));
  120. debuggerProxy.notifyHostCurrentSourcePosition("Resources/" + this.loadedFileName, reqLine);
  121. }
  122. /*
  123. * UI update handling when exec-status update arrives
  124. */
  125. doUiUpdate() {
  126. var now = Date.now();
  127. // Note: loadedFileName can be either from target or from server, but they
  128. // must match exactly. We could do a loose match here, but exact matches
  129. // are needed for proper breakpoint handling anyway.
  130. this.loadedFileExecuting = (this.loadedFileName === this.currFileName);
  131. // If we just started running, store a timestamp so we can grey out the
  132. // source view only if we execute long enough (i.e. we"re not just
  133. // stepping).
  134. if (this.currState !== this.prevState && this.currState === "running") {
  135. this.startedRunning = now;
  136. }
  137. // If we just became paused, check for eval watch
  138. if (this.currState !== this.prevState && this.currState === "paused") {
  139. if ($("#eval-watch").is(":checked")) {
  140. this.submitEval(); // don"t clear eval input
  141. }
  142. }
  143. // Update current execution state
  144. if (this.currFileName === "" && this.currLine === 0) {
  145. $("#current-fileline").text("");
  146. } else {
  147. $("#current-fileline").text(String(this.currFileName) + ":" + String(this.currLine));
  148. }
  149. if (this.currFuncName === "" && this.currPc === 0) {
  150. $("#current-funcpc").text("");
  151. } else {
  152. $("#current-funcpc").text(String(this.currFuncName) + "() pc " + String(this.currPc));
  153. }
  154. $("#current-state").text(String(this.currState));
  155. // Update buttons
  156. if (this.currState !== this.prevState || this.currAttached !== this.prevAttached || this.forceButtonUpdate) {
  157. $("#stepinto-button").prop("disabled", !this.currAttached || this.currState !== "paused");
  158. $("#stepover-button").prop("disabled", !this.currAttached || this.currState !== "paused");
  159. $("#stepout-button").prop("disabled", !this.currAttached || this.currState !== "paused");
  160. $("#resume-button").prop("disabled", !this.currAttached || this.currState !== "paused");
  161. $("#pause-button").prop("disabled", !this.currAttached || this.currState !== "running");
  162. $("#attach-button").prop("disabled", this.currAttached);
  163. if (this.currAttached) {
  164. $("#attach-button").removeClass("enabled");
  165. } else {
  166. $("#attach-button").addClass("enabled");
  167. }
  168. $("#detach-button").prop("disabled", !this.currAttached);
  169. $("#eval-button").prop("disabled", !this.currAttached);
  170. $("#add-breakpoint-button").prop("disabled", !this.currAttached);
  171. $("#delete-all-breakpoints-button").prop("disabled", !this.currAttached);
  172. $(".delete-breakpoint-button").prop("disabled", !this.currAttached);
  173. $("#putvar-button").prop("disabled", !this.currAttached);
  174. $("#getvar-button").prop("disabled", !this.currAttached);
  175. $("#heap-dump-download-button").prop("disabled", !this.currAttached);
  176. }
  177. if (this.currState !== "running" || this.forceButtonUpdate) {
  178. // Remove pending highlight once we"re no longer running.
  179. $("#pause-button").removeClass("pending");
  180. $("#eval-button").removeClass("pending");
  181. }
  182. this.forceButtonUpdate = false;
  183. // Make source window grey when running for a longer time, use a small
  184. // delay to avoid flashing grey when stepping.
  185. if (this.currState === "running" && now - this.startedRunning >= 500) {
  186. $("#source-pre").removeClass("notrunning");
  187. $("#current-state").removeClass("notrunning");
  188. } else {
  189. $("#source-pre").addClass("notrunning");
  190. $("#current-state").addClass("notrunning");
  191. }
  192. // Force source view to match currFileName only when running or when
  193. // just became paused (from running or detached).
  194. var fetchSource = false;
  195. if (typeof this.currFileName === "string") {
  196. if (this.currState === "running" ||
  197. (this.prevState !== "paused" && this.currState === "paused") ||
  198. (this.currAttached !== this.prevAttached)) {
  199. if (this.activeFileName !== this.currFileName) {
  200. fetchSource = true;
  201. this.activeFileName = this.currFileName;
  202. this.activeLine = this.currLine;
  203. this.activeHighlight = null;
  204. this.requestSourceRefetch();
  205. }
  206. }
  207. }
  208. // Force line update (scrollTop) only when running or just became paused.
  209. // Otherwise let user browse and scroll source files freely.
  210. if (!fetchSource) {
  211. if ((this.prevState !== "paused" && this.currState === "paused") ||
  212. this.currState === "running") {
  213. this.loadedLinePending = this.currLine || 0;
  214. }
  215. }
  216. }
  217. deleteAllBreakpoints(notifyHost = true) {
  218. this.socket.emit("delete-all-breakpoints");
  219. this.breakpointDecorator.clearBreakpointDecorations();
  220. if (notifyHost) {
  221. debuggerProxy.removeAllBreakpoints();
  222. }
  223. }
  224. addBreakpoint(fileName: string, lineNumber: number, notifyHost = true) {
  225. fileName = this.fixBreakpointFilename(fileName);
  226. this.breakpointDecorator.addBreakpointDecoration(fileName, lineNumber);
  227. this.socket.emit("add-breakpoint", {
  228. fileName,
  229. lineNumber
  230. });
  231. if (notifyHost) {
  232. debuggerProxy.addBreakpoint(fileName, lineNumber);
  233. }
  234. }
  235. toggleBreakpoint(fileName: string, lineNumber: number, notifyHost = true) {
  236. fileName = this.fixBreakpointFilename(fileName);
  237. this.breakpointDecorator.toggleBreakpoint(fileName, lineNumber);
  238. this.socket.emit("toggle-breakpoint", {
  239. fileName,
  240. lineNumber
  241. });
  242. if (notifyHost) {
  243. debuggerProxy.toggleBreakpoint(fileName, lineNumber);
  244. }
  245. }
  246. removeBreakpoint(fileName: string, lineNumber: number, notifyHost = true) {
  247. fileName = this.fixBreakpointFilename(fileName);
  248. this.breakpointDecorator.removeBreakpointDecoration(fileName, lineNumber);
  249. this.socket.emit("delete-breakpoint", {
  250. fileName,
  251. lineNumber
  252. });
  253. if (notifyHost) {
  254. debuggerProxy.removeBreakpoint(fileName, lineNumber);
  255. }
  256. }
  257. pause() {
  258. this.socket.emit("pause", {});
  259. // Pause may take seconds to complete so indicate it is pending.
  260. $("#pause-button").addClass("pending");
  261. }
  262. resume() {
  263. this.socket.emit("resume", {});
  264. }
  265. stepInto() {
  266. this.socket.emit("stepinto", {});
  267. setTimeout(() => this.doSourceUpdate(), 125);
  268. }
  269. stepOut() {
  270. this.socket.emit("stepout", {});
  271. setTimeout(() => this.doSourceUpdate(), 125);
  272. }
  273. stepOver() {
  274. this.socket.emit("stepover", {});
  275. setTimeout(() => this.doSourceUpdate(), 125);
  276. }
  277. initSocket() {
  278. /*
  279. * Init socket.io and add handlers
  280. */
  281. this.socket = io.connect("http://localhost:9092"); // returns a Manager
  282. setInterval(() => {
  283. this.socket.emit("keepalive", {
  284. userAgent: (navigator || {} as Navigator).userAgent
  285. });
  286. }, 30000);
  287. this.socket.on("connect", () => {
  288. $("#socketio-info").text("connected");
  289. this.currState = "connected";
  290. this.fetchSourceList();
  291. });
  292. this.socket.on("disconnect", () => {
  293. $("#socketio-info").text("not connected");
  294. this.currState = "disconnected";
  295. });
  296. this.socket.on("reconnecting", () => {
  297. $("#socketio-info").text("reconnecting");
  298. this.currState = "reconnecting";
  299. });
  300. this.socket.on("error", (err) => {
  301. $("#socketio-info").text(err);
  302. });
  303. this.socket.on("replaced", () => {
  304. // XXX: how to minimize the chance we"ll further communciate with the
  305. // server or reconnect to it? socket.reconnection()?
  306. // We"d like to window.close() here but can"t (not allowed from scripts).
  307. // Alert is the next best thing.
  308. alert("Debugger connection replaced by a new one, do you have multiple tabs open? If so, please close this tab.");
  309. });
  310. this.socket.on("keepalive", (msg) => {
  311. // Not really interesting in the UI
  312. // $("#server-info").text(new Date() + ": " + JSON.stringify(msg));
  313. });
  314. this.socket.on("basic-info", (msg) => {
  315. $("#duk-version").text(String(msg.duk_version));
  316. $("#duk-git-describe").text(String(msg.duk_git_describe));
  317. $("#target-info").text(String(msg.target_info));
  318. $("#endianness").text(String(msg.endianness));
  319. });
  320. this.socket.on("exec-status", (msg) => {
  321. // Not 100% reliable if callstack has several functions of the same name
  322. if (this.bytecodeDialogOpen && (this.currFileName != msg.fileName || this.currFuncName != msg.funcName)) {
  323. this.socket.emit("get-bytecode", {});
  324. }
  325. this.currFileName = msg.fileName;
  326. this.currFuncName = msg.funcName;
  327. this.currLine = msg.line;
  328. this.currPc = msg.pc;
  329. this.currState = msg.state;
  330. this.currAttached = msg.attached;
  331. // Duktape now restricts execution status updates quite effectively so
  332. // there"s no need to rate limit UI updates now.
  333. this.doUiUpdate();
  334. this.prevState = this.currState;
  335. this.prevAttached = this.currAttached;
  336. });
  337. // Update the "console" output based on lines sent by the server. The server
  338. // rate limits these updates to keep the browser load under control. Even
  339. // better would be for the client to pull this (and other stuff) on its own.
  340. this.socket.on("output-lines", (msg) => {
  341. var elem = $("#output");
  342. var i, n, ent;
  343. elem.empty();
  344. for (i = 0, n = msg.length; i < n; i++) {
  345. ent = msg[i];
  346. if (ent.type === "print") {
  347. elem.append($("<div></div>").text(ent.message));
  348. } else if (ent.type === "alert") {
  349. elem.append($("<div class='alert'></div>").text(ent.message));
  350. } else if (ent.type === "log") {
  351. elem.append($("<div class='log loglevel' + ent.level + ''></div>").text(ent.message));
  352. } else if (ent.type === "debugger-info") {
  353. elem.append($("<div class='debugger-info'><div>").text(ent.message));
  354. } else if (ent.type === "debugger-debug") {
  355. elem.append($("<div class='debugger-debug'><div>").text(ent.message));
  356. } else {
  357. elem.append($("<div></div>").text(ent.message));
  358. }
  359. }
  360. // http://stackoverflow.com/questions/14918787/jquery-scroll-to-bottom-of-div-even-after-it-updates
  361. // Stop queued animations so that we always scroll quickly to bottom
  362. $("#output").stop(true);
  363. $("#output").animate({ scrollTop: $("#output")[0].scrollHeight }, 1000);
  364. });
  365. this.socket.on("callstack", (msg) => {
  366. var elem = $("#callstack");
  367. var s1, s2, div;
  368. this.currCallstack = msg.callstack;
  369. elem.empty();
  370. msg.callstack.forEach((e) => {
  371. s1 = $("<a class='rest'></a>").text(e.fileName + ":" + e.lineNumber + " (pc " + e.pc + ")"); // float
  372. s1.on("click", () => {
  373. this.activeFileName = e.fileName;
  374. this.activeLine = e.lineNumber || 1;
  375. this.activeHighlight = this.activeLine;
  376. this.requestSourceRefetch();
  377. });
  378. s2 = $("<span class='func'></span>").text(e.funcName + "()");
  379. div = $("<div></div>");
  380. div.append(s1);
  381. div.append(s2);
  382. elem.append(div);
  383. });
  384. });
  385. this.socket.on("locals", (msg) => {
  386. var elem = $("#locals");
  387. var s1, s2, div;
  388. var i, n, e;
  389. this.currLocals = msg.locals;
  390. elem.empty();
  391. for (i = 0, n = msg.locals.length; i < n; i++) {
  392. e = msg.locals[i];
  393. s1 = $("<span class='value'></span>").text(e.value); // float
  394. s2 = $("<span class='key'></span>").text(e.key);
  395. div = $("<div></div>");
  396. div.append(s1);
  397. div.append(s2);
  398. elem.append(div);
  399. }
  400. });
  401. this.socket.on("debug-stats", (msg) => {
  402. $("#debug-rx-bytes").text(this.formatBytes(msg.rxBytes));
  403. $("#debug-rx-dvalues").text(msg.rxDvalues);
  404. $("#debug-rx-messages").text(msg.rxMessages);
  405. $("#debug-rx-kbrate").text((msg.rxBytesPerSec / 1024).toFixed(2));
  406. $("#debug-tx-bytes").text(this.formatBytes(msg.txBytes));
  407. $("#debug-tx-dvalues").text(msg.txDvalues);
  408. $("#debug-tx-messages").text(msg.txMessages);
  409. $("#debug-tx-kbrate").text((msg.txBytesPerSec / 1024).toFixed(2));
  410. });
  411. this.socket.on("breakpoints", (msg) => {
  412. var elem = $("#breakpoints");
  413. var div;
  414. var sub;
  415. this.currBreakpoints = msg.breakpoints;
  416. elem.empty();
  417. // First line is special
  418. div = $("<div></div>");
  419. sub = $("<button id='delete-all-breakpoints-button'></button>").text("Delete all breakpoints");
  420. sub.on("click", () => {
  421. this.deleteAllBreakpoints();
  422. });
  423. div.append(sub);
  424. //sub = $("<input id='add-breakpoint-file'></input>").val("file.js");
  425. //div.append(sub);
  426. //sub = $("<span></span>").text(":");
  427. //div.append(sub);
  428. //sub = $("<input id='add-breakpoint-line'></input>").val("123");
  429. //div.append(sub);
  430. //sub = $("<button id='add-breakpoint-button'></button>").text("Add breakpoint");
  431. //sub.on("click", () => {
  432. //this.addBreakpoint($("#add-breakpoint-file").val(), Number($("#add-breakpoint-line").val()));
  433. //});
  434. //div.append(sub);
  435. //sub = $("<span id='breakpoint-hint'></span>").text("or dblclick source");
  436. //div.append(sub);
  437. elem.append(div);
  438. // Active breakpoints follow
  439. msg.breakpoints.forEach((bp) => {
  440. var div;
  441. var sub;
  442. div = $("<div class='breakpoint-line'></div>");
  443. sub = $("<button class='delete-breakpoint-button'></button>").text("Delete");
  444. sub.on("click", () => {
  445. this.removeBreakpoint(bp.fileName, bp.lineNumber);
  446. });
  447. div.append(sub);
  448. sub = $("<a></a>").text((bp.fileName || "?") + ":" + (bp.lineNumber || 0));
  449. sub.on("click", () => {
  450. this.activeFileName = bp.fileName || "";
  451. this.activeLine = bp.lineNumber || 1;
  452. this.activeHighlight = this.activeLine;
  453. this.requestSourceRefetch();
  454. });
  455. div.append(sub);
  456. elem.append(div);
  457. });
  458. this.forceButtonUpdate = true;
  459. this.doUiUpdate();
  460. });
  461. this.socket.on("eval-result", (msg) => {
  462. $("#eval-output").text((msg.error ? "ERROR: " : "") + msg.result);
  463. // Remove eval button "pulsating" glow when we get a result
  464. $("#eval-button").removeClass("pending");
  465. });
  466. this.socket.on("getvar-result", (msg) => {
  467. $("#var-output").text(msg.found ? msg.result : "NOTFOUND");
  468. });
  469. this.socket.on("bytecode", (msg) => {
  470. var elem, div;
  471. elem = $("#bytecode-preformatted");
  472. elem.empty();
  473. msg.preformatted.split("\n").forEach((line, idx) => {
  474. div = $("<div></div>");
  475. div.text(line);
  476. elem.append(div);
  477. });
  478. this.bytecodeIdxHighlight = null;
  479. this.bytecodeIdxInstr = msg.idxPreformattedInstructions;
  480. });
  481. $("#stepinto-button").click(() => this.stepInto());
  482. $("#stepover-button").click(() => this.stepOver());
  483. $("#stepout-button").click(() => this.stepOut());
  484. $("#pause-button").click(() => this.pause());
  485. $("#resume-button").click(() => this.resume());
  486. $("#attach-button").click(() => {
  487. this.socket.emit("attach", {});
  488. this.retrieveBreakpoints();
  489. });
  490. $("#detach-button").click(() => {
  491. this.socket.emit("detach", {});
  492. });
  493. $("#about-button").click(() => {
  494. $("#about-dialog").dialog("open");
  495. });
  496. $("#show-bytecode-button").click(() => {
  497. this.bytecodeDialogOpen = true;
  498. $("#bytecode-dialog").dialog("open");
  499. let elem = $("#bytecode-preformatted");
  500. elem.empty().text("Loading bytecode...");
  501. this.socket.emit("get-bytecode", {});
  502. });
  503. $("#eval-button").click(() => {
  504. this.submitEval();
  505. $("#eval-input").val("");
  506. });
  507. $("#getvar-button").click(() => {
  508. this.socket.emit("getvar", { varname: $("#varname-input").val() });
  509. });
  510. $("#putvar-button").click(() => {
  511. // The variable value is parsed as JSON right now, but it"d be better to
  512. // also be able to parse buffer values etc.
  513. var val = JSON.parse($("#varvalue-input").val());
  514. this.socket.emit("putvar", { varname: $("#varname-input").val(), varvalue: val });
  515. });
  516. $("#source-code").dblclick((event) => {
  517. var target = event.target;
  518. var elems = $("#source-code div");
  519. var i, n;
  520. var line = 0;
  521. // XXX: any faster way; elems doesn"t have e.g. indexOf()
  522. for (i = 0, n = elems.length; i < n; i++) {
  523. if (target === elems[i]) {
  524. line = i + 1;
  525. }
  526. }
  527. this.toggleBreakpoint(this.loadedFileName, line);
  528. });
  529. }
  530. submitEval() {
  531. this.socket.emit("eval", { input: $("#eval-input").val() });
  532. // Eval may take seconds to complete so indicate it is pending.
  533. $("#eval-button").addClass("pending");
  534. }
  535. setSourceText(data) {
  536. this.editor.deltaDecorations([], []);
  537. this.editor.getModel().setValue(data);
  538. this.sourceEditedLines = [];
  539. }
  540. /*
  541. * AJAX request handling to fetch source files
  542. */
  543. requestSourceFile(fileName: string, lineNumber: number) {
  544. console.log(`Retrieving File: ${fileName}`);
  545. // get the code
  546. return HostInteropType.getInstance().getFileResource("Resources/" + fileName).then((data: string) => {
  547. this.loadedFileName = fileName;
  548. this.loadedLineCount = data.split("\n").length; // XXX: ignore issue with last empty line for now
  549. this.loadedFileExecuting = (this.loadedFileName === this.currFileName);
  550. this.setSourceText(data);
  551. this.loadedLinePending = this.activeLine || 1;
  552. this.highlightLine = this.activeHighlight; // may be null
  553. this.activeLine = null;
  554. this.activeHighlight = null;
  555. this.doSourceUpdate();
  556. }).catch((e: Editor.ClientExtensions.AtomicErrorMessage) => {
  557. console.log("Error loading code: " + e.error_message);
  558. // Not worth alerting about because source fetch errors happen
  559. // all the time, e.g. for dynamically evaluated code.
  560. this.sourceFetchXhr = null;
  561. // XXX: prevent retry of no-such-file by negative caching?
  562. this.loadedFileName = fileName;
  563. this.loadedLineCount = 1;
  564. this.loadedFileExecuting = false;
  565. this.setSourceText("// Cannot load source file: " + fileName);
  566. this.loadedLinePending = 1;
  567. this.activeLine = null;
  568. this.activeHighlight = null;
  569. this.doSourceUpdate();
  570. });
  571. }
  572. requestSourceRefetch() {
  573. if (!this.activeFileName) {
  574. return;
  575. }
  576. this.requestSourceFile(this.activeFileName, this.activeLine).then(() => {
  577. // XXX: hacky transition, make source change visible
  578. $("#source-pre").fadeTo("fast", 0.25, () => {
  579. $("#source-pre").fadeTo("fast", 1.0);
  580. });
  581. }).catch((e: Editor.ClientExtensions.AtomicErrorMessage) => {
  582. // XXX: error transition here
  583. console.log("Error loading code: " + e.error_message);
  584. $("#source-pre").fadeTo("fast", 0.25, function() {
  585. $("#source-pre").fadeTo("fast", 1.0);
  586. });
  587. });
  588. }
  589. /*
  590. * AJAX request for fetching the source list
  591. */
  592. fetchSourceList() {
  593. /* TODO: Fix Ajax
  594. $.ajax({
  595. type: "POST",
  596. url: "/sourceList",
  597. data: JSON.stringify({}),
  598. contentType: "application/json",
  599. success: function(data, status, jqxhr) {
  600. var elem = $("#source-select");
  601. data = JSON.parse(data);
  602. elem.empty();
  603. var opt = $("<option></option>").attr({ "value": "__none__" }).text("No source file selected");
  604. elem.append(opt);
  605. data.forEach(function(ent) {
  606. var opt = $("<option></option>").attr({ "value": ent }).text(ent);
  607. elem.append(opt);
  608. });
  609. elem.change(function() {
  610. activeFileName = elem.val();
  611. activeLine = 1;
  612. requestSourceRefetch();
  613. });
  614. },
  615. error: function(jqxhr, status, err) {
  616. // This is worth alerting about as the UI is somewhat unusable
  617. // if we don"t get a source list.
  618. alert("Failed to load source list: " + err);
  619. },
  620. dataType: "text"
  621. });
  622. */
  623. }
  624. /*
  625. * Initialization
  626. */
  627. initialize(editorProvided) {
  628. this.editor = editorProvided;
  629. this.breakpointDecorator = new BreakpointDecoratorManager(editorProvided);
  630. var showAbout = true;
  631. this.initSocket();
  632. // Source is updated periodically. Other code can also call doSourceUpdate()
  633. // directly if an immediate update is needed.
  634. // this.sourceUpdateInterval = setInterval(this.doSourceUpdate.bind(this), this.SOURCE_UPDATE_INTERVAL);
  635. this.editor.onMouseMove((e) => {
  636. var targetZone = e.target.toString();
  637. if (targetZone.indexOf("GUTTER_GLYPH_MARGIN") != -1) {
  638. var line = e.target.position.lineNumber;
  639. this.breakpointDecorator.updateMarginHover(line);
  640. } else {
  641. this.breakpointDecorator.removeMarginHover();
  642. }
  643. });
  644. this.editor.onMouseDown((e) => {
  645. var targetZone = e.target.toString();
  646. if (targetZone.indexOf("GUTTER_GLYPH_MARGIN") != -1) {
  647. var line = e.target.position.lineNumber;
  648. this.toggleBreakpoint(this.loadedFileName, line);
  649. }
  650. });
  651. // About dialog, shown automatically on first startup.
  652. $("#about-dialog").dialog({
  653. autoOpen: false,
  654. hide: "fade", // puff
  655. show: "fade", // slide, puff
  656. width: 500,
  657. height: 300
  658. });
  659. // Bytecode dialog
  660. $("#bytecode-dialog").dialog({
  661. autoOpen: false,
  662. hide: "fade", // puff
  663. show: "fade", // slide, puff
  664. width: 700,
  665. height: 600,
  666. close: () => {
  667. this.bytecodeDialogOpen = false;
  668. this.bytecodeIdxHighlight = null;
  669. this.bytecodeIdxInstr = 0;
  670. }
  671. });
  672. // http://diveintohtml5.info/storage.html
  673. if (typeof localStorage !== "undefined") {
  674. if (localStorage.getItem("about-shown")) {
  675. showAbout = false;
  676. } else {
  677. localStorage.setItem("about-shown", "yes");
  678. }
  679. }
  680. if (showAbout) {
  681. $("#about-dialog").dialog("open");
  682. }
  683. // onclick handler for exec status text
  684. function loadCurrFunc() {
  685. this.activeFileName = this.currFileName;
  686. this.activeLine = this.currLine;
  687. this.requestSourceRefetch();
  688. }
  689. $("#exec-other").on("click", loadCurrFunc);
  690. // Enter handling for eval input
  691. // https://forum.jquery.com/topic/bind-html-input-to-enter-key-keypress
  692. $("#eval-input").keypress((event) => {
  693. if (event.keyCode == 13) {
  694. this.submitEval();
  695. $("#eval-input").val("");
  696. }
  697. });
  698. // Eval watch handling
  699. $("#eval-watch").change(() => {
  700. // nop
  701. });
  702. this.registerDebuggerFunctions();
  703. this.forceButtonUpdate = true;
  704. this.doUiUpdate();
  705. };
  706. async retrieveBreakpoints() {
  707. let s = await debuggerProxy.getBreakpoints();
  708. // If the filename starts with "Resources" then trim it off since the module
  709. // name won't have that, but the editor uses it
  710. s.forEach(b => this.addBreakpoint(b.fileName, b.lineNumber));
  711. }
  712. registerDebuggerFunctions() {
  713. // Register the callback functions
  714. const interop = HostInteropType.getInstance();
  715. interop.addCustomHostRoutine(
  716. debuggerProxy.debuggerHostKeys.toggleBreakpoint,
  717. this.toggleBreakpoint.bind(this));
  718. interop.addCustomHostRoutine(
  719. debuggerProxy.debuggerHostKeys.addBreakpoint,
  720. this.addBreakpoint.bind(this));
  721. interop.addCustomHostRoutine(
  722. debuggerProxy.debuggerHostKeys.removeBreakpoint,
  723. this.removeBreakpoint.bind(this));
  724. interop.addCustomHostRoutine(
  725. debuggerProxy.debuggerHostKeys.pause,
  726. this.pause.bind(this));
  727. debuggerProxy.registerDebuggerListener("DuktapeDebugger");
  728. }
  729. fixBreakpointFilename(fileName: string): string {
  730. return fileName.replace(/^Resources\//, "");
  731. }
  732. }