DuktapeDebugger.ts 34 KB

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