mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-07 04:26:32 +08:00
This adds an `icon()` method to `ScopeChainEntry`, and extends the `ScopeChainSideparPane` with an optional `<img>` element that shows this icon. This implements the recommended UX improvements to make it easier to differentiate between the scopes themself and the items listed in the scopes. It is implemented via a new method `getScopeInfo()` method in the language plugin extension API that returns the scope type, a user visible type name, and the optional icon. Before: https://imgur.com/a/xIvhdM4 After: https://imgur.com/a/6WHoceO Bug: chromium:1136484 Change-Id: I5927e1502bc88b0d802aca5dcbc220df8d7c7f5d Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2461769 Commit-Queue: Benedikt Meurer <bmeurer@chromium.org> Reviewed-by: Peter Müller <petermueller@chromium.org> Reviewed-by: Philip Pfaffe <pfaffe@chromium.org>
942 lines
29 KiB
JavaScript
942 lines
29 KiB
JavaScript
/*
|
|
* Copyright (C) 2012 Google Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are
|
|
* met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above
|
|
* copyright notice, this list of conditions and the following disclaimer
|
|
* in the documentation and/or other materials provided with the
|
|
* distribution.
|
|
* * Neither the name of Google Inc. nor the names of its
|
|
* contributors may be used to endorse or promote products derived from
|
|
* this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// @ts-nocheck
|
|
// TODO(crbug.com/1011811): Enable TypeScript compiler checks
|
|
|
|
export function defineCommonExtensionSymbols(apiPrivate) {
|
|
if (!apiPrivate.panels) {
|
|
apiPrivate.panels = {};
|
|
}
|
|
apiPrivate.panels.SearchAction = {
|
|
CancelSearch: 'cancelSearch',
|
|
PerformSearch: 'performSearch',
|
|
NextSearchResult: 'nextSearchResult',
|
|
PreviousSearchResult: 'previousSearchResult'
|
|
};
|
|
|
|
/** @enum {string} */
|
|
apiPrivate.Events = {
|
|
ButtonClicked: 'button-clicked-',
|
|
PanelObjectSelected: 'panel-objectSelected-',
|
|
NetworkRequestFinished: 'network-request-finished',
|
|
OpenResource: 'open-resource',
|
|
PanelSearch: 'panel-search-',
|
|
RecordingStarted: 'trace-recording-started-',
|
|
RecordingStopped: 'trace-recording-stopped-',
|
|
ResourceAdded: 'resource-added',
|
|
ResourceContentCommitted: 'resource-content-committed',
|
|
ViewShown: 'view-shown-',
|
|
ViewHidden: 'view-hidden-'
|
|
};
|
|
|
|
/** @enum {string} */
|
|
apiPrivate.Commands = {
|
|
AddRequestHeaders: 'addRequestHeaders',
|
|
AddTraceProvider: 'addTraceProvider',
|
|
ApplyStyleSheet: 'applyStyleSheet',
|
|
CompleteTraceSession: 'completeTraceSession',
|
|
CreatePanel: 'createPanel',
|
|
CreateSidebarPane: 'createSidebarPane',
|
|
CreateToolbarButton: 'createToolbarButton',
|
|
EvaluateOnInspectedPage: 'evaluateOnInspectedPage',
|
|
ForwardKeyboardEvent: '_forwardKeyboardEvent',
|
|
GetHAR: 'getHAR',
|
|
GetPageResources: 'getPageResources',
|
|
GetRequestContent: 'getRequestContent',
|
|
GetResourceContent: 'getResourceContent',
|
|
InspectedURLChanged: 'inspectedURLChanged',
|
|
OpenResource: 'openResource',
|
|
Reload: 'Reload',
|
|
Subscribe: 'subscribe',
|
|
SetOpenResourceHandler: 'setOpenResourceHandler',
|
|
SetResourceContent: 'setResourceContent',
|
|
SetSidebarContent: 'setSidebarContent',
|
|
SetSidebarHeight: 'setSidebarHeight',
|
|
SetSidebarPage: 'setSidebarPage',
|
|
ShowPanel: 'showPanel',
|
|
Unsubscribe: 'unsubscribe',
|
|
UpdateButton: 'updateButton',
|
|
RegisterLanguageExtensionPlugin: 'registerLanguageExtensionPlugin'
|
|
};
|
|
|
|
/** @enum {string} */
|
|
apiPrivate.LanguageExtensionPluginCommands = {
|
|
AddRawModule: 'addRawModule',
|
|
RemoveRawModule: 'removeRawModule',
|
|
SourceLocationToRawLocation: 'sourceLocationToRawLocation',
|
|
RawLocationToSourceLocation: 'rawLocationToSourceLocation',
|
|
GetScopeInfo: 'getScopeInfo',
|
|
ListVariablesInScope: 'listVariablesInScope',
|
|
EvaluateVariable: 'evaluateVariable',
|
|
GetFunctionInfo: 'getFunctionInfo'
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {!ExtensionDescriptor} extensionInfo
|
|
* @param {string} inspectedTabId
|
|
* @param {string} themeName
|
|
* @param {!Array<number>} keysToForward
|
|
* @param {number} injectedScriptId
|
|
* @param {function(!Object, !Object)} testHook
|
|
* @suppressGlobalPropertiesCheck
|
|
*/
|
|
self.injectedExtensionAPI = function(
|
|
extensionInfo, inspectedTabId, themeName, keysToForward, testHook, injectedScriptId) {
|
|
const keysToForwardSet = new Set(keysToForward);
|
|
const chrome = window.chrome || {};
|
|
const devtools_descriptor = Object.getOwnPropertyDescriptor(chrome, 'devtools');
|
|
if (devtools_descriptor) {
|
|
return;
|
|
}
|
|
|
|
const apiPrivate = {};
|
|
|
|
defineCommonExtensionSymbols(apiPrivate);
|
|
|
|
const commands = apiPrivate.Commands;
|
|
const languageExtensionPluginCommands = apiPrivate.LanguageExtensionPluginCommands;
|
|
const events = apiPrivate.Events;
|
|
let userAction = false;
|
|
|
|
// Here and below, all constructors are private to API implementation.
|
|
// For a public type Foo, if internal fields are present, these are on
|
|
// a private FooImpl type, an instance of FooImpl is used in a closure
|
|
// by Foo consutrctor to re-bind publicly exported members to an instance
|
|
// of Foo.
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
function EventSinkImpl(type, customDispatch) {
|
|
this._type = type;
|
|
this._listeners = [];
|
|
this._customDispatch = customDispatch;
|
|
}
|
|
|
|
EventSinkImpl.prototype = {
|
|
addListener: function(callback) {
|
|
if (typeof callback !== 'function') {
|
|
throw 'addListener: callback is not a function';
|
|
}
|
|
if (this._listeners.length === 0) {
|
|
extensionServer.sendRequest({command: commands.Subscribe, type: this._type});
|
|
}
|
|
this._listeners.push(callback);
|
|
extensionServer.registerHandler('notify-' + this._type, this._dispatch.bind(this));
|
|
},
|
|
|
|
removeListener: function(callback) {
|
|
const listeners = this._listeners;
|
|
|
|
for (let i = 0; i < listeners.length; ++i) {
|
|
if (listeners[i] === callback) {
|
|
listeners.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
if (this._listeners.length === 0) {
|
|
extensionServer.sendRequest({command: commands.Unsubscribe, type: this._type});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {...} vararg
|
|
*/
|
|
_fire: function(vararg) {
|
|
const listeners = this._listeners.slice();
|
|
for (let i = 0; i < listeners.length; ++i) {
|
|
listeners[i].apply(null, arguments);
|
|
}
|
|
},
|
|
|
|
_dispatch: function(request) {
|
|
if (this._customDispatch) {
|
|
this._customDispatch.call(this, request);
|
|
} else {
|
|
this._fire.apply(this, request.arguments);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
function InspectorExtensionAPI() {
|
|
this.inspectedWindow = new InspectedWindow();
|
|
this.panels = new Panels();
|
|
this.network = new Network();
|
|
this.timeline = new Timeline();
|
|
this.languageServices = new LanguageServicesAPI();
|
|
defineDeprecatedProperty(this, 'webInspector', 'resources', 'network');
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
function Network() {
|
|
/**
|
|
* @this {EventSinkImpl}
|
|
*/
|
|
function dispatchRequestEvent(message) {
|
|
const request = message.arguments[1];
|
|
request.__proto__ = new Request(message.arguments[0]);
|
|
this._fire(request);
|
|
}
|
|
this.onRequestFinished = new EventSink(events.NetworkRequestFinished, dispatchRequestEvent);
|
|
defineDeprecatedProperty(this, 'network', 'onFinished', 'onRequestFinished');
|
|
this.onNavigated = new EventSink(events.InspectedURLChanged);
|
|
}
|
|
|
|
Network.prototype = {
|
|
getHAR: function(callback) {
|
|
function callbackWrapper(result) {
|
|
const entries = (result && result.entries) || [];
|
|
for (let i = 0; i < entries.length; ++i) {
|
|
entries[i].__proto__ = new Request(entries[i]._requestId);
|
|
delete entries[i]._requestId;
|
|
}
|
|
callback(result);
|
|
}
|
|
extensionServer.sendRequest({command: commands.GetHAR}, callback && callbackWrapper);
|
|
},
|
|
|
|
addRequestHeaders: function(headers) {
|
|
extensionServer.sendRequest(
|
|
{command: commands.AddRequestHeaders, headers: headers, extensionId: window.location.hostname});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
function RequestImpl(id) {
|
|
this._id = id;
|
|
}
|
|
|
|
RequestImpl.prototype = {
|
|
getContent: function(callback) {
|
|
function callbackWrapper(response) {
|
|
callback(response.content, response.encoding);
|
|
}
|
|
extensionServer.sendRequest({command: commands.GetRequestContent, id: this._id}, callback && callbackWrapper);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
function Panels() {
|
|
const panels = {
|
|
elements: new ElementsPanel(),
|
|
sources: new SourcesPanel(),
|
|
};
|
|
|
|
function panelGetter(name) {
|
|
return panels[name];
|
|
}
|
|
for (const panel in panels) {
|
|
Object.defineProperty(this, panel, {get: panelGetter.bind(null, panel), enumerable: true});
|
|
}
|
|
this.applyStyleSheet = function(styleSheet) {
|
|
extensionServer.sendRequest({command: commands.ApplyStyleSheet, styleSheet: styleSheet});
|
|
};
|
|
}
|
|
|
|
Panels.prototype = {
|
|
create: function(title, icon, page, callback) {
|
|
const id = 'extension-panel-' + extensionServer.nextObjectId();
|
|
const request = {command: commands.CreatePanel, id: id, title: title, icon: icon, page: page};
|
|
extensionServer.sendRequest(request, callback && callback.bind(this, new ExtensionPanel(id)));
|
|
},
|
|
|
|
setOpenResourceHandler: function(callback) {
|
|
const hadHandler = extensionServer.hasHandler(events.OpenResource);
|
|
|
|
function callbackWrapper(message) {
|
|
// Allow the panel to show itself when handling the event.
|
|
userAction = true;
|
|
try {
|
|
callback.call(null, new Resource(message.resource), message.lineNumber);
|
|
} finally {
|
|
userAction = false;
|
|
}
|
|
}
|
|
|
|
if (!callback) {
|
|
extensionServer.unregisterHandler(events.OpenResource);
|
|
} else {
|
|
extensionServer.registerHandler(events.OpenResource, callbackWrapper);
|
|
}
|
|
|
|
// Only send command if we either removed an existing handler or added handler and had none before.
|
|
if (hadHandler === !callback) {
|
|
extensionServer.sendRequest({command: commands.SetOpenResourceHandler, 'handlerPresent': !!callback});
|
|
}
|
|
},
|
|
|
|
openResource: function(url, lineNumber, callback) {
|
|
extensionServer.sendRequest({command: commands.OpenResource, 'url': url, 'lineNumber': lineNumber}, callback);
|
|
},
|
|
|
|
get SearchAction() {
|
|
return apiPrivate.panels.SearchAction;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
function ExtensionViewImpl(id) {
|
|
this._id = id;
|
|
|
|
/**
|
|
* @this {EventSinkImpl}
|
|
*/
|
|
function dispatchShowEvent(message) {
|
|
const frameIndex = message.arguments[0];
|
|
if (typeof frameIndex === 'number') {
|
|
this._fire(window.parent.frames[frameIndex]);
|
|
} else {
|
|
this._fire();
|
|
}
|
|
}
|
|
|
|
if (id) {
|
|
this.onShown = new EventSink(events.ViewShown + id, dispatchShowEvent);
|
|
this.onHidden = new EventSink(events.ViewHidden + id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* @extends {ExtensionViewImpl}
|
|
* @param {string} hostPanelName
|
|
*/
|
|
function PanelWithSidebarImpl(hostPanelName) {
|
|
ExtensionViewImpl.call(this, null);
|
|
this._hostPanelName = hostPanelName;
|
|
this.onSelectionChanged = new EventSink(events.PanelObjectSelected + hostPanelName);
|
|
}
|
|
|
|
PanelWithSidebarImpl.prototype = {
|
|
createSidebarPane: function(title, callback) {
|
|
const id = 'extension-sidebar-' + extensionServer.nextObjectId();
|
|
const request = {command: commands.CreateSidebarPane, panel: this._hostPanelName, id: id, title: title};
|
|
function callbackWrapper() {
|
|
callback(new ExtensionSidebarPane(id));
|
|
}
|
|
extensionServer.sendRequest(request, callback && callbackWrapper);
|
|
},
|
|
|
|
__proto__: ExtensionViewImpl.prototype
|
|
};
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
function LanguageServicesAPIImpl() {
|
|
this._plugins = new Map();
|
|
}
|
|
|
|
LanguageServicesAPIImpl.prototype = {
|
|
/**
|
|
* @param {*} plugin The language plugin instance to register.
|
|
* @param {string} pluginName The plugin name
|
|
* @param {{language: string, symbol_types: !Array<string>}} supportedScriptTypes Script language and debug symbol types supported by this extension.
|
|
*/
|
|
registerLanguageExtensionPlugin: function(plugin, pluginName, supportedScriptTypes) {
|
|
if (this._plugins.has(plugin)) {
|
|
throw new Error('Tried to register a plugin twice');
|
|
}
|
|
const channel = new MessageChannel();
|
|
const port = channel.port1;
|
|
this._plugins.set(plugin, port);
|
|
port.onmessage = ({data: {requestId, method, parameters}}) => {
|
|
dispatchMethodCall(method, parameters)
|
|
.then(result => port.postMessage({requestId, result}))
|
|
.catch(error => port.postMessage({requestId, error: {message: error.message}}));
|
|
};
|
|
|
|
/**
|
|
* @param {string} method
|
|
* @param {*} parameters
|
|
* @return {!Promise<*>}
|
|
*/
|
|
function dispatchMethodCall(method, parameters) {
|
|
switch (method) {
|
|
case languageExtensionPluginCommands.AddRawModule:
|
|
return plugin.addRawModule(parameters.rawModuleId, parameters.symbolsURL, parameters.rawModule);
|
|
case languageExtensionPluginCommands.RemoveRawModule:
|
|
return plugin.removeRawModule(parameters.rawModuleId);
|
|
case languageExtensionPluginCommands.SourceLocationToRawLocation:
|
|
return plugin.sourceLocationToRawLocation(parameters.sourceLocation);
|
|
case languageExtensionPluginCommands.RawLocationToSourceLocation:
|
|
return plugin.rawLocationToSourceLocation(parameters.rawLocation);
|
|
case languageExtensionPluginCommands.GetScopeInfo:
|
|
return plugin.getScopeInfo(parameters.type);
|
|
case languageExtensionPluginCommands.ListVariablesInScope:
|
|
return plugin.listVariablesInScope(parameters.rawLocation);
|
|
case languageExtensionPluginCommands.EvaluateVariable:
|
|
return plugin.evaluateVariable(parameters.name, parameters.location);
|
|
case languageExtensionPluginCommands.GetFunctionInfo:
|
|
return plugin.getFunctionInfo(parameters.rawLocation);
|
|
}
|
|
throw new Error(`Unknown language plugin method ${method}`);
|
|
}
|
|
|
|
extensionServer.sendRequest(
|
|
{command: commands.RegisterLanguageExtensionPlugin, pluginName, port: channel.port2, supportedScriptTypes},
|
|
undefined, [channel.port2]);
|
|
}
|
|
};
|
|
|
|
function declareInterfaceClass(implConstructor) {
|
|
return function() {
|
|
const impl = {__proto__: implConstructor.prototype};
|
|
implConstructor.apply(impl, arguments);
|
|
populateInterfaceClass(this, impl);
|
|
};
|
|
}
|
|
|
|
function defineDeprecatedProperty(object, className, oldName, newName) {
|
|
let warningGiven = false;
|
|
function getter() {
|
|
if (!warningGiven) {
|
|
console.warn(className + '.' + oldName + ' is deprecated. Use ' + className + '.' + newName + ' instead');
|
|
warningGiven = true;
|
|
}
|
|
return object[newName];
|
|
}
|
|
object.__defineGetter__(oldName, getter);
|
|
}
|
|
|
|
function extractCallbackArgument(args) {
|
|
const lastArgument = args[args.length - 1];
|
|
return typeof lastArgument === 'function' ? lastArgument : undefined;
|
|
}
|
|
|
|
const LanguageServicesAPI = declareInterfaceClass(LanguageServicesAPIImpl);
|
|
const Button = declareInterfaceClass(ButtonImpl);
|
|
const EventSink = declareInterfaceClass(EventSinkImpl);
|
|
const ExtensionPanel = declareInterfaceClass(ExtensionPanelImpl);
|
|
const ExtensionSidebarPane = declareInterfaceClass(ExtensionSidebarPaneImpl);
|
|
/**
|
|
* @constructor
|
|
* @param {string} hostPanelName
|
|
*/
|
|
const PanelWithSidebarClass = declareInterfaceClass(PanelWithSidebarImpl);
|
|
const Request = declareInterfaceClass(RequestImpl);
|
|
const Resource = declareInterfaceClass(ResourceImpl);
|
|
const TraceSession = declareInterfaceClass(TraceSessionImpl);
|
|
|
|
class ElementsPanel extends PanelWithSidebarClass {
|
|
constructor() {
|
|
super('elements');
|
|
}
|
|
}
|
|
|
|
class SourcesPanel extends PanelWithSidebarClass {
|
|
constructor() {
|
|
super('sources');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* @extends {ExtensionViewImpl}
|
|
*/
|
|
function ExtensionPanelImpl(id) {
|
|
ExtensionViewImpl.call(this, id);
|
|
this.onSearch = new EventSink(events.PanelSearch + id);
|
|
}
|
|
|
|
ExtensionPanelImpl.prototype = {
|
|
/**
|
|
* @return {!Object}
|
|
*/
|
|
createStatusBarButton: function(iconPath, tooltipText, disabled) {
|
|
const id = 'button-' + extensionServer.nextObjectId();
|
|
const request = {
|
|
command: commands.CreateToolbarButton,
|
|
panel: this._id,
|
|
id: id,
|
|
icon: iconPath,
|
|
tooltip: tooltipText,
|
|
disabled: !!disabled
|
|
};
|
|
extensionServer.sendRequest(request);
|
|
return new Button(id);
|
|
},
|
|
|
|
show: function() {
|
|
if (!userAction) {
|
|
return;
|
|
}
|
|
|
|
const request = {command: commands.ShowPanel, id: this._id};
|
|
extensionServer.sendRequest(request);
|
|
},
|
|
|
|
__proto__: ExtensionViewImpl.prototype
|
|
};
|
|
|
|
/**
|
|
* @constructor
|
|
* @extends {ExtensionViewImpl}
|
|
*/
|
|
function ExtensionSidebarPaneImpl(id) {
|
|
ExtensionViewImpl.call(this, id);
|
|
}
|
|
|
|
ExtensionSidebarPaneImpl.prototype = {
|
|
setHeight: function(height) {
|
|
extensionServer.sendRequest({command: commands.SetSidebarHeight, id: this._id, height: height});
|
|
},
|
|
|
|
setExpression: function(expression, rootTitle, evaluateOptions) {
|
|
const request = {
|
|
command: commands.SetSidebarContent,
|
|
id: this._id,
|
|
expression: expression,
|
|
rootTitle: rootTitle,
|
|
evaluateOnPage: true,
|
|
};
|
|
if (typeof evaluateOptions === 'object') {
|
|
request.evaluateOptions = evaluateOptions;
|
|
}
|
|
extensionServer.sendRequest(request, extractCallbackArgument(arguments));
|
|
},
|
|
|
|
setObject: function(jsonObject, rootTitle, callback) {
|
|
extensionServer.sendRequest(
|
|
{command: commands.SetSidebarContent, id: this._id, expression: jsonObject, rootTitle: rootTitle}, callback);
|
|
},
|
|
|
|
setPage: function(page) {
|
|
extensionServer.sendRequest({command: commands.SetSidebarPage, id: this._id, page: page});
|
|
},
|
|
|
|
__proto__: ExtensionViewImpl.prototype
|
|
};
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
function ButtonImpl(id) {
|
|
this._id = id;
|
|
this.onClicked = new EventSink(events.ButtonClicked + id);
|
|
}
|
|
|
|
ButtonImpl.prototype = {
|
|
update: function(iconPath, tooltipText, disabled) {
|
|
const request =
|
|
{command: commands.UpdateButton, id: this._id, icon: iconPath, tooltip: tooltipText, disabled: !!disabled};
|
|
extensionServer.sendRequest(request);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
function Timeline() {
|
|
}
|
|
|
|
Timeline.prototype = {
|
|
/**
|
|
* @param {string} categoryName
|
|
* @param {string} categoryTooltip
|
|
* @return {!TraceProvider}
|
|
*/
|
|
addTraceProvider: function(categoryName, categoryTooltip) {
|
|
const id = 'extension-trace-provider-' + extensionServer.nextObjectId();
|
|
extensionServer.sendRequest(
|
|
{command: commands.AddTraceProvider, id: id, categoryName: categoryName, categoryTooltip: categoryTooltip});
|
|
return new TraceProvider(id);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {string} id
|
|
*/
|
|
function TraceSessionImpl(id) {
|
|
this._id = id;
|
|
}
|
|
|
|
TraceSessionImpl.prototype = {
|
|
/**
|
|
* @param {string=} url
|
|
* @param {number=} timeOffset
|
|
*/
|
|
complete: function(url, timeOffset) {
|
|
const request =
|
|
{command: commands.CompleteTraceSession, id: this._id, url: url || '', timeOffset: timeOffset || 0};
|
|
extensionServer.sendRequest(request);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {string} id
|
|
*/
|
|
function TraceProvider(id) {
|
|
/**
|
|
* @this {EventSinkImpl}
|
|
*/
|
|
function dispatchRecordingStarted(message) {
|
|
const sessionId = message.arguments[0];
|
|
this._fire(new TraceSession(sessionId));
|
|
}
|
|
|
|
this.onRecordingStarted = new EventSink(events.RecordingStarted + id, dispatchRecordingStarted);
|
|
this.onRecordingStopped = new EventSink(events.RecordingStopped + id);
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
function InspectedWindow() {
|
|
/**
|
|
* @this {EventSinkImpl}
|
|
*/
|
|
function dispatchResourceEvent(message) {
|
|
this._fire(new Resource(message.arguments[0]));
|
|
}
|
|
|
|
/**
|
|
* @this {EventSinkImpl}
|
|
*/
|
|
function dispatchResourceContentEvent(message) {
|
|
this._fire(new Resource(message.arguments[0]), message.arguments[1]);
|
|
}
|
|
|
|
this.onResourceAdded = new EventSink(events.ResourceAdded, dispatchResourceEvent);
|
|
this.onResourceContentCommitted = new EventSink(events.ResourceContentCommitted, dispatchResourceContentEvent);
|
|
}
|
|
|
|
InspectedWindow.prototype = {
|
|
reload: function(optionsOrUserAgent) {
|
|
let options = null;
|
|
if (typeof optionsOrUserAgent === 'object') {
|
|
options = optionsOrUserAgent;
|
|
} else if (typeof optionsOrUserAgent === 'string') {
|
|
options = {userAgent: optionsOrUserAgent};
|
|
console.warn(
|
|
'Passing userAgent as string parameter to inspectedWindow.reload() is deprecated. ' +
|
|
'Use inspectedWindow.reload({ userAgent: value}) instead.');
|
|
}
|
|
extensionServer.sendRequest({command: commands.Reload, options: options});
|
|
},
|
|
|
|
/**
|
|
* @return {?Object}
|
|
*/
|
|
eval: function(expression, evaluateOptions) {
|
|
const callback = extractCallbackArgument(arguments);
|
|
function callbackWrapper(result) {
|
|
if (result.isError || result.isException) {
|
|
callback(undefined, result);
|
|
} else {
|
|
callback(result.value);
|
|
}
|
|
}
|
|
const request = {command: commands.EvaluateOnInspectedPage, expression: expression};
|
|
if (typeof evaluateOptions === 'object') {
|
|
request.evaluateOptions = evaluateOptions;
|
|
}
|
|
extensionServer.sendRequest(request, callback && callbackWrapper);
|
|
return null;
|
|
},
|
|
|
|
getResources: function(callback) {
|
|
function wrapResource(resourceData) {
|
|
return new Resource(resourceData);
|
|
}
|
|
function callbackWrapper(resources) {
|
|
callback(resources.map(wrapResource));
|
|
}
|
|
extensionServer.sendRequest({command: commands.GetPageResources}, callback && callbackWrapper);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
function ResourceImpl(resourceData) {
|
|
this._url = resourceData.url;
|
|
this._type = resourceData.type;
|
|
}
|
|
|
|
ResourceImpl.prototype = {
|
|
get url() {
|
|
return this._url;
|
|
},
|
|
|
|
get type() {
|
|
return this._type;
|
|
},
|
|
|
|
getContent: function(callback) {
|
|
function callbackWrapper(response) {
|
|
callback(response.content, response.encoding);
|
|
}
|
|
|
|
extensionServer.sendRequest({command: commands.GetResourceContent, url: this._url}, callback && callbackWrapper);
|
|
},
|
|
|
|
setContent: function(content, commit, callback) {
|
|
extensionServer.sendRequest(
|
|
{command: commands.SetResourceContent, url: this._url, content: content, commit: commit}, callback);
|
|
}
|
|
};
|
|
|
|
function getTabId() {
|
|
return inspectedTabId;
|
|
}
|
|
|
|
let keyboardEventRequestQueue = [];
|
|
let forwardTimer = null;
|
|
|
|
/**
|
|
* @suppressGlobalPropertiesCheck
|
|
*/
|
|
function forwardKeyboardEvent(event) {
|
|
// Check if the event should be forwarded.
|
|
// This is a workaround for crbug.com/923338.
|
|
const focused = document.activeElement;
|
|
if (focused) {
|
|
const isInput = focused.nodeName === 'INPUT' || focused.nodeName === 'TEXTAREA';
|
|
if (isInput && !(event.ctrlKey || event.altKey || event.metaKey)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
let modifiers = 0;
|
|
if (event.shiftKey) {
|
|
modifiers |= 1;
|
|
}
|
|
if (event.ctrlKey) {
|
|
modifiers |= 2;
|
|
}
|
|
if (event.altKey) {
|
|
modifiers |= 4;
|
|
}
|
|
if (event.metaKey) {
|
|
modifiers |= 8;
|
|
}
|
|
const num = (event.keyCode & 255) | (modifiers << 8);
|
|
// We only care about global hotkeys, not about random text
|
|
if (!keysToForwardSet.has(num)) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
const requestPayload = {
|
|
eventType: event.type,
|
|
ctrlKey: event.ctrlKey,
|
|
altKey: event.altKey,
|
|
metaKey: event.metaKey,
|
|
shiftKey: event.shiftKey,
|
|
keyIdentifier: event.keyIdentifier,
|
|
key: event.key,
|
|
code: event.code,
|
|
location: event.location,
|
|
keyCode: event.keyCode
|
|
};
|
|
keyboardEventRequestQueue.push(requestPayload);
|
|
if (!forwardTimer) {
|
|
forwardTimer = setTimeout(forwardEventQueue, 0);
|
|
}
|
|
}
|
|
|
|
function forwardEventQueue() {
|
|
forwardTimer = null;
|
|
const request = {command: commands.ForwardKeyboardEvent, entries: keyboardEventRequestQueue};
|
|
extensionServer.sendRequest(request);
|
|
keyboardEventRequestQueue = [];
|
|
}
|
|
|
|
document.addEventListener('keydown', forwardKeyboardEvent, false);
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
function ExtensionServerClient() {
|
|
this._callbacks = {};
|
|
this._handlers = {};
|
|
this._lastRequestId = 0;
|
|
this._lastObjectId = 0;
|
|
|
|
this.registerHandler('callback', this._onCallback.bind(this));
|
|
|
|
const channel = new MessageChannel();
|
|
this._port = channel.port1;
|
|
this._port.addEventListener('message', this._onMessage.bind(this), false);
|
|
this._port.start();
|
|
|
|
window.parent.postMessage('registerExtension', '*', [channel.port2]);
|
|
}
|
|
|
|
ExtensionServerClient.prototype = {
|
|
/**
|
|
* @param {!Object} message
|
|
* @param {function()=} callback
|
|
* @param {!Array<*>=} transfers
|
|
*/
|
|
sendRequest: function(message, callback, transfers) {
|
|
if (typeof callback === 'function') {
|
|
message.requestId = this._registerCallback(callback);
|
|
}
|
|
this._port.postMessage(message, transfers);
|
|
},
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
hasHandler: function(command) {
|
|
return !!this._handlers[command];
|
|
},
|
|
|
|
registerHandler: function(command, handler) {
|
|
this._handlers[command] = handler;
|
|
},
|
|
|
|
unregisterHandler: function(command) {
|
|
delete this._handlers[command];
|
|
},
|
|
|
|
/**
|
|
* @return {string}
|
|
*/
|
|
nextObjectId: function() {
|
|
return injectedScriptId.toString() + '_' + ++this._lastObjectId;
|
|
},
|
|
|
|
_registerCallback: function(callback) {
|
|
const id = ++this._lastRequestId;
|
|
this._callbacks[id] = callback;
|
|
return id;
|
|
},
|
|
|
|
_onCallback: function(request) {
|
|
if (request.requestId in this._callbacks) {
|
|
const callback = this._callbacks[request.requestId];
|
|
delete this._callbacks[request.requestId];
|
|
callback(request.result);
|
|
}
|
|
},
|
|
|
|
_onMessage: function(event) {
|
|
const request = event.data;
|
|
const handler = this._handlers[request.command];
|
|
if (handler) {
|
|
handler.call(this, request);
|
|
}
|
|
}
|
|
};
|
|
|
|
function populateInterfaceClass(interfaze, implementation) {
|
|
for (const member in implementation) {
|
|
if (member.charAt(0) === '_') {
|
|
continue;
|
|
}
|
|
let descriptor = null;
|
|
// Traverse prototype chain until we find the owner.
|
|
for (let owner = implementation; owner && !descriptor; owner = owner.__proto__) {
|
|
descriptor = Object.getOwnPropertyDescriptor(owner, member);
|
|
}
|
|
if (!descriptor) {
|
|
continue;
|
|
}
|
|
if (typeof descriptor.value === 'function') {
|
|
interfaze[member] = descriptor.value.bind(implementation);
|
|
} else if (typeof descriptor.get === 'function') {
|
|
interfaze.__defineGetter__(member, descriptor.get.bind(implementation));
|
|
} else {
|
|
Object.defineProperty(interfaze, member, descriptor);
|
|
}
|
|
}
|
|
}
|
|
|
|
const extensionServer = new ExtensionServerClient();
|
|
const coreAPI = new InspectorExtensionAPI();
|
|
|
|
Object.defineProperty(chrome, 'devtools', {value: {}, enumerable: true});
|
|
|
|
// Only expose tabId on chrome.devtools.inspectedWindow, not webInspector.inspectedWindow.
|
|
chrome.devtools.inspectedWindow = {};
|
|
Object.defineProperty(chrome.devtools.inspectedWindow, 'tabId', {get: getTabId});
|
|
chrome.devtools.inspectedWindow.__proto__ = coreAPI.inspectedWindow;
|
|
chrome.devtools.network = coreAPI.network;
|
|
chrome.devtools.panels = coreAPI.panels;
|
|
chrome.devtools.panels.themeName = themeName;
|
|
chrome.devtools.languageServices = new LanguageServicesAPI();
|
|
|
|
// default to expose experimental APIs for now.
|
|
if (extensionInfo.exposeExperimentalAPIs !== false) {
|
|
chrome.experimental = chrome.experimental || {};
|
|
chrome.experimental.devtools = chrome.experimental.devtools || {};
|
|
|
|
const properties = Object.getOwnPropertyNames(coreAPI);
|
|
for (let i = 0; i < properties.length; ++i) {
|
|
const descriptor = Object.getOwnPropertyDescriptor(coreAPI, properties[i]);
|
|
if (descriptor) {
|
|
Object.defineProperty(chrome.experimental.devtools, properties[i], descriptor);
|
|
}
|
|
}
|
|
chrome.experimental.devtools.inspectedWindow = chrome.devtools.inspectedWindow;
|
|
}
|
|
|
|
if (extensionInfo.exposeWebInspectorNamespace) {
|
|
window.webInspector = coreAPI;
|
|
}
|
|
testHook(extensionServer, coreAPI);
|
|
};
|
|
|
|
/**
|
|
* @param {!{startPage: string, name: string, exposeExperimentalAPIs: boolean}} extensionInfo
|
|
* @param {string} inspectedTabId
|
|
* @param {string} themeName
|
|
* @param {!Array<number>} keysToForward
|
|
* @param {function(!Object, !Object)|undefined} testHook
|
|
* @return {string}
|
|
*/
|
|
self.buildExtensionAPIInjectedScript = function(extensionInfo, inspectedTabId, themeName, keysToForward, testHook) {
|
|
const argumentsJSON =
|
|
[extensionInfo, inspectedTabId || null, themeName, keysToForward].map(_ => JSON.stringify(_)).join(',');
|
|
if (!testHook) {
|
|
testHook = () => {};
|
|
}
|
|
return '(function(injectedScriptId){ ' + defineCommonExtensionSymbols.toString() + ';' +
|
|
'(' + self.injectedExtensionAPI.toString() + ')(' + argumentsJSON + ',' + testHook + ', injectedScriptId);' +
|
|
'})';
|
|
};
|