DuktapeDebugger.ts 32 KB

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