Mypal/devtools/client/sourceeditor/debugger.js

337 lines
8.2 KiB
JavaScript

/* 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;
});