diff options
Diffstat (limited to 'devtools/client/sourceeditor/debugger.js')
-rw-r--r-- | devtools/client/sourceeditor/debugger.js | 336 |
1 files changed, 336 insertions, 0 deletions
diff --git a/devtools/client/sourceeditor/debugger.js b/devtools/client/sourceeditor/debugger.js new file mode 100644 index 000000000..de63962a6 --- /dev/null +++ b/devtools/client/sourceeditor/debugger.js @@ -0,0 +1,336 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); +const promise = require("promise"); +const dbginfo = new WeakMap(); + +// These functions implement search within the debugger. Since +// search in the debugger is different from other components, +// we can't use search.js CodeMirror addon. This is a slightly +// modified version of that addon. Depends on searchcursor.js. + +function SearchState() { + this.posFrom = this.posTo = this.query = null; +} + +function getSearchState(cm) { + return cm.state.search || (cm.state.search = new SearchState()); +} + +function getSearchCursor(cm, query, pos) { + // If the query string is all lowercase, do a case insensitive search. + return cm.getSearchCursor(query, pos, + typeof query == "string" && query == query.toLowerCase()); +} + +/** + * If there's a saved search, selects the next results. + * Otherwise, creates a new search and selects the first + * result. + */ +function doSearch(ctx, rev, query) { + let { cm } = ctx; + let state = getSearchState(cm); + + if (state.query) { + searchNext(ctx, rev); + return; + } + + cm.operation(function () { + if (state.query) { + return; + } + + state.query = query; + state.posFrom = state.posTo = { line: 0, ch: 0 }; + searchNext(ctx, rev); + }); +} + +/** + * Selects the next result of a saved search. + */ +function searchNext(ctx, rev) { + let { cm, ed } = ctx; + cm.operation(function () { + let state = getSearchState(cm); + let cursor = getSearchCursor(cm, state.query, + rev ? state.posFrom : state.posTo); + + if (!cursor.find(rev)) { + cursor = getSearchCursor(cm, state.query, rev ? + { line: cm.lastLine(), ch: null } : { line: cm.firstLine(), ch: 0 }); + if (!cursor.find(rev)) { + return; + } + } + + ed.alignLine(cursor.from().line, "center"); + cm.setSelection(cursor.from(), cursor.to()); + state.posFrom = cursor.from(); + state.posTo = cursor.to(); + }); +} + +/** + * Clears the currently saved search. + */ +function clearSearch(cm) { + let state = getSearchState(cm); + + if (!state.query) { + return; + } + + state.query = null; +} + +// Exported functions + +/** + * This function is called whenever Editor is extended with functions + * from this module. See Editor.extend for more info. + */ +function initialize(ctx) { + let { ed } = ctx; + + dbginfo.set(ed, { + breakpoints: {}, + debugLocation: null + }); +} + +/** + * True if editor has a visual breakpoint at that line, false + * otherwise. + */ +function hasBreakpoint(ctx, line) { + let { cm } = ctx; + // In some rare occasions CodeMirror might not be properly initialized yet, so + // return an exceptional value in that case. + if (cm.lineInfo(line) === null) { + return null; + } + let markers = cm.lineInfo(line).wrapClass; + + return markers != null && + markers.includes("breakpoint"); +} + +/** + * Adds a visual breakpoint for a specified line. Third + * parameter 'cond' can hold any object. + * + * After adding a breakpoint, this function makes Editor to + * emit a breakpointAdded event. + */ +function addBreakpoint(ctx, line, cond) { + function _addBreakpoint() { + let { ed, cm } = ctx; + let meta = dbginfo.get(ed); + let info = cm.lineInfo(line); + + // The line does not exist in the editor. This is harmless, the + // architecture calling this assumes the editor will handle this + // gracefully, and make sure breakpoints exist when they need to. + if (!info) { + return; + } + + ed.addLineClass(line, "breakpoint"); + meta.breakpoints[line] = { condition: cond }; + + // TODO(jwl): why is `info` null when breaking on page reload? + info.handle.on("delete", function onDelete() { + info.handle.off("delete", onDelete); + meta.breakpoints[info.line] = null; + }); + + if (cond) { + setBreakpointCondition(ctx, line); + } + ed.emit("breakpointAdded", line); + deferred.resolve(); + } + + if (hasBreakpoint(ctx, line)) { + return null; + } + + let deferred = promise.defer(); + // If lineInfo() returns null, wait a tick to give the editor a chance to + // initialize properly. + if (ctx.cm.lineInfo(line) === null) { + DevToolsUtils.executeSoon(() => _addBreakpoint()); + } else { + _addBreakpoint(); + } + return deferred.promise; +} + +/** + * Helps reset the debugger's breakpoint state + * - removes the breakpoints in the editor + * - cleares the debugger's breakpoint state + * + * Note, does not *actually* remove a source's breakpoints. + * The canonical state is kept in the app state. + * + */ +function removeBreakpoints(ctx) { + let { ed, cm } = ctx; + + let meta = dbginfo.get(ed); + if (meta.breakpoints != null) { + meta.breakpoints = {}; + } + + cm.doc.iter((line) => { + // The hasBreakpoint is a slow operation: checks the line type, whether cm + // is initialized and creates several new objects. Inlining the line's + // wrapClass property check directly. + if (line.wrapClass == null || !line.wrapClass.includes("breakpoint")) { + return; + } + removeBreakpoint(ctx, line); + }); +} + +/** + * Removes a visual breakpoint from a specified line and + * makes Editor emit a breakpointRemoved event. + */ +function removeBreakpoint(ctx, line) { + if (!hasBreakpoint(ctx, line)) { + return; + } + + let { ed, cm } = ctx; + let meta = dbginfo.get(ed); + let info = cm.lineInfo(line); + + meta.breakpoints[info.line] = null; + ed.removeLineClass(info.line, "breakpoint"); + ed.removeLineClass(info.line, "conditional"); + ed.emit("breakpointRemoved", line); +} + +function moveBreakpoint(ctx, fromLine, toLine) { + let { ed } = ctx; + + ed.removeBreakpoint(fromLine); + ed.addBreakpoint(toLine); +} + +function setBreakpointCondition(ctx, line) { + let { ed, cm } = ctx; + let info = cm.lineInfo(line); + + // The line does not exist in the editor. This is harmless, the + // architecture calling this assumes the editor will handle this + // gracefully, and make sure breakpoints exist when they need to. + if (!info) { + return; + } + + ed.addLineClass(line, "conditional"); +} + +function removeBreakpointCondition(ctx, line) { + let { ed } = ctx; + + ed.removeLineClass(line, "conditional"); +} + +/** + * Returns a list of all breakpoints in the current Editor. + */ +function getBreakpoints(ctx) { + let { ed } = ctx; + let meta = dbginfo.get(ed); + + return Object.keys(meta.breakpoints).reduce((acc, line) => { + if (meta.breakpoints[line] != null) { + acc.push({ line: line, condition: meta.breakpoints[line].condition }); + } + return acc; + }, []); +} + +/** + * Saves a debug location information and adds a visual anchor to + * the breakpoints gutter. This is used by the debugger UI to + * display the line on which the Debugger is currently paused. + */ +function setDebugLocation(ctx, line) { + let { ed } = ctx; + let meta = dbginfo.get(ed); + + clearDebugLocation(ctx); + + meta.debugLocation = line; + ed.addLineClass(line, "debug-line"); +} + +/** + * Returns a line number that corresponds to the current debug + * location. + */ +function getDebugLocation(ctx) { + let { ed } = ctx; + let meta = dbginfo.get(ed); + + return meta.debugLocation; +} + +/** + * Clears the debug location. Clearing the debug location + * also removes a visual anchor from the breakpoints gutter. + */ +function clearDebugLocation(ctx) { + let { ed } = ctx; + let meta = dbginfo.get(ed); + + if (meta.debugLocation != null) { + ed.removeLineClass(meta.debugLocation, "debug-line"); + meta.debugLocation = null; + } +} + +/** + * Starts a new search. + */ +function find(ctx, query) { + clearSearch(ctx.cm); + doSearch(ctx, false, query); +} + +/** + * Finds the next item based on the currently saved search. + */ +function findNext(ctx, query) { + doSearch(ctx, false, query); +} + +/** + * Finds the previous item based on the currently saved search. + */ +function findPrev(ctx, query) { + doSearch(ctx, true, query); +} + +// Export functions + +[ + initialize, hasBreakpoint, addBreakpoint, removeBreakpoint, moveBreakpoint, + setBreakpointCondition, removeBreakpointCondition, getBreakpoints, removeBreakpoints, + setDebugLocation, getDebugLocation, clearDebugLocation, find, findNext, + findPrev +].forEach(func => { + module.exports[func.name] = func; +}); |