mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-01 17:57:36 +08:00
This adds a new method `unregisterLanguageExtensionPlugin()` to the `LanguageServicesAPI` exposed to DevTools extensions, which allows the developer to unregister a previously registred language extension plugin. This is useful for example when the configuration for an extension changes and the front-end should reload the source mappings. Drive-by-fix: Also make the registerLanguageExtensionPlugin and the unregisterLanguageExtensionPlugin methods return promises, so that the extensions can properly await / chain them. Fixed: chromium:1151285 Bug: chromium:1151280, chromium:1041362, chromium:1083146 Change-Id: Ib62b70c717754d70cf827ba1a97fb5c3e358ee1f Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2552506 Reviewed-by: Philip Pfaffe <pfaffe@chromium.org> Commit-Queue: Benedikt Meurer <bmeurer@chromium.org> Auto-Submit: Benedikt Meurer <bmeurer@chromium.org>
978 lines
31 KiB
JavaScript
978 lines
31 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',
|
|
GetTypeInfo: 'getTypeInfo',
|
|
GetFormatter: 'getFormatter',
|
|
GetFunctionInfo: 'getFunctionInfo',
|
|
GetInlinedFunctionRanges: 'getInlinedFunctionRanges',
|
|
GetInlinedCalleesRanges: 'getInlinedCalleesRanges'
|
|
};
|
|
|
|
/** @enum {string} */
|
|
apiPrivate.LanguageExtensionPluginEvents = {
|
|
UnregisteredLanguageExtensionPlugin: 'unregisteredLanguageExtensionPlugin'
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @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 languageExtensionPluginEvents = apiPrivate.LanguageExtensionPluginEvents;
|
|
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() {
|
|
/** @type {!Map<*, !MessagePort>} */
|
|
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.
|
|
* @return {!Promise<void>}
|
|
*/
|
|
registerLanguageExtensionPlugin: async function(plugin, pluginName, supportedScriptTypes) {
|
|
if (this._plugins.has(plugin)) {
|
|
throw new Error(`Tried to register plugin '${pluginName}' 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.GetTypeInfo:
|
|
return plugin.getTypeInfo(parameters.expression, parameters.context);
|
|
case languageExtensionPluginCommands.GetFormatter:
|
|
return plugin.getFormatter(parameters.expressionOrField, parameters.context);
|
|
case languageExtensionPluginCommands.GetFunctionInfo:
|
|
return plugin.getFunctionInfo(parameters.rawLocation);
|
|
case languageExtensionPluginCommands.GetInlinedFunctionRanges:
|
|
return plugin.getInlinedFunctionRanges(parameters.rawLocation);
|
|
case languageExtensionPluginCommands.GetInlinedCalleesRanges:
|
|
return plugin.getInlinedCalleesRanges(parameters.rawLocation);
|
|
}
|
|
throw new Error(`Unknown language plugin method ${method}`);
|
|
}
|
|
|
|
await new Promise(resolve => {
|
|
extensionServer.sendRequest(
|
|
{command: commands.RegisterLanguageExtensionPlugin, pluginName, port: channel.port2, supportedScriptTypes},
|
|
() => resolve(), [channel.port2]);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* @param {*} plugin The language plugin instance to unregister.
|
|
* @return {!Promise<void>}
|
|
*/
|
|
unregisterLanguageExtensionPlugin: async function(plugin) {
|
|
const port = this._plugins.get(plugin);
|
|
if (!port) {
|
|
throw new Error('Tried to unregister a plugin that was not previously registered');
|
|
}
|
|
this._plugins.delete(plugin);
|
|
port.postMessage({event: languageExtensionPluginEvents.UnregisteredLanguageExtensionPlugin});
|
|
port.close();
|
|
}
|
|
};
|
|
|
|
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);' +
|
|
'})';
|
|
};
|