UNPKG

@dcloudio/uni-debugger

Version:

uni-app debugger

781 lines (714 loc) 26.1 kB
/* * 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) { if( sourceURL.indexOf('Runtime.Android.js') !== -1 || sourceURL.indexOf('Runtime.iOS.js') !== -1 ){ return null } 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) { if( sourceURL.indexOf('Runtime.Android.js') !== -1 || sourceURL.indexOf('Runtime.iOS.js') !== -1 ){ return document.createElement('span'); } 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) { if( url && ( url.indexOf('Runtime.Android.js') !== -1 || url.indexOf('Runtime.iOS.js') !== -1 ) ){ return document.createElement('span'); } 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())); } };