summaryrefslogtreecommitdiffstats
path: root/devtools/client/sourceeditor/debugger.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/sourceeditor/debugger.js')
-rw-r--r--devtools/client/sourceeditor/debugger.js336
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;
+});