| /** |
| * @module modules/debugger-app-sk |
| * @description <h2><code>debugger-app-sk</code></h2> |
| * |
| */ |
| import { $$ } from 'common-sk/modules/dom'; |
| import 'codemirror/mode/clike/clike'; // Syntax highlighting for c-like languages. |
| import CodeMirror, { EditorConfiguration } from 'codemirror'; |
| import { define } from 'elements-sk/define'; |
| import { html, TemplateResult } from 'lit-html'; |
| |
| import '../../../infra-sk/modules/theme-chooser-sk'; |
| import { isDarkMode } from '../../../infra-sk/modules/theme-chooser-sk/theme-chooser-sk'; |
| import { ElementSk } from '../../../infra-sk/modules/ElementSk'; |
| import * as SkSLConstants from '../sksl-constants/sksl-constants'; |
| |
| import { Convert, DebugTrace } from '../debug-trace/debug-trace'; |
| import { DebugTracePlayer, VariableData } from '../debug-trace-player/debug-trace-player'; |
| |
| // It is assumed that this symbol is being provided by a version.js file loaded in before this |
| // file. |
| declare const SKIA_VERSION: string; |
| |
| // Define a new mode and mime-type for SkSL shaders. We follow the shader naming |
| // convention found in CodeMirror. |
| CodeMirror.defineMIME('x-shader/x-sksl', { |
| name: 'clike', |
| keywords: SkSLConstants.keywords, |
| types: SkSLConstants.types, |
| builtin: SkSLConstants.builtins, |
| blockKeywords: SkSLConstants.blockKeywords, |
| defKeywords: SkSLConstants.defKeywords, |
| typeFirstDefinitions: true, |
| atoms: SkSLConstants.atoms, |
| modeProps: { fold: ['brace', 'include'] }, |
| }); |
| |
| export class DebuggerAppSk extends ElementSk { |
| private trace: DebugTrace | null = null; |
| |
| private player: DebugTracePlayer = new DebugTracePlayer(); |
| |
| private codeMirror: CodeMirror.Editor | null = null; |
| |
| private currentLineHandle: CodeMirror.LineHandle | null = null; |
| |
| constructor() { |
| super(DebuggerAppSk.template); |
| } |
| |
| private static themeFromCurrentMode(): string { |
| return isDarkMode() ? 'ambiance' : 'base16-light'; |
| } |
| |
| connectedCallback(): void { |
| super.connectedCallback(); |
| this._render(); |
| |
| // Set up drag-and-drop support. |
| this.enableDragAndDrop($$<HTMLDivElement>('#drag-area', this)!); |
| |
| // Set up CodeMirror. |
| this.codeMirror = CodeMirror($$<HTMLDivElement>('#codeEditor', this)!, <EditorConfiguration>{ |
| value: '/*** Drag in a DebugTrace JSON file to start the debugger. ***/', |
| lineNumbers: true, |
| mode: 'x-shader/x-sksl', |
| theme: DebuggerAppSk.themeFromCurrentMode(), |
| viewportMargin: Infinity, |
| scrollbarStyle: 'native', |
| readOnly: true, |
| gutters: ['CodeMirror-linenumbers', 'cm-breakpoints'], |
| fixedGutter: true, |
| }); |
| this.codeMirror!.on('gutterClick', (_, line: number) => { |
| // 'line' comes from CodeMirror so is indexed starting from zero. |
| this.toggleBreakpoint(line + 1); |
| }); |
| |
| // Listen for theme changes. |
| document.addEventListener('theme-chooser-toggle', () => { |
| this.codeMirror!.setOption('theme', DebuggerAppSk.themeFromCurrentMode()); |
| }); |
| } |
| |
| getEditor(): CodeMirror.Editor | null { |
| return this.codeMirror; |
| } |
| |
| private updateControls(): void { |
| this.updateCurrentLineMarker(); |
| this._render(); |
| } |
| |
| private updateCurrentLineMarker(): void { |
| if (this.currentLineHandle !== null) { |
| this.codeMirror!.removeLineClass(this.currentLineHandle!, 'background', 'cm-current-line'); |
| this.currentLineHandle = null; |
| } |
| |
| if (!this.player.traceHasCompleted()) { |
| // Subtract one from the line number because CodeMirror uses zero-indexed lines. |
| const lineNumber = this.player.getCurrentLine() - 1; |
| this.currentLineHandle = this.codeMirror!.addLineClass(lineNumber, 'background', |
| 'cm-current-line'); |
| this.codeMirror!.scrollIntoView({ line: lineNumber, ch: 0 }, /*margin=*/36); |
| } |
| } |
| |
| private stackDisplay(): TemplateResult[] { |
| if (this.trace) { |
| let stack = this.player.getCallStack().map((idx: number) => this.trace!.functions[idx].name); |
| if (stack.length > 0) { |
| stack = stack.reverse(); |
| stack[0] = '➔ ' + stack[0]; |
| return stack.map((text: string) => html`<tr><td>${text}</td></tr>`); |
| } |
| } |
| return [html`<tr><td>at global scope</td></tr>`]; |
| } |
| |
| private varsDisplay(): TemplateResult[] { |
| if (this.trace) { |
| const vars: VariableData[] = this.player.getStackDepth() > 0 |
| ? this.player.getLocalVariables(this.player.getStackDepth() - 1) |
| : this.player.getGlobalVariables(); |
| if (vars.length > 0) { |
| return vars.map((v: VariableData) => { |
| const name: string = this.trace!.slots[v.slotIndex].name + |
| this.player.getSlotComponentSuffix(v.slotIndex); |
| const highlight: string = v.dirty ? 'highlighted' : ''; |
| return html`<tr><td class='${highlight}'>${name}</td><td>${v.value}</td></tr>`; |
| }); |
| } |
| } |
| return [html`<tr><td> </td></tr>`]; |
| } |
| |
| loadJSONData(jsonData: string): void { |
| try { |
| this.trace = Convert.toDebugTrace(jsonData); |
| this.codeMirror!.setValue(this.trace.source.join('\n')); |
| this.player.setBreakpoints(new Set()); |
| this.resetTrace(); |
| this.resetBreakpointGutter(); |
| this._render(); |
| } catch (ex) { |
| this.codeMirror!.setValue((ex instanceof Error) ? ex.message : String(ex)); |
| } |
| } |
| |
| private enableDragAndDrop(dropArea: HTMLDivElement): void { |
| dropArea.addEventListener('dragover', (event: DragEvent) => { |
| event.stopPropagation(); |
| event.preventDefault(); |
| event.dataTransfer!.dropEffect = 'move'; |
| }); |
| |
| dropArea.addEventListener('drop', (event: DragEvent) => { |
| event.stopPropagation(); |
| event.preventDefault(); |
| const fileList = event.dataTransfer!.files; |
| if (fileList.length === 1) { |
| const reader = new FileReader(); |
| reader.addEventListener('load', () => { |
| try { |
| this.loadJSONData(reader.result as string); |
| } catch (ex) { |
| this.codeMirror!.setValue('Unable to read JSON trace file.'); |
| } |
| }); |
| reader.readAsText(fileList[0]); |
| } |
| }); |
| } |
| |
| step(): void { |
| this.player.step(); |
| this.updateControls(); |
| } |
| |
| stepOver(): void { |
| this.player.stepOver(); |
| this.updateControls(); |
| } |
| |
| stepOut(): void { |
| this.player.stepOut(); |
| this.updateControls(); |
| } |
| |
| run(): void { |
| this.player.run(); |
| this.updateControls(); |
| } |
| |
| resetTrace(): void { |
| this.player.reset(this.trace); |
| this.player.step(); |
| this.updateControls(); |
| } |
| |
| private static makeDivWithClass(name: string): HTMLDivElement { |
| const marker: HTMLDivElement = document.createElement("div"); |
| marker.classList.add(name); |
| return marker; |
| } |
| |
| private resetBreakpointGutter(): void { |
| this.codeMirror!.clearGutter('cm-breakpoints'); |
| this.player.getLineNumbersReached().forEach((timesReached: number, line: number) => { |
| this.codeMirror!.setGutterMarker(line - 1, 'cm-breakpoints', |
| DebuggerAppSk.makeDivWithClass('cm-reachable')); |
| }); |
| } |
| |
| toggleBreakpoint(line: number): void { |
| // The line number is 1-indexed. |
| if (this.player.getBreakpoints().has(line)) { |
| this.player.removeBreakpoint(line); |
| this.codeMirror!.setGutterMarker(line - 1, 'cm-breakpoints', |
| DebuggerAppSk.makeDivWithClass('cm-reachable')); |
| } else if (this.player.getLineNumbersReached().has(line)) { |
| this.player.addBreakpoint(line); |
| this.codeMirror!.setGutterMarker(line - 1, 'cm-breakpoints', |
| DebuggerAppSk.makeDivWithClass('cm-breakpoint')); |
| } else { |
| // Don't allow breakpoints to be set on unreachable lines. |
| } |
| |
| this._render(); |
| } |
| |
| private static template = (self: DebuggerAppSk): TemplateResult => html` |
| <div id="drag-area"> |
| <header> |
| <h2>SkSL Debugger</h2> |
| <span> |
| <a |
| id="githash" |
| href="https://skia.googlesource.com/skia/+show/${SKIA_VERSION}" |
| > |
| ${SKIA_VERSION.slice(0, 7)} |
| </a> |
| <theme-chooser-sk></theme-chooser-sk> |
| </span> |
| </header> |
| <main> |
| <div id=debuggerControls> |
| <span id=buttonGroup> |
| <button ?disabled=${self.trace === null} |
| @click=${self.resetTrace} |
| class=action> |
| Reset |
| </button> |
| </span> |
| <span id=buttonGroup> |
| <button ?disabled=${self.trace === null} |
| @click=${self.stepOver} |
| class=action> |
| Step |
| </button> |
| <button ?disabled=${self.trace === null} |
| @click=${self.step} |
| class=action> |
| Step In |
| </button> |
| <button ?disabled=${self.trace === null} |
| @click=${self.stepOut} |
| class=action> |
| Step Out |
| </button> |
| </span> |
| <span id=buttonGroup> |
| <button ?disabled=${self.trace === null} |
| @click=${self.run} |
| class=action> |
| ${self.player.getBreakpoints().size > 0 ? 'Run to Breakpoint' : 'Run'} |
| </button> |
| </span> |
| </div> |
| <br> |
| <div id="debuggerPane"> |
| <div id="codeEditor"></div> |
| <div id="debuggerTables"> |
| <table> |
| <thead><th>Stack</th></thead> |
| <tbody>${self.stackDisplay()}</tbody> |
| </table> |
| <table> |
| <thead><th colspan=2>Variables</th></thead> |
| <tbody>${self.varsDisplay()}</tbody> |
| </table> |
| </div> |
| </div> |
| </main> |
| </div> |
| `; |
| } |
| |
| define('debugger-app-sk', DebuggerAppSk); |