mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-10 13:44:39 +08:00
Move and rename files. NOAUTOREVERT=true NOPRESUBMIT=true NOTREECHECKS=true Bug: 768828 TBR=darin@chromium.org NOTRY=true Change-Id: I66d3b155808bc5bdbf237b80208e1e552bcf7f28 Reviewed-on: https://chromium-review.googlesource.com/1001153 Reviewed-by: Blink Reformat <blink-reformat@chromium.org> Commit-Queue: Blink Reformat <blink-reformat@chromium.org> Cr-Original-Commit-Position: refs/heads/master@{#549061} Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src Cr-Mirrored-Commit: 0aee4434a4dba42a42abaea9bfbc0cd196a63bc1
760 lines
25 KiB
JavaScript
760 lines
25 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.
|
|
*/
|
|
|
|
/**
|
|
* @implements {SDK.TargetManager.Observer}
|
|
* @unrestricted
|
|
*/
|
|
Components.Linkifier = class {
|
|
/**
|
|
* @param {number=} maxLengthForDisplayedURLs
|
|
* @param {boolean=} useLinkDecorator
|
|
*/
|
|
constructor(maxLengthForDisplayedURLs, useLinkDecorator) {
|
|
this._maxLength = maxLengthForDisplayedURLs || UI.MaxLengthForDisplayedURLs;
|
|
/** @type {!Map<!SDK.Target, !Array<!Element>>} */
|
|
this._anchorsByTarget = new Map();
|
|
/** @type {!Map<!SDK.Target, !Bindings.LiveLocationPool>} */
|
|
this._locationPoolByTarget = new Map();
|
|
this._useLinkDecorator = !!useLinkDecorator;
|
|
Components.Linkifier._instances.add(this);
|
|
SDK.targetManager.observeTargets(this);
|
|
}
|
|
|
|
/**
|
|
* @param {!Components.LinkDecorator} decorator
|
|
*/
|
|
static setLinkDecorator(decorator) {
|
|
console.assert(!Components.Linkifier._decorator, 'Cannot re-register link decorator.');
|
|
Components.Linkifier._decorator = decorator;
|
|
decorator.addEventListener(Components.LinkDecorator.Events.LinkIconChanged, onLinkIconChanged);
|
|
for (const linkifier of Components.Linkifier._instances)
|
|
linkifier._updateAllAnchorDecorations();
|
|
|
|
/**
|
|
* @param {!Common.Event} event
|
|
*/
|
|
function onLinkIconChanged(event) {
|
|
const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data);
|
|
const links = uiSourceCode[Components.Linkifier._sourceCodeAnchors] || [];
|
|
for (const link of links)
|
|
Components.Linkifier._updateLinkDecorations(link);
|
|
}
|
|
}
|
|
|
|
_updateAllAnchorDecorations() {
|
|
for (const anchors of this._anchorsByTarget.values()) {
|
|
for (const anchor of anchors)
|
|
Components.Linkifier._updateLinkDecorations(anchor);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} anchor
|
|
* @param {!Workspace.UILocation} uiLocation
|
|
*/
|
|
static _bindUILocation(anchor, uiLocation) {
|
|
Components.Linkifier._linkInfo(anchor).uiLocation = uiLocation;
|
|
if (!uiLocation)
|
|
return;
|
|
const uiSourceCode = uiLocation.uiSourceCode;
|
|
let sourceCodeAnchors = uiSourceCode[Components.Linkifier._sourceCodeAnchors];
|
|
if (!sourceCodeAnchors) {
|
|
sourceCodeAnchors = new Set();
|
|
uiSourceCode[Components.Linkifier._sourceCodeAnchors] = sourceCodeAnchors;
|
|
}
|
|
sourceCodeAnchors.add(anchor);
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} anchor
|
|
*/
|
|
static _unbindUILocation(anchor) {
|
|
const info = Components.Linkifier._linkInfo(anchor);
|
|
if (!info.uiLocation)
|
|
return;
|
|
|
|
const uiSourceCode = info.uiLocation.uiSourceCode;
|
|
info.uiLocation = null;
|
|
const sourceCodeAnchors = uiSourceCode[Components.Linkifier._sourceCodeAnchors];
|
|
if (sourceCodeAnchors)
|
|
sourceCodeAnchors.delete(anchor);
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {!SDK.Target} target
|
|
*/
|
|
targetAdded(target) {
|
|
this._anchorsByTarget.set(target, []);
|
|
this._locationPoolByTarget.set(target, new Bindings.LiveLocationPool());
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {!SDK.Target} target
|
|
*/
|
|
targetRemoved(target) {
|
|
const locationPool = /** @type {!Bindings.LiveLocationPool} */ (this._locationPoolByTarget.remove(target));
|
|
locationPool.disposeAll();
|
|
const anchors = this._anchorsByTarget.remove(target);
|
|
for (const anchor of anchors) {
|
|
const info = Components.Linkifier._linkInfo(anchor);
|
|
info.liveLocation = null;
|
|
Components.Linkifier._unbindUILocation(anchor);
|
|
if (info.fallback) {
|
|
anchor.href = info.fallback.href;
|
|
anchor.title = info.fallback.title;
|
|
anchor.className = info.fallback.className;
|
|
anchor.textContent = info.fallback.textContent;
|
|
anchor[Components.Linkifier._infoSymbol] = info.fallback[Components.Linkifier._infoSymbol];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {?SDK.Target} target
|
|
* @param {?string} scriptId
|
|
* @param {string} sourceURL
|
|
* @param {number} lineNumber
|
|
* @param {number=} columnNumber
|
|
* @param {string=} classes
|
|
* @return {?Element}
|
|
*/
|
|
maybeLinkifyScriptLocation(target, scriptId, sourceURL, lineNumber, columnNumber, classes) {
|
|
let fallbackAnchor = null;
|
|
if (sourceURL) {
|
|
fallbackAnchor = Components.Linkifier.linkifyURL(
|
|
sourceURL,
|
|
{className: classes, lineNumber: lineNumber, columnNumber: columnNumber, maxLength: this._maxLength});
|
|
}
|
|
if (!target || target.isDisposed())
|
|
return fallbackAnchor;
|
|
const debuggerModel = target.model(SDK.DebuggerModel);
|
|
if (!debuggerModel)
|
|
return fallbackAnchor;
|
|
|
|
const rawLocation =
|
|
(scriptId ? debuggerModel.createRawLocationByScriptId(scriptId, lineNumber, columnNumber || 0) : null) ||
|
|
debuggerModel.createRawLocationByURL(sourceURL, lineNumber, columnNumber || 0);
|
|
if (!rawLocation)
|
|
return fallbackAnchor;
|
|
|
|
const anchor = Components.Linkifier._createLink('', classes || '');
|
|
const info = Components.Linkifier._linkInfo(anchor);
|
|
info.enableDecorator = this._useLinkDecorator;
|
|
info.fallback = fallbackAnchor;
|
|
info.liveLocation = Bindings.debuggerWorkspaceBinding.createLiveLocation(
|
|
rawLocation, this._updateAnchor.bind(this, anchor),
|
|
/** @type {!Bindings.LiveLocationPool} */ (this._locationPoolByTarget.get(rawLocation.debuggerModel.target())));
|
|
|
|
const anchors = /** @type {!Array<!Element>} */ (this._anchorsByTarget.get(rawLocation.debuggerModel.target()));
|
|
anchors.push(anchor);
|
|
return anchor;
|
|
}
|
|
|
|
/**
|
|
* @param {?SDK.Target} target
|
|
* @param {?string} scriptId
|
|
* @param {string} sourceURL
|
|
* @param {number} lineNumber
|
|
* @param {number=} columnNumber
|
|
* @param {string=} classes
|
|
* @return {!Element}
|
|
*/
|
|
linkifyScriptLocation(target, scriptId, sourceURL, lineNumber, columnNumber, classes) {
|
|
const scriptLink = this.maybeLinkifyScriptLocation(target, scriptId, sourceURL, lineNumber, columnNumber, classes);
|
|
return scriptLink ||
|
|
Components.Linkifier.linkifyURL(
|
|
sourceURL,
|
|
{className: classes, lineNumber: lineNumber, columnNumber: columnNumber, maxLength: this._maxLength});
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.DebuggerModel.Location} rawLocation
|
|
* @param {string} fallbackUrl
|
|
* @param {string=} classes
|
|
* @return {!Element}
|
|
*/
|
|
linkifyRawLocation(rawLocation, fallbackUrl, classes) {
|
|
return this.linkifyScriptLocation(
|
|
rawLocation.debuggerModel.target(), rawLocation.scriptId, fallbackUrl, rawLocation.lineNumber,
|
|
rawLocation.columnNumber, classes);
|
|
}
|
|
|
|
/**
|
|
* @param {?SDK.Target} target
|
|
* @param {!Protocol.Runtime.CallFrame} callFrame
|
|
* @param {string=} classes
|
|
* @return {?Element}
|
|
*/
|
|
maybeLinkifyConsoleCallFrame(target, callFrame, classes) {
|
|
return this.maybeLinkifyScriptLocation(
|
|
target, callFrame.scriptId, callFrame.url, callFrame.lineNumber, callFrame.columnNumber, classes);
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.Target} target
|
|
* @param {!Protocol.Runtime.StackTrace} stackTrace
|
|
* @param {string=} classes
|
|
* @return {!Element}
|
|
*/
|
|
linkifyStackTraceTopFrame(target, stackTrace, classes) {
|
|
console.assert(stackTrace.callFrames && stackTrace.callFrames.length);
|
|
|
|
const topFrame = stackTrace.callFrames[0];
|
|
const fallbackAnchor = Components.Linkifier.linkifyURL(topFrame.url, {
|
|
className: classes,
|
|
lineNumber: topFrame.lineNumber,
|
|
columnNumber: topFrame.columnNumber,
|
|
maxLength: this._maxLength
|
|
});
|
|
if (target.isDisposed())
|
|
return fallbackAnchor;
|
|
|
|
const debuggerModel = target.model(SDK.DebuggerModel);
|
|
const rawLocations = debuggerModel.createRawLocationsByStackTrace(stackTrace);
|
|
if (rawLocations.length === 0)
|
|
return fallbackAnchor;
|
|
|
|
const anchor = Components.Linkifier._createLink('', classes || '');
|
|
const info = Components.Linkifier._linkInfo(anchor);
|
|
info.enableDecorator = this._useLinkDecorator;
|
|
info.fallback = fallbackAnchor;
|
|
info.liveLocation = Bindings.debuggerWorkspaceBinding.createStackTraceTopFrameLiveLocation(
|
|
rawLocations, this._updateAnchor.bind(this, anchor),
|
|
/** @type {!Bindings.LiveLocationPool} */ (this._locationPoolByTarget.get(target)));
|
|
|
|
const anchors = /** @type {!Array<!Element>} */ (this._anchorsByTarget.get(target));
|
|
anchors.push(anchor);
|
|
return anchor;
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.CSSLocation} rawLocation
|
|
* @param {string=} classes
|
|
* @return {!Element}
|
|
*/
|
|
linkifyCSSLocation(rawLocation, classes) {
|
|
const anchor = Components.Linkifier._createLink('', classes || '');
|
|
const info = Components.Linkifier._linkInfo(anchor);
|
|
info.enableDecorator = this._useLinkDecorator;
|
|
info.liveLocation = Bindings.cssWorkspaceBinding.createLiveLocation(
|
|
rawLocation, this._updateAnchor.bind(this, anchor),
|
|
/** @type {!Bindings.LiveLocationPool} */ (this._locationPoolByTarget.get(rawLocation.cssModel().target())));
|
|
|
|
const anchors = /** @type {!Array<!Element>} */ (this._anchorsByTarget.get(rawLocation.cssModel().target()));
|
|
anchors.push(anchor);
|
|
return anchor;
|
|
}
|
|
|
|
reset() {
|
|
for (const target of this._anchorsByTarget.keysArray()) {
|
|
this.targetRemoved(target);
|
|
this.targetAdded(target);
|
|
}
|
|
}
|
|
|
|
dispose() {
|
|
for (const target of this._anchorsByTarget.keysArray())
|
|
this.targetRemoved(target);
|
|
SDK.targetManager.unobserveTargets(this);
|
|
Components.Linkifier._instances.delete(this);
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} anchor
|
|
* @param {!Bindings.LiveLocation} liveLocation
|
|
*/
|
|
_updateAnchor(anchor, liveLocation) {
|
|
Components.Linkifier._unbindUILocation(anchor);
|
|
const uiLocation = liveLocation.uiLocation();
|
|
if (!uiLocation)
|
|
return;
|
|
|
|
Components.Linkifier._bindUILocation(anchor, uiLocation);
|
|
const text = uiLocation.linkText(true /* skipTrim */);
|
|
Components.Linkifier._setTrimmedText(anchor, text, this._maxLength);
|
|
|
|
let titleText = uiLocation.uiSourceCode.url();
|
|
if (typeof uiLocation.lineNumber === 'number')
|
|
titleText += ':' + (uiLocation.lineNumber + 1);
|
|
anchor.title = titleText;
|
|
anchor.classList.toggle('webkit-html-blackbox-link', liveLocation.isBlackboxed());
|
|
Components.Linkifier._updateLinkDecorations(anchor);
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} anchor
|
|
*/
|
|
static _updateLinkDecorations(anchor) {
|
|
const info = Components.Linkifier._linkInfo(anchor);
|
|
if (!info || !info.enableDecorator)
|
|
return;
|
|
if (!Components.Linkifier._decorator || !info.uiLocation)
|
|
return;
|
|
if (info.icon && info.icon.parentElement)
|
|
anchor.removeChild(info.icon);
|
|
const icon = Components.Linkifier._decorator.linkIcon(info.uiLocation.uiSourceCode);
|
|
if (icon) {
|
|
icon.style.setProperty('margin-right', '2px');
|
|
anchor.insertBefore(icon, anchor.firstChild);
|
|
}
|
|
info.icon = icon;
|
|
}
|
|
|
|
/**
|
|
* @param {string} url
|
|
* @param {!Components.LinkifyURLOptions=} options
|
|
* @return {!Element}
|
|
*/
|
|
static linkifyURL(url, options) {
|
|
options = options || {};
|
|
const text = options.text;
|
|
const className = options.className || '';
|
|
const lineNumber = options.lineNumber;
|
|
const columnNumber = options.columnNumber;
|
|
const preventClick = options.preventClick;
|
|
const maxLength = options.maxLength || UI.MaxLengthForDisplayedURLs;
|
|
if (!url || url.trim().toLowerCase().startsWith('javascript:')) {
|
|
const element = createElementWithClass('span', className);
|
|
element.textContent = text || url || Common.UIString('(unknown)');
|
|
return element;
|
|
}
|
|
|
|
let linkText = text || Bindings.displayNameForURL(url);
|
|
if (typeof lineNumber === 'number' && !text)
|
|
linkText += ':' + (lineNumber + 1);
|
|
const title = linkText !== url ? url : '';
|
|
const link = Components.Linkifier._createLink(linkText, className, maxLength, title, url, preventClick);
|
|
const info = Components.Linkifier._linkInfo(link);
|
|
if (typeof lineNumber === 'number')
|
|
info.lineNumber = lineNumber;
|
|
if (typeof columnNumber === 'number')
|
|
info.columnNumber = columnNumber;
|
|
return link;
|
|
}
|
|
|
|
/**
|
|
* @param {!Object} revealable
|
|
* @param {string} text
|
|
* @param {string=} fallbackHref
|
|
* @return {!Element}
|
|
*/
|
|
static linkifyRevealable(revealable, text, fallbackHref) {
|
|
const link = Components.Linkifier._createLink(text, '', UI.MaxLengthForDisplayedURLs, undefined, fallbackHref);
|
|
Components.Linkifier._linkInfo(link).revealable = revealable;
|
|
return link;
|
|
}
|
|
|
|
/**
|
|
* @param {string} text
|
|
* @param {string} className
|
|
* @param {number=} maxLength
|
|
* @param {string=} title
|
|
* @param {string=} href
|
|
* @param {boolean=} preventClick
|
|
* @returns{!Element}
|
|
*/
|
|
static _createLink(text, className, maxLength, title, href, preventClick) {
|
|
const link = createElementWithClass('span', className);
|
|
link.classList.add('devtools-link');
|
|
if (title)
|
|
link.title = title;
|
|
if (href)
|
|
link.href = href;
|
|
Components.Linkifier._setTrimmedText(link, text, maxLength);
|
|
link[Components.Linkifier._infoSymbol] = {
|
|
icon: null,
|
|
enableDecorator: false,
|
|
uiLocation: null,
|
|
liveLocation: null,
|
|
url: href || null,
|
|
lineNumber: null,
|
|
columnNumber: null,
|
|
revealable: null,
|
|
fallback: null
|
|
};
|
|
if (!preventClick)
|
|
link.addEventListener('click', Components.Linkifier._handleClick, false);
|
|
else
|
|
link.classList.add('devtools-link-prevent-click');
|
|
return link;
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} link
|
|
* @param {string} text
|
|
* @param {number=} maxLength
|
|
*/
|
|
static _setTrimmedText(link, text, maxLength) {
|
|
link.removeChildren();
|
|
if (maxLength && text.length > maxLength) {
|
|
const middleSplit = splitMiddle(text, maxLength);
|
|
appendTextWithoutHashes(middleSplit[0]);
|
|
appendHiddenText(middleSplit[1]);
|
|
appendTextWithoutHashes(middleSplit[2]);
|
|
} else {
|
|
appendTextWithoutHashes(text);
|
|
}
|
|
|
|
/**
|
|
* @param {string} string
|
|
*/
|
|
function appendHiddenText(string) {
|
|
const ellipsisNode = link.createChild('span', 'devtools-link-ellipsis').createTextChild('\u2026');
|
|
ellipsisNode[Components.Linkifier._untruncatedNodeTextSymbol] = string;
|
|
}
|
|
|
|
/**
|
|
* @param {string} string
|
|
*/
|
|
function appendTextWithoutHashes(string) {
|
|
const hashSplit = TextUtils.TextUtils.splitStringByRegexes(string, [/[a-f0-9]{20,}/g]);
|
|
for (const match of hashSplit) {
|
|
if (match.regexIndex === -1) {
|
|
link.createTextChild(match.value);
|
|
} else {
|
|
link.createTextChild(match.value.substring(0, 7));
|
|
appendHiddenText(match.value.substring(7));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} string
|
|
* @param {number} maxLength
|
|
* @return {!Array<string>}
|
|
*/
|
|
function splitMiddle(string, maxLength) {
|
|
let leftIndex = Math.floor(maxLength / 2);
|
|
let rightIndex = string.length - Math.ceil(maxLength / 2) + 1;
|
|
|
|
// Do not truncate between characters that use multiple code points (emojis).
|
|
if (string.codePointAt(rightIndex - 1) >= 0x10000) {
|
|
rightIndex++;
|
|
leftIndex++;
|
|
}
|
|
if (leftIndex > 0 && string.codePointAt(leftIndex - 1) >= 0x10000)
|
|
leftIndex--;
|
|
return [string.substring(0, leftIndex), string.substring(leftIndex, rightIndex), string.substring(rightIndex)];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Node} node
|
|
* @return {string}
|
|
*/
|
|
static untruncatedNodeText(node) {
|
|
return node[Components.Linkifier._untruncatedNodeTextSymbol] || node.textContent;
|
|
}
|
|
|
|
/**
|
|
* @param {?Element} link
|
|
* @return {?Components._LinkInfo}
|
|
*/
|
|
static _linkInfo(link) {
|
|
return /** @type {?Components._LinkInfo} */ (link ? link[Components.Linkifier._infoSymbol] || null : null);
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
*/
|
|
static _handleClick(event) {
|
|
const link = /** @type {!Element} */ (event.currentTarget);
|
|
event.consume(true);
|
|
if (UI.isBeingEdited(/** @type {!Node} */ (event.target)) || link.hasSelection())
|
|
return;
|
|
const actions = Components.Linkifier._linkActions(link);
|
|
if (actions.length)
|
|
actions[0].handler.call(null);
|
|
}
|
|
|
|
/**
|
|
* @return {!Common.Setting}
|
|
*/
|
|
static _linkHandlerSetting() {
|
|
if (!Components.Linkifier._linkHandlerSettingInstance) {
|
|
Components.Linkifier._linkHandlerSettingInstance =
|
|
Common.settings.createSetting('openLinkHandler', Common.UIString('auto'));
|
|
}
|
|
return Components.Linkifier._linkHandlerSettingInstance;
|
|
}
|
|
|
|
/**
|
|
* @param {string} title
|
|
* @param {!Components.Linkifier.LinkHandler} handler
|
|
*/
|
|
static registerLinkHandler(title, handler) {
|
|
Components.Linkifier._linkHandlers.set(title, handler);
|
|
self.runtime.sharedInstance(Components.Linkifier.LinkHandlerSettingUI)._update();
|
|
}
|
|
|
|
/**
|
|
* @param {string} title
|
|
*/
|
|
static unregisterLinkHandler(title) {
|
|
Components.Linkifier._linkHandlers.delete(title);
|
|
self.runtime.sharedInstance(Components.Linkifier.LinkHandlerSettingUI)._update();
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} link
|
|
* @return {?Workspace.UILocation}
|
|
*/
|
|
static uiLocation(link) {
|
|
const info = Components.Linkifier._linkInfo(link);
|
|
return info ? info.uiLocation : null;
|
|
}
|
|
|
|
/**
|
|
* @param {?Element} link
|
|
* @return {!Array<{title: string, handler: function()}>}
|
|
*/
|
|
static _linkActions(link) {
|
|
const info = Components.Linkifier._linkInfo(link);
|
|
const result = [];
|
|
if (!info)
|
|
return result;
|
|
|
|
let url = '';
|
|
let uiLocation = null;
|
|
if (info.uiLocation) {
|
|
uiLocation = info.uiLocation;
|
|
url = uiLocation.uiSourceCode.contentURL();
|
|
} else if (info.url) {
|
|
url = info.url;
|
|
const uiSourceCode = Workspace.workspace.uiSourceCodeForURL(url) ||
|
|
Workspace.workspace.uiSourceCodeForURL(Common.ParsedURL.urlWithoutHash(url));
|
|
uiLocation = uiSourceCode ? uiSourceCode.uiLocation(info.lineNumber || 0, info.columnNumber || 0) : null;
|
|
}
|
|
const resource = url ? Bindings.resourceForURL(url) : null;
|
|
const contentProvider = uiLocation ? uiLocation.uiSourceCode : resource;
|
|
|
|
const revealable = info.revealable || uiLocation || resource;
|
|
if (revealable) {
|
|
const destination = Common.Revealer.revealDestination(revealable);
|
|
result.push({
|
|
section: 'reveal',
|
|
title: destination ? ls`Reveal in ${destination}` : ls`Reveal`,
|
|
handler: () => Common.Revealer.reveal(revealable)
|
|
});
|
|
}
|
|
if (contentProvider) {
|
|
const lineNumber = uiLocation ? uiLocation.lineNumber : info.lineNumber || 0;
|
|
for (const title of Components.Linkifier._linkHandlers.keys()) {
|
|
const handler = Components.Linkifier._linkHandlers.get(title);
|
|
const action = {
|
|
section: 'reveal',
|
|
title: Common.UIString('Open using %s', title),
|
|
handler: handler.bind(null, contentProvider, lineNumber)
|
|
};
|
|
if (title === Components.Linkifier._linkHandlerSetting().get())
|
|
result.unshift(action);
|
|
else
|
|
result.push(action);
|
|
}
|
|
}
|
|
if (resource || info.url) {
|
|
result.push({
|
|
section: 'reveal',
|
|
title: UI.openLinkExternallyLabel(),
|
|
handler: () => InspectorFrontendHost.openInNewTab(url)
|
|
});
|
|
result.push(
|
|
{section: 'clipboard', title: UI.copyLinkAddressLabel(), handler: () => InspectorFrontendHost.copyText(url)});
|
|
}
|
|
return result;
|
|
}
|
|
};
|
|
|
|
/** @type {!Set<!Components.Linkifier>} */
|
|
Components.Linkifier._instances = new Set();
|
|
/** @type {?Components.LinkDecorator} */
|
|
Components.Linkifier._decorator = null;
|
|
|
|
Components.Linkifier._sourceCodeAnchors = Symbol('Linkifier.anchors');
|
|
Components.Linkifier._infoSymbol = Symbol('Linkifier.info');
|
|
Components.Linkifier._untruncatedNodeTextSymbol = Symbol('Linkifier.untruncatedNodeText');
|
|
|
|
/**
|
|
* @typedef {{
|
|
* icon: ?UI.Icon,
|
|
* enableDecorator: boolean,
|
|
* uiLocation: ?Workspace.UILocation,
|
|
* liveLocation: ?Bindings.LiveLocation,
|
|
* url: ?string,
|
|
* lineNumber: ?number,
|
|
* columnNumber: ?number,
|
|
* revealable: ?Object,
|
|
* fallback: ?Element
|
|
* }}
|
|
*/
|
|
Components._LinkInfo;
|
|
|
|
/**
|
|
* @typedef {{
|
|
* text: (string|undefined),
|
|
* className: (string|undefined),
|
|
* lineNumber: (number|undefined),
|
|
* columnNumber: (number|undefined),
|
|
* preventClick: (boolean|undefined),
|
|
* maxLength: (number|undefined)
|
|
* }}
|
|
*/
|
|
Components.LinkifyURLOptions;
|
|
|
|
/**
|
|
* The maximum length before strings are considered too long for finding URLs.
|
|
* @const
|
|
* @type {number}
|
|
*/
|
|
Components.Linkifier.MaxLengthToIgnoreLinkifier = 10000;
|
|
|
|
/**
|
|
* @typedef {function(!Common.ContentProvider, number)}
|
|
*/
|
|
Components.Linkifier.LinkHandler;
|
|
|
|
/** @type {!Map<string, !Components.Linkifier.LinkHandler>} */
|
|
Components.Linkifier._linkHandlers = new Map();
|
|
|
|
/**
|
|
* @extends {Common.EventTarget}
|
|
* @interface
|
|
*/
|
|
Components.LinkDecorator = function() {};
|
|
|
|
Components.LinkDecorator.prototype = {
|
|
/**
|
|
* @param {!Workspace.UISourceCode} uiSourceCode
|
|
* @return {?UI.Icon}
|
|
*/
|
|
linkIcon(uiSourceCode) {}
|
|
};
|
|
|
|
Components.LinkDecorator.Events = {
|
|
LinkIconChanged: Symbol('LinkIconChanged')
|
|
};
|
|
|
|
/**
|
|
* @implements {UI.ContextMenu.Provider}
|
|
* @unrestricted
|
|
*/
|
|
Components.Linkifier.LinkContextMenuProvider = class {
|
|
/**
|
|
* @override
|
|
* @param {!Event} event
|
|
* @param {!UI.ContextMenu} contextMenu
|
|
* @param {!Object} target
|
|
*/
|
|
appendApplicableItems(event, contextMenu, target) {
|
|
let targetNode = /** @type {!Node} */ (target);
|
|
while (targetNode && !targetNode[Components.Linkifier._infoSymbol])
|
|
targetNode = targetNode.parentNodeOrShadowHost();
|
|
const link = /** @type {?Element} */ (targetNode);
|
|
const actions = Components.Linkifier._linkActions(link);
|
|
for (const action of actions)
|
|
contextMenu.section(action.section).appendItem(action.title, action.handler);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @implements {UI.SettingUI}
|
|
* @unrestricted
|
|
*/
|
|
Components.Linkifier.LinkHandlerSettingUI = class {
|
|
constructor() {
|
|
this._element = createElementWithClass('select', 'chrome-select');
|
|
this._element.addEventListener('change', this._onChange.bind(this), false);
|
|
this._update();
|
|
}
|
|
|
|
_update() {
|
|
this._element.removeChildren();
|
|
const names = Components.Linkifier._linkHandlers.keysArray();
|
|
names.unshift(Common.UIString('auto'));
|
|
for (const name of names) {
|
|
const option = createElement('option');
|
|
option.textContent = name;
|
|
option.selected = name === Components.Linkifier._linkHandlerSetting().get();
|
|
this._element.appendChild(option);
|
|
}
|
|
this._element.disabled = names.length <= 1;
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
*/
|
|
_onChange(event) {
|
|
const value = event.target.value;
|
|
Components.Linkifier._linkHandlerSetting().set(value);
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {?Element}
|
|
*/
|
|
settingElement() {
|
|
return UI.SettingsUI.createCustomSetting(Common.UIString('Link handling:'), this._element);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @implements {UI.ContextMenu.Provider}
|
|
* @unrestricted
|
|
*/
|
|
Components.Linkifier.ContentProviderContextMenuProvider = class {
|
|
/**
|
|
* @override
|
|
* @param {!Event} event
|
|
* @param {!UI.ContextMenu} contextMenu
|
|
* @param {!Object} target
|
|
*/
|
|
appendApplicableItems(event, contextMenu, target) {
|
|
const contentProvider = /** @type {!Common.ContentProvider} */ (target);
|
|
if (!contentProvider.contentURL())
|
|
return;
|
|
|
|
contextMenu.revealSection().appendItem(
|
|
UI.openLinkExternallyLabel(), () => InspectorFrontendHost.openInNewTab(contentProvider.contentURL()));
|
|
for (const title of Components.Linkifier._linkHandlers.keys()) {
|
|
const handler = Components.Linkifier._linkHandlers.get(title);
|
|
contextMenu.revealSection().appendItem(
|
|
Common.UIString('Open using %s', title), handler.bind(null, contentProvider, 0));
|
|
}
|
|
if (contentProvider instanceof SDK.NetworkRequest)
|
|
return;
|
|
|
|
contextMenu.clipboardSection().appendItem(
|
|
UI.copyLinkAddressLabel(), () => InspectorFrontendHost.copyText(contentProvider.contentURL()));
|
|
}
|
|
};
|