mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
563 lines
17 KiB
JavaScript
563 lines
17 KiB
JavaScript
/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
|
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is mozilla.org code.
|
|
*
|
|
* The Initial Developer of the Original Code is
|
|
* Mozilla Foundation
|
|
* Portions created by the Initial Developer are Copyright (C) 2011
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Dave Camp <dcamp@mozilla.com>
|
|
* Panos Astithas <past@mozilla.com>
|
|
* Victor Porof <vporof@mozilla.com>
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
"use strict";
|
|
|
|
Components.utils.import("resource:///modules/source-editor.jsm");
|
|
|
|
var gInitialized = false;
|
|
var gClient = null;
|
|
var gTabClient = null;
|
|
|
|
|
|
function initDebugger()
|
|
{
|
|
window.removeEventListener("DOMContentLoaded", initDebugger, false);
|
|
if (gInitialized) {
|
|
return;
|
|
}
|
|
gInitialized = true;
|
|
|
|
DebuggerView.Stackframes.initialize();
|
|
DebuggerView.Properties.initialize();
|
|
DebuggerView.Scripts.initialize();
|
|
}
|
|
|
|
/**
|
|
* Called by chrome to set up a debugging session.
|
|
*
|
|
* @param DebuggerClient aClient
|
|
* The debugger client.
|
|
* @param object aTabGrip
|
|
* The remote protocol grip of the tab.
|
|
*/
|
|
function startDebuggingTab(aClient, aTabGrip)
|
|
{
|
|
gClient = aClient;
|
|
|
|
gClient.attachTab(aTabGrip.actor, function(aResponse, aTabClient) {
|
|
if (aTabClient) {
|
|
gTabClient = aTabClient;
|
|
gClient.attachThread(aResponse.threadActor, function(aResponse, aThreadClient) {
|
|
if (!aThreadClient) {
|
|
dump("Couldn't attach to thread: "+aResponse.error+"\n");
|
|
return;
|
|
}
|
|
ThreadState.connect(aThreadClient, function() {
|
|
StackFrames.connect(aThreadClient, function() {
|
|
SourceScripts.connect(aThreadClient, function() {
|
|
aThreadClient.resume();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function shutdownDebugger()
|
|
{
|
|
window.removeEventListener("unload", shutdownDebugger, false);
|
|
|
|
SourceScripts.disconnect();
|
|
StackFrames.disconnect();
|
|
ThreadState.disconnect();
|
|
ThreadState.activeThread = false;
|
|
|
|
DebuggerView.Stackframes.destroy();
|
|
DebuggerView.Properties.destroy();
|
|
DebuggerView.Scripts.destroy();
|
|
}
|
|
|
|
|
|
/**
|
|
* ThreadState keeps the UI up to date with the state of the
|
|
* thread (paused/attached/etc.).
|
|
*/
|
|
var ThreadState = {
|
|
activeThread: null,
|
|
|
|
/**
|
|
* Connect to a thread client.
|
|
* @param object aThreadClient
|
|
* The thread client.
|
|
* @param function aCallback
|
|
* The next function in the initialization sequence.
|
|
*/
|
|
connect: function TS_connect(aThreadClient, aCallback) {
|
|
this.activeThread = aThreadClient;
|
|
aThreadClient.addListener("paused", ThreadState.update);
|
|
aThreadClient.addListener("resumed", ThreadState.update);
|
|
aThreadClient.addListener("detached", ThreadState.update);
|
|
this.update();
|
|
aCallback && aCallback();
|
|
},
|
|
|
|
/**
|
|
* Update the UI after a thread state change.
|
|
*/
|
|
update: function TS_update(aEvent) {
|
|
DebuggerView.Stackframes.updateState(this.activeThread.state);
|
|
if (aEvent == "detached") {
|
|
ThreadState.activeThread = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Disconnect from the client.
|
|
*/
|
|
disconnect: function TS_disconnect() {
|
|
this.activeThread.removeListener("paused", ThreadState.update);
|
|
this.activeThread.removeListener("resumed", ThreadState.update);
|
|
this.activeThread.removeListener("detached", ThreadState.update);
|
|
}
|
|
};
|
|
|
|
ThreadState.update = ThreadState.update.bind(ThreadState);
|
|
|
|
/**
|
|
* Keeps the stack frame list up-to-date, using the thread client's
|
|
* stack frame cache.
|
|
*/
|
|
var StackFrames = {
|
|
pageSize: 25,
|
|
activeThread: null,
|
|
selectedFrame: null,
|
|
|
|
/**
|
|
* Watch a given thread client.
|
|
* @param object aThreadClient
|
|
* The thread client.
|
|
* @param function aCallback
|
|
* The next function in the initialization sequence.
|
|
*/
|
|
connect: function SF_connect(aThreadClient, aCallback) {
|
|
DebuggerView.Stackframes.addClickListener(this.onClick);
|
|
|
|
this.activeThread = aThreadClient;
|
|
aThreadClient.addListener("paused", this.onPaused);
|
|
aThreadClient.addListener("framesadded", this.onFrames);
|
|
aThreadClient.addListener("framescleared", this.onFramesCleared);
|
|
this.onFramesCleared();
|
|
aCallback && aCallback();
|
|
},
|
|
|
|
/**
|
|
* Disconnect from the client.
|
|
*/
|
|
disconnect: function TS_disconnect() {
|
|
this.activeThread.removeListener("paused", this.onPaused);
|
|
this.activeThread.removeListener("framesadded", this.onFrames);
|
|
this.activeThread.removeListener("framescleared", this.onFramesCleared);
|
|
},
|
|
|
|
/**
|
|
* Handler for the thread client's paused notification.
|
|
*/
|
|
onPaused: function SF_onPaused() {
|
|
this.activeThread.fillFrames(this.pageSize);
|
|
},
|
|
|
|
/**
|
|
* Handler for the thread client's framesadded notification.
|
|
*/
|
|
onFrames: function SF_onFrames() {
|
|
DebuggerView.Stackframes.empty();
|
|
|
|
for each (let frame in this.activeThread.cachedFrames) {
|
|
this._addFramePanel(frame);
|
|
}
|
|
|
|
if (this.activeThread.moreFrames) {
|
|
DebuggerView.Stackframes.dirty = true;
|
|
}
|
|
|
|
if (!this.selectedFrame) {
|
|
this.selectFrame(0);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Handler for the thread client's framescleared notification.
|
|
*/
|
|
onFramesCleared: function SF_onFramesCleared() {
|
|
DebuggerView.Stackframes.emptyText();
|
|
this.selectedFrame = null;
|
|
// Clear the properties as well.
|
|
DebuggerView.Properties.localScope.empty();
|
|
DebuggerView.Properties.globalScope.empty();
|
|
},
|
|
|
|
/**
|
|
* Event handler for clicks on stack frames.
|
|
*/
|
|
onClick: function SF_onClick(aEvent) {
|
|
let target = aEvent.target;
|
|
while (target) {
|
|
if (target.stackFrame) {
|
|
this.selectFrame(target.stackFrame.depth);
|
|
return;
|
|
}
|
|
target = target.parentNode;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Marks the stack frame in the specified depth as selected and updates the
|
|
* properties view with the stack frame's data.
|
|
*
|
|
* @param number aDepth
|
|
* The depth of the frame in the stack.
|
|
*/
|
|
selectFrame: function SF_selectFrame(aDepth) {
|
|
if (this.selectedFrame !== null) {
|
|
DebuggerView.Stackframes.highlightFrame(this.selectedFrame, false);
|
|
}
|
|
|
|
this.selectedFrame = aDepth;
|
|
if (aDepth !== null) {
|
|
DebuggerView.Stackframes.highlightFrame(this.selectedFrame, true);
|
|
}
|
|
|
|
let frame = this.activeThread.cachedFrames[aDepth];
|
|
if (!frame) {
|
|
return;
|
|
}
|
|
// Move the editor's caret to the proper line.
|
|
if (DebuggerView.Scripts.isSelected(frame.where.url) && frame.where.line) {
|
|
window.editor.setCaretPosition(frame.where.line - 1);
|
|
} else if (DebuggerView.Scripts.contains(frame.where.url)) {
|
|
DebuggerView.Scripts.selectScript(frame.where.url);
|
|
SourceScripts.onChange({ target: DebuggerView.Scripts._scripts });
|
|
window.editor.setCaretPosition(frame.where.line - 1);
|
|
}
|
|
// Display the local variables.
|
|
let localScope = DebuggerView.Properties.localScope;
|
|
localScope.empty();
|
|
// Add "this".
|
|
if (frame["this"]) {
|
|
let thisVar = localScope.addVar("this");
|
|
thisVar.setGrip({ "type": frame["this"].type,
|
|
"class": frame["this"].class });
|
|
this._addExpander(thisVar, frame["this"]);
|
|
}
|
|
|
|
if (frame.arguments && frame.arguments.length > 0) {
|
|
// Add "arguments".
|
|
let argsVar = localScope.addVar("arguments");
|
|
argsVar.setGrip({ "type": "object", "class": "Arguments" });
|
|
this._addExpander(argsVar, frame.arguments);
|
|
|
|
// Add variables for every argument.
|
|
let objClient = this.activeThread.pauseGrip(frame.callee);
|
|
objClient.getSignature(function SF_getSignature(aResponse) {
|
|
for (let i = 0; i < aResponse.parameters.length; i++) {
|
|
let param = aResponse.parameters[i];
|
|
let paramVar = localScope.addVar(param);
|
|
let paramVal = frame.arguments[i];
|
|
paramVar.setGrip(paramVal);
|
|
this._addExpander(paramVar, paramVal);
|
|
}
|
|
}.bind(this));
|
|
}
|
|
},
|
|
|
|
_addExpander: function SF_addExpander(aVar, aObject) {
|
|
// No need for expansion for null and undefined values, but we do need them
|
|
// for frame.arguments which is a regular array.
|
|
if (!aObject || typeof aObject != "object" ||
|
|
(aObject.type != "object" && !Array.isArray(aObject))) {
|
|
return;
|
|
}
|
|
// Add a dummy property to force the twisty to show up.
|
|
aVar.addProperties({ " ": { value: " " }});
|
|
aVar.onexpand = this._addVarProperties.bind(this, aVar, aObject);
|
|
},
|
|
|
|
_addVarProperties: function SF_addVarProperties(aVar, aObject) {
|
|
// Retrieve the properties only once.
|
|
if (aVar.fetched) {
|
|
return;
|
|
}
|
|
// Clear the placeholder property put in place to display the twisty.
|
|
aVar.empty();
|
|
|
|
// For arrays we have to construct a grip-like object to pass into
|
|
// addProperties.
|
|
if (Array.isArray(aObject)) {
|
|
let properties = { length: { writable: true, value: aObject.length } };
|
|
for (let i = 0; i < aObject.length; i++) {
|
|
properties[i + ""] = { value: aObject[i] };
|
|
}
|
|
aVar.addProperties(properties);
|
|
// Expansion handlers must be set after the properties are added.
|
|
for (let i = 0; i < aObject.length; i++) {
|
|
this._addExpander(aVar[i + ""], aObject[i]);
|
|
}
|
|
aVar.fetched = true;
|
|
return;
|
|
}
|
|
|
|
let objClient = this.activeThread.pauseGrip(aObject);
|
|
objClient.getPrototypeAndProperties(function SF_onProtoAndProps(aResponse) {
|
|
// Add __proto__.
|
|
if (aResponse.prototype.type != "null") {
|
|
let properties = {};
|
|
properties["__proto__ "] = { value: aResponse.prototype };
|
|
aVar.addProperties(properties);
|
|
// Expansion handlers must be set after the properties are added.
|
|
this._addExpander(aVar["__proto__ "], aResponse.prototype);
|
|
}
|
|
|
|
// Sort the rest of the properties before adding them, for better UX.
|
|
let properties = {};
|
|
for each (let prop in Object.keys(aResponse.ownProperties).sort()) {
|
|
properties[prop] = aResponse.ownProperties[prop];
|
|
}
|
|
aVar.addProperties(properties);
|
|
// Expansion handlers must be set after the properties are added.
|
|
for (let prop in aResponse.ownProperties) {
|
|
this._addExpander(aVar[prop], aResponse.ownProperties[prop].value);
|
|
}
|
|
|
|
aVar.fetched = true;
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
* Adds the specified stack frame to the list.
|
|
*
|
|
* @param Debugger.Frame aFrame
|
|
* The new frame to add.
|
|
*/
|
|
_addFramePanel: function SF_addFramePanel(aFrame) {
|
|
let depth = aFrame.depth;
|
|
let idText = "#" + aFrame.depth + " ";
|
|
let nameText = this._frameTitle(aFrame);
|
|
|
|
let panel = DebuggerView.Stackframes.addFrame(depth, idText, nameText);
|
|
|
|
if (panel) {
|
|
panel.stackFrame = aFrame;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Loads more stack frames from the debugger server cache.
|
|
*/
|
|
_addMoreFrames: function SF_addMoreFrames() {
|
|
this.activeThread.fillFrames(
|
|
this.activeThread.cachedFrames.length + this.pageSize);
|
|
},
|
|
|
|
/**
|
|
* Create a textual representation for the stack frame specified, for
|
|
* displaying in the stack frame list.
|
|
*
|
|
* @param Debugger.Frame aFrame
|
|
* The stack frame to label.
|
|
*/
|
|
_frameTitle: function SF_frameTitle(aFrame) {
|
|
if (aFrame.type == "call") {
|
|
return aFrame["calleeName"] ? aFrame["calleeName"] + "()" : "(anonymous)";
|
|
}
|
|
|
|
return "(" + aFrame.type + ")";
|
|
}
|
|
};
|
|
|
|
StackFrames.onPaused = StackFrames.onPaused.bind(StackFrames);
|
|
StackFrames.onFrames = StackFrames.onFrames.bind(StackFrames);
|
|
StackFrames.onFramesCleared = StackFrames.onFramesCleared.bind(StackFrames);
|
|
StackFrames.onClick = StackFrames.onClick.bind(StackFrames);
|
|
|
|
/**
|
|
* Keeps the source script list up-to-date, using the thread client's
|
|
* source script cache.
|
|
*/
|
|
var SourceScripts = {
|
|
pageSize: 25,
|
|
activeThread: null,
|
|
|
|
/**
|
|
* Watch a given thread client.
|
|
* @param object aThreadClient
|
|
* The thread client.
|
|
* @param function aCallback
|
|
* The next function in the initialization sequence.
|
|
*/
|
|
connect: function SS_connect(aThreadClient, aCallback) {
|
|
DebuggerView.Scripts.addChangeListener(this.onChange);
|
|
|
|
this.activeThread = aThreadClient;
|
|
aThreadClient.addListener("paused", this.onPaused);
|
|
aThreadClient.addListener("scriptsadded", this.onScripts);
|
|
aThreadClient.addListener("scriptscleared", this.onScriptsCleared);
|
|
this.onScriptsCleared();
|
|
aCallback && aCallback();
|
|
},
|
|
|
|
/**
|
|
* Disconnect from the client.
|
|
*/
|
|
disconnect: function TS_disconnect() {
|
|
this.activeThread.removeListener("paused", this.onPaused);
|
|
this.activeThread.removeListener("scriptsadded", this.onScripts);
|
|
this.activeThread.removeListener("scriptscleared", this.onScriptsCleared);
|
|
},
|
|
|
|
/**
|
|
* Handler for the thread client's paused notification. This is triggered only
|
|
* once, to retrieve the list of scripts known to the server from before the
|
|
* client was ready to handle new script notifications.
|
|
*/
|
|
onPaused: function SS_onPaused() {
|
|
this.activeThread.removeListener("paused", this.onPaused);
|
|
this.activeThread.fillScripts();
|
|
},
|
|
|
|
/**
|
|
* Handler for the debugger client's unsolicited newScript notification.
|
|
*/
|
|
onNewScript: function SS_onNewScript(aNotification, aPacket) {
|
|
this._addScript({ url: aPacket.url, startLine: aPacket.startLine });
|
|
},
|
|
|
|
/**
|
|
* Handler for the thread client's scriptsadded notification.
|
|
*/
|
|
onScripts: function SS_onScripts() {
|
|
for each (let script in this.activeThread.cachedScripts) {
|
|
this._addScript(script);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Handler for the thread client's scriptscleared notification.
|
|
*/
|
|
onScriptsCleared: function SS_onScriptsCleared() {
|
|
DebuggerView.Scripts.empty();
|
|
},
|
|
|
|
/**
|
|
* Handler for changes on the selected source script.
|
|
*/
|
|
onChange: function SS_onChange(aEvent) {
|
|
let scripts = aEvent.target;
|
|
if (!scripts.selectedItem) {
|
|
return;
|
|
}
|
|
let script = scripts.selectedItem.getUserData("sourceScript");
|
|
this.setEditorMode(script.url, script.contentType);
|
|
this._showScript(script);
|
|
},
|
|
|
|
/**
|
|
* Sets the proper editor mode (JS or HTML) according to the specified
|
|
* content type, or by determining the type from the URL.
|
|
*
|
|
* @param string aUrl
|
|
* The script URL.
|
|
* @param string aContentType [optional]
|
|
* The script content type.
|
|
*/
|
|
setEditorMode: function SS_setEditorMode(aUrl, aContentType) {
|
|
if (aContentType) {
|
|
if (/javascript/.test(aContentType)) {
|
|
window.editor.setMode(SourceEditor.MODES.JAVASCRIPT);
|
|
} else {
|
|
window.editor.setMode(SourceEditor.MODES.HTML);
|
|
}
|
|
return;
|
|
}
|
|
|
|
let url = aUrl;
|
|
// Trim the query part.
|
|
let q = url.indexOf('?');
|
|
if (q > -1) {
|
|
url = url.slice(0, q);
|
|
}
|
|
|
|
if (url.slice(-3) == ".js") {
|
|
window.editor.setMode(SourceEditor.MODES.JAVASCRIPT);
|
|
} else {
|
|
window.editor.setMode(SourceEditor.MODES.HTML);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Add the specified script to the list and display it in the editor if the
|
|
* editor is empty.
|
|
*/
|
|
_addScript: function SS_addScript(aScript) {
|
|
DebuggerView.Scripts.addScript(aScript.url, aScript);
|
|
|
|
if (window.editor.getCharCount() == 0) {
|
|
this._showScript(aScript);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load the editor with the script text if available, otherwise fire an event
|
|
* to load and display the script text.
|
|
*/
|
|
_showScript: function SS_showScript(aScript) {
|
|
if (!aScript.loaded) {
|
|
// Notify the chrome code that we need to load a script file.
|
|
var evt = document.createEvent("CustomEvent");
|
|
evt.initCustomEvent("Debugger:LoadSource", true, false, aScript.url);
|
|
document.documentElement.dispatchEvent(evt);
|
|
window.editor.setText(DebuggerView.getStr("loadingText"));
|
|
} else {
|
|
window.editor.setText(aScript.text);
|
|
}
|
|
}
|
|
};
|
|
|
|
SourceScripts.onPaused = SourceScripts.onPaused.bind(SourceScripts);
|
|
SourceScripts.onScripts = SourceScripts.onScripts.bind(SourceScripts);
|
|
SourceScripts.onNewScript = SourceScripts.onNewScript.bind(SourceScripts);
|
|
SourceScripts.onScriptsCleared = SourceScripts.onScriptsCleared.bind(SourceScripts);
|
|
SourceScripts.onChange = SourceScripts.onChange.bind(SourceScripts);
|
|
|
|
window.addEventListener("DOMContentLoaded", initDebugger, false);
|
|
window.addEventListener("unload", shutdownDebugger, false);
|