| /* |
| * Copyright (C) 2011 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. |
| */ |
| |
| /** |
| * @unrestricted |
| */ |
| Sources.JavaScriptSourceFrame = class extends Sources.UISourceCodeFrame { |
| /** |
| * @param {!Workspace.UISourceCode} uiSourceCode |
| */ |
| constructor(uiSourceCode) { |
| super(uiSourceCode); |
| |
| this._scriptsPanel = Sources.SourcesPanel.instance(); |
| this._breakpointManager = Bindings.breakpointManager; |
| if (uiSourceCode.project().type() === Workspace.projectTypes.Debugger) |
| this.element.classList.add('source-frame-debugger-script'); |
| |
| this._popoverHelper = new Components.ObjectPopoverHelper( |
| this._scriptsPanel.element, this._getPopoverAnchor.bind(this), this._resolveObjectForPopover.bind(this), |
| this._onHidePopover.bind(this), true); |
| this._popoverHelper.setTimeout(250, 250); |
| |
| this.textEditor.element.addEventListener('keydown', this._onKeyDown.bind(this), true); |
| |
| this.textEditor.addEventListener( |
| SourceFrame.SourcesTextEditor.Events.GutterClick, this._handleGutterClick.bind(this), this); |
| |
| this._breakpointManager.addEventListener( |
| Bindings.BreakpointManager.Events.BreakpointAdded, this._breakpointAdded, this); |
| this._breakpointManager.addEventListener( |
| Bindings.BreakpointManager.Events.BreakpointRemoved, this._breakpointRemoved, this); |
| |
| this.uiSourceCode().addEventListener( |
| Workspace.UISourceCode.Events.SourceMappingChanged, this._onSourceMappingChanged, this); |
| this.uiSourceCode().addEventListener( |
| Workspace.UISourceCode.Events.WorkingCopyChanged, this._workingCopyChanged, this); |
| this.uiSourceCode().addEventListener( |
| Workspace.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this); |
| this.uiSourceCode().addEventListener( |
| Workspace.UISourceCode.Events.TitleChanged, this._showBlackboxInfobarIfNeeded, this); |
| |
| /** @type {!Map.<!SDK.Target, !Bindings.ResourceScriptFile>}*/ |
| this._scriptFileForTarget = new Map(); |
| var targets = SDK.targetManager.targets(); |
| for (var i = 0; i < targets.length; ++i) { |
| var scriptFile = Bindings.debuggerWorkspaceBinding.scriptFile(uiSourceCode, targets[i]); |
| if (scriptFile) |
| this._updateScriptFile(targets[i]); |
| } |
| |
| if (this._scriptFileForTarget.size || uiSourceCode.extension() === 'js' || |
| uiSourceCode.project().type() === Workspace.projectTypes.Snippets) |
| this._compiler = new Sources.JavaScriptCompiler(this); |
| |
| Common.moduleSetting('skipStackFramesPattern').addChangeListener(this._showBlackboxInfobarIfNeeded, this); |
| Common.moduleSetting('skipContentScripts').addChangeListener(this._showBlackboxInfobarIfNeeded, this); |
| this._showBlackboxInfobarIfNeeded(); |
| /** @type {!Map.<number, !Element>} */ |
| this._valueWidgets = new Map(); |
| } |
| |
| /** |
| * @override |
| * @return {!Array<!UI.ToolbarItem>} |
| */ |
| syncToolbarItems() { |
| var result = super.syncToolbarItems(); |
| var originURL = Bindings.CompilerScriptMapping.uiSourceCodeOrigin(this.uiSourceCode()); |
| if (originURL) { |
| var parsedURL = originURL.asParsedURL(); |
| if (parsedURL) |
| result.push(new UI.ToolbarText(Common.UIString('(source mapped from %s)', parsedURL.displayName))); |
| } |
| |
| if (this.uiSourceCode().project().type() === Workspace.projectTypes.Snippets) { |
| result.push(new UI.ToolbarSeparator(true)); |
| var runSnippet = UI.Toolbar.createActionButtonForId('debugger.run-snippet'); |
| runSnippet.setText(Host.isMac() ? Common.UIString('\u2318+Enter') : Common.UIString('Ctrl+Enter')); |
| result.push(runSnippet); |
| } |
| |
| return result; |
| } |
| |
| _updateInfobars() { |
| this.attachInfobars([this._blackboxInfobar, this._divergedInfobar]); |
| } |
| |
| _showDivergedInfobar() { |
| if (!this.uiSourceCode().contentType().isScript()) |
| return; |
| |
| if (this._divergedInfobar) |
| this._divergedInfobar.dispose(); |
| |
| var infobar = new UI.Infobar(UI.Infobar.Type.Warning, Common.UIString('Workspace mapping mismatch')); |
| this._divergedInfobar = infobar; |
| |
| var fileURL = this.uiSourceCode().url(); |
| infobar.createDetailsRowMessage(Common.UIString('The content of this file on the file system:\u00a0')) |
| .appendChild(UI.linkifyURLAsNode(fileURL, fileURL, 'source-frame-infobar-details-url', true)); |
| |
| var scriptURL = this.uiSourceCode().url(); |
| infobar.createDetailsRowMessage(Common.UIString('does not match the loaded script:\u00a0')) |
| .appendChild(UI.linkifyURLAsNode(scriptURL, scriptURL, 'source-frame-infobar-details-url', true)); |
| |
| infobar.createDetailsRowMessage(); |
| infobar.createDetailsRowMessage(Common.UIString('Possible solutions are:')); |
| |
| if (Common.moduleSetting('cacheDisabled').get()) { |
| infobar.createDetailsRowMessage(' - ').createTextChild(Common.UIString('Reload inspected page')); |
| } else { |
| infobar.createDetailsRowMessage(' - ').createTextChild(Common.UIString( |
| 'Check "Disable cache" in settings and reload inspected page (recommended setup for authoring and debugging)')); |
| } |
| infobar.createDetailsRowMessage(' - ').createTextChild(Common.UIString( |
| 'Check that your file and script are both loaded from the correct source and their contents match')); |
| |
| this._updateInfobars(); |
| } |
| |
| _hideDivergedInfobar() { |
| if (!this._divergedInfobar) |
| return; |
| this._divergedInfobar.dispose(); |
| delete this._divergedInfobar; |
| } |
| |
| _showBlackboxInfobarIfNeeded() { |
| var uiSourceCode = this.uiSourceCode(); |
| if (!uiSourceCode.contentType().hasScripts()) |
| return; |
| var projectType = uiSourceCode.project().type(); |
| if (projectType === Workspace.projectTypes.Snippets) |
| return; |
| if (!Bindings.blackboxManager.isBlackboxedUISourceCode(uiSourceCode)) { |
| this._hideBlackboxInfobar(); |
| return; |
| } |
| |
| if (this._blackboxInfobar) |
| this._blackboxInfobar.dispose(); |
| |
| var infobar = new UI.Infobar(UI.Infobar.Type.Warning, Common.UIString('This script is blackboxed in debugger')); |
| this._blackboxInfobar = infobar; |
| |
| infobar.createDetailsRowMessage( |
| Common.UIString('Debugger will skip stepping through this script, and will not stop on exceptions')); |
| |
| var scriptFile = this._scriptFileForTarget.size ? this._scriptFileForTarget.valuesArray()[0] : null; |
| if (scriptFile && scriptFile.hasSourceMapURL()) |
| infobar.createDetailsRowMessage(Common.UIString('Source map found, but ignored for blackboxed file.')); |
| infobar.createDetailsRowMessage(); |
| infobar.createDetailsRowMessage(Common.UIString('Possible ways to cancel this behavior are:')); |
| |
| infobar.createDetailsRowMessage(' - ').createTextChild( |
| Common.UIString('Go to "%s" tab in settings', Common.UIString('Blackboxing'))); |
| var unblackboxLink = infobar.createDetailsRowMessage(' - ').createChild('span', 'link'); |
| unblackboxLink.textContent = Common.UIString('Unblackbox this script'); |
| unblackboxLink.addEventListener('click', unblackbox, false); |
| |
| function unblackbox() { |
| Bindings.blackboxManager.unblackboxUISourceCode(uiSourceCode); |
| if (projectType === Workspace.projectTypes.ContentScripts) |
| Bindings.blackboxManager.unblackboxContentScripts(); |
| } |
| |
| this._updateInfobars(); |
| } |
| |
| _hideBlackboxInfobar() { |
| if (!this._blackboxInfobar) |
| return; |
| this._blackboxInfobar.dispose(); |
| delete this._blackboxInfobar; |
| } |
| |
| /** |
| * @override |
| */ |
| wasShown() { |
| super.wasShown(); |
| if (this._executionLocation && this.loaded) { |
| // We need SourcesTextEditor to be initialized prior to this call. @see crbug.com/499889 |
| setImmediate(this._generateValuesInSource.bind(this)); |
| } |
| } |
| |
| /** |
| * @override |
| */ |
| willHide() { |
| super.willHide(); |
| this._popoverHelper.hidePopover(); |
| } |
| |
| /** |
| * @override |
| */ |
| onUISourceCodeContentChanged() { |
| this._removeAllBreakpoints(); |
| super.onUISourceCodeContentChanged(); |
| } |
| |
| /** |
| * @override |
| */ |
| onTextChanged(oldRange, newRange) { |
| this._scriptsPanel.updateLastModificationTime(); |
| super.onTextChanged(oldRange, newRange); |
| if (this._compiler) |
| this._compiler.scheduleCompile(); |
| } |
| |
| /** |
| * @override |
| * @return {!Promise} |
| */ |
| populateLineGutterContextMenu(contextMenu, lineNumber) { |
| /** |
| * @this {Sources.JavaScriptSourceFrame} |
| */ |
| function populate(resolve, reject) { |
| var uiLocation = new Workspace.UILocation(this.uiSourceCode(), lineNumber, 0); |
| this._scriptsPanel.appendUILocationItems(contextMenu, uiLocation); |
| var breakpoints = this._breakpointManager.findBreakpoints(this.uiSourceCode(), lineNumber); |
| if (!breakpoints.length) { |
| // This row doesn't have a breakpoint: We want to show Add Breakpoint and Add and Edit Breakpoint. |
| contextMenu.appendItem( |
| Common.UIString('Add breakpoint'), this._createNewBreakpoint.bind(this, lineNumber, '', true)); |
| contextMenu.appendItem( |
| Common.UIString('Add conditional breakpoint…'), this._editBreakpointCondition.bind(this, lineNumber)); |
| contextMenu.appendItem( |
| Common.UIString('Never pause here'), this._createNewBreakpoint.bind(this, lineNumber, 'false', true)); |
| } else { |
| var breakpoint = breakpoints[0]; |
| |
| // This row has a breakpoint, we want to show edit and remove breakpoint, and either disable or enable. |
| contextMenu.appendItem(Common.UIString('Remove breakpoint'), breakpoint.remove.bind(breakpoint)); |
| contextMenu.appendItem( |
| Common.UIString('Edit breakpoint…'), this._editBreakpointCondition.bind(this, lineNumber, breakpoint)); |
| if (breakpoint.enabled()) |
| contextMenu.appendItem(Common.UIString('Disable breakpoint'), breakpoint.setEnabled.bind(breakpoint, false)); |
| else |
| contextMenu.appendItem(Common.UIString('Enable breakpoint'), breakpoint.setEnabled.bind(breakpoint, true)); |
| } |
| resolve(); |
| } |
| return new Promise(populate.bind(this)); |
| } |
| |
| /** |
| * @override |
| * @return {!Promise} |
| */ |
| populateTextAreaContextMenu(contextMenu, lineNumber, columnNumber) { |
| /** |
| * @param {!Bindings.ResourceScriptFile} scriptFile |
| */ |
| function addSourceMapURL(scriptFile) { |
| Sources.AddSourceMapURLDialog.show(addSourceMapURLDialogCallback.bind(null, scriptFile)); |
| } |
| |
| /** |
| * @param {!Bindings.ResourceScriptFile} scriptFile |
| * @param {string} url |
| */ |
| function addSourceMapURLDialogCallback(scriptFile, url) { |
| if (!url) |
| return; |
| scriptFile.addSourceMapURL(url); |
| } |
| |
| /** |
| * @this {Sources.JavaScriptSourceFrame} |
| */ |
| function populateSourceMapMembers() { |
| if (this.uiSourceCode().project().type() === Workspace.projectTypes.Network && |
| Common.moduleSetting('jsSourceMapsEnabled').get() && |
| !Bindings.blackboxManager.isBlackboxedUISourceCode(this.uiSourceCode())) { |
| if (this._scriptFileForTarget.size) { |
| var scriptFile = this._scriptFileForTarget.valuesArray()[0]; |
| var addSourceMapURLLabel = Common.UIString.capitalize('Add ^source ^map\u2026'); |
| contextMenu.appendItem(addSourceMapURLLabel, addSourceMapURL.bind(null, scriptFile)); |
| contextMenu.appendSeparator(); |
| } |
| } |
| } |
| |
| return super.populateTextAreaContextMenu(contextMenu, lineNumber, columnNumber) |
| .then(populateSourceMapMembers.bind(this)); |
| } |
| |
| _workingCopyChanged(event) { |
| if (this._supportsEnabledBreakpointsWhileEditing() || this._scriptFileForTarget.size) |
| return; |
| |
| if (this.uiSourceCode().isDirty()) |
| this._muteBreakpointsWhileEditing(); |
| else |
| this._restoreBreakpointsAfterEditing(); |
| } |
| |
| _workingCopyCommitted(event) { |
| this._scriptsPanel.updateLastModificationTime(); |
| if (this._supportsEnabledBreakpointsWhileEditing()) |
| return; |
| |
| if (!this._scriptFileForTarget.size) |
| this._restoreBreakpointsAfterEditing(); |
| } |
| |
| _didMergeToVM() { |
| if (this._supportsEnabledBreakpointsWhileEditing()) |
| return; |
| this._updateDivergedInfobar(); |
| this._restoreBreakpointsIfConsistentScripts(); |
| } |
| |
| _didDivergeFromVM() { |
| if (this._supportsEnabledBreakpointsWhileEditing()) |
| return; |
| this._updateDivergedInfobar(); |
| this._muteBreakpointsWhileEditing(); |
| } |
| |
| _muteBreakpointsWhileEditing() { |
| if (this._muted) |
| return; |
| for (var lineNumber = 0; lineNumber < this._textEditor.linesCount; ++lineNumber) { |
| var breakpointDecoration = this._textEditor.getAttribute(lineNumber, 'breakpoint'); |
| if (!breakpointDecoration) |
| continue; |
| this._removeBreakpointDecoration(lineNumber); |
| this._addBreakpointDecoration( |
| lineNumber, breakpointDecoration.columnNumber, breakpointDecoration.condition, breakpointDecoration.enabled, |
| true); |
| } |
| this._muted = true; |
| } |
| |
| _updateDivergedInfobar() { |
| if (this.uiSourceCode().project().type() !== Workspace.projectTypes.FileSystem) { |
| this._hideDivergedInfobar(); |
| return; |
| } |
| |
| var scriptFiles = this._scriptFileForTarget.valuesArray(); |
| var hasDivergedScript = false; |
| for (var i = 0; i < scriptFiles.length; ++i) |
| hasDivergedScript = hasDivergedScript || scriptFiles[i].hasDivergedFromVM(); |
| |
| if (this._divergedInfobar) { |
| if (!hasDivergedScript) |
| this._hideDivergedInfobar(); |
| } else { |
| if (hasDivergedScript && !this.uiSourceCode().isDirty()) |
| this._showDivergedInfobar(); |
| } |
| } |
| |
| _supportsEnabledBreakpointsWhileEditing() { |
| return this.uiSourceCode().project().type() === Workspace.projectTypes.Snippets; |
| } |
| |
| _restoreBreakpointsIfConsistentScripts() { |
| var scriptFiles = this._scriptFileForTarget.valuesArray(); |
| for (var i = 0; i < scriptFiles.length; ++i) { |
| if (scriptFiles[i].hasDivergedFromVM() || scriptFiles[i].isMergingToVM()) |
| return; |
| } |
| |
| this._restoreBreakpointsAfterEditing(); |
| } |
| |
| _restoreBreakpointsAfterEditing() { |
| delete this._muted; |
| var breakpoints = {}; |
| // Save and remove muted breakpoint decorations. |
| for (var lineNumber = 0; lineNumber < this._textEditor.linesCount; ++lineNumber) { |
| var breakpointDecoration = this._textEditor.getAttribute(lineNumber, 'breakpoint'); |
| if (breakpointDecoration) { |
| breakpoints[lineNumber] = breakpointDecoration; |
| this._removeBreakpointDecoration(lineNumber); |
| } |
| } |
| |
| // Remove all breakpoints. |
| this._removeAllBreakpoints(); |
| |
| // Restore all breakpoints from saved decorations. |
| for (var lineNumberString in breakpoints) { |
| var lineNumber = parseInt(lineNumberString, 10); |
| if (isNaN(lineNumber)) |
| continue; |
| var breakpointDecoration = breakpoints[lineNumberString]; |
| this._setBreakpoint( |
| lineNumber, breakpointDecoration.columnNumber, breakpointDecoration.condition, breakpointDecoration.enabled); |
| } |
| } |
| |
| _removeAllBreakpoints() { |
| var breakpoints = this._breakpointManager.breakpointsForUISourceCode(this.uiSourceCode()); |
| for (var i = 0; i < breakpoints.length; ++i) |
| breakpoints[i].remove(); |
| } |
| |
| /** |
| * @param {string} tokenType |
| * @return {boolean} |
| */ |
| _isIdentifier(tokenType) { |
| return tokenType.startsWith('js-variable') || tokenType.startsWith('js-property') || tokenType === 'js-def'; |
| } |
| |
| _getPopoverAnchor(element, event) { |
| var target = UI.context.flavor(SDK.Target); |
| var debuggerModel = SDK.DebuggerModel.fromTarget(target); |
| if (!debuggerModel || !debuggerModel.isPaused()) |
| return; |
| |
| var textPosition = this.textEditor.coordinatesToCursorPosition(event.x, event.y); |
| if (!textPosition) |
| return; |
| var mouseLine = textPosition.startLine; |
| var mouseColumn = textPosition.startColumn; |
| var textSelection = this.textEditor.selection().normalize(); |
| if (textSelection && !textSelection.isEmpty()) { |
| if (textSelection.startLine !== textSelection.endLine || textSelection.startLine !== mouseLine || |
| mouseColumn < textSelection.startColumn || mouseColumn > textSelection.endColumn) |
| return; |
| |
| var leftCorner = this.textEditor.cursorPositionToCoordinates(textSelection.startLine, textSelection.startColumn); |
| var rightCorner = this.textEditor.cursorPositionToCoordinates(textSelection.endLine, textSelection.endColumn); |
| var anchorBox = new AnchorBox(leftCorner.x, leftCorner.y, rightCorner.x - leftCorner.x, leftCorner.height); |
| anchorBox.highlight = { |
| lineNumber: textSelection.startLine, |
| startColumn: textSelection.startColumn, |
| endColumn: textSelection.endColumn - 1 |
| }; |
| anchorBox.forSelection = true; |
| return anchorBox; |
| } |
| |
| var token = this.textEditor.tokenAtTextPosition(textPosition.startLine, textPosition.startColumn); |
| if (!token || !token.type) |
| return; |
| var lineNumber = textPosition.startLine; |
| var line = this.textEditor.line(lineNumber); |
| var tokenContent = line.substring(token.startColumn, token.endColumn); |
| |
| var isIdentifier = this._isIdentifier(token.type); |
| if (!isIdentifier && (token.type !== 'js-keyword' || tokenContent !== 'this')) |
| return; |
| |
| var leftCorner = this.textEditor.cursorPositionToCoordinates(lineNumber, token.startColumn); |
| var rightCorner = this.textEditor.cursorPositionToCoordinates(lineNumber, token.endColumn - 1); |
| var anchorBox = new AnchorBox(leftCorner.x, leftCorner.y, rightCorner.x - leftCorner.x, leftCorner.height); |
| |
| anchorBox.highlight = {lineNumber: lineNumber, startColumn: token.startColumn, endColumn: token.endColumn - 1}; |
| |
| return anchorBox; |
| } |
| |
| _resolveObjectForPopover(anchorBox, showCallback, objectGroupName) { |
| var target = UI.context.flavor(SDK.Target); |
| var selectedCallFrame = UI.context.flavor(SDK.DebuggerModel.CallFrame); |
| if (!selectedCallFrame) { |
| this._popoverHelper.hidePopover(); |
| return; |
| } |
| var lineNumber = anchorBox.highlight.lineNumber; |
| var startHighlight = anchorBox.highlight.startColumn; |
| var endHighlight = anchorBox.highlight.endColumn; |
| var line = this.textEditor.line(lineNumber); |
| if (!anchorBox.forSelection) { |
| while (startHighlight > 1 && line.charAt(startHighlight - 1) === '.') { |
| var token = this.textEditor.tokenAtTextPosition(lineNumber, startHighlight - 2); |
| if (!token || !token.type) { |
| this._popoverHelper.hidePopover(); |
| return; |
| } |
| startHighlight = token.startColumn; |
| } |
| } |
| var evaluationText = line.substring(startHighlight, endHighlight + 1); |
| Sources.SourceMapNamesResolver |
| .resolveExpression( |
| selectedCallFrame, evaluationText, this.uiSourceCode(), lineNumber, startHighlight, endHighlight) |
| .then(onResolve.bind(this)); |
| |
| /** |
| * @param {?string=} text |
| * @this {Sources.JavaScriptSourceFrame} |
| */ |
| function onResolve(text) { |
| selectedCallFrame.evaluate( |
| text || evaluationText, objectGroupName, false, true, false, false, showObjectPopover.bind(this)); |
| } |
| |
| /** |
| * @param {?Protocol.Runtime.RemoteObject} result |
| * @param {!Protocol.Runtime.ExceptionDetails=} exceptionDetails |
| * @this {Sources.JavaScriptSourceFrame} |
| */ |
| function showObjectPopover(result, exceptionDetails) { |
| var target = UI.context.flavor(SDK.Target); |
| var potentiallyUpdatedCallFrame = UI.context.flavor(SDK.DebuggerModel.CallFrame); |
| if (selectedCallFrame !== potentiallyUpdatedCallFrame || !result) { |
| this._popoverHelper.hidePopover(); |
| return; |
| } |
| this._popoverAnchorBox = anchorBox; |
| showCallback(target.runtimeModel.createRemoteObject(result), !!exceptionDetails, this._popoverAnchorBox); |
| // Popover may have been removed by showCallback(). |
| if (this._popoverAnchorBox) { |
| var highlightRange = new Common.TextRange(lineNumber, startHighlight, lineNumber, endHighlight); |
| this._popoverAnchorBox._highlightDescriptor = |
| this.textEditor.highlightRange(highlightRange, 'source-frame-eval-expression'); |
| } |
| } |
| } |
| |
| _onHidePopover() { |
| if (!this._popoverAnchorBox) |
| return; |
| if (this._popoverAnchorBox._highlightDescriptor) |
| this.textEditor.removeHighlight(this._popoverAnchorBox._highlightDescriptor); |
| delete this._popoverAnchorBox; |
| } |
| |
| /** |
| * @param {number} lineNumber |
| * @param {number} columnNumber |
| * @param {string} condition |
| * @param {boolean} enabled |
| * @param {boolean} mutedWhileEditing |
| */ |
| _addBreakpointDecoration(lineNumber, columnNumber, condition, enabled, mutedWhileEditing) { |
| var breakpoint = {condition: condition, enabled: enabled, columnNumber: columnNumber}; |
| |
| this.textEditor.setAttribute(lineNumber, 'breakpoint', breakpoint); |
| |
| var disabled = !enabled || mutedWhileEditing; |
| this.textEditor.addBreakpoint(lineNumber, disabled, !!condition); |
| } |
| |
| _removeBreakpointDecoration(lineNumber) { |
| this.textEditor.removeAttribute(lineNumber, 'breakpoint'); |
| this.textEditor.removeBreakpoint(lineNumber); |
| } |
| |
| _onKeyDown(event) { |
| if (event.key === 'Escape') { |
| if (this._popoverHelper.isPopoverVisible()) { |
| this._popoverHelper.hidePopover(); |
| event.consume(); |
| } |
| } |
| } |
| |
| /** |
| * @param {number} lineNumber |
| * @param {!Bindings.BreakpointManager.Breakpoint=} breakpoint |
| */ |
| _editBreakpointCondition(lineNumber, breakpoint) { |
| this._conditionElement = this._createConditionElement(lineNumber); |
| this.textEditor.addDecoration(this._conditionElement, lineNumber); |
| |
| /** |
| * @this {Sources.JavaScriptSourceFrame} |
| */ |
| function finishEditing(committed, element, newText) { |
| this.textEditor.removeDecoration(this._conditionElement, lineNumber); |
| delete this._conditionEditorElement; |
| delete this._conditionElement; |
| if (!committed) |
| return; |
| |
| if (breakpoint) |
| breakpoint.setCondition(newText); |
| else |
| this._createNewBreakpoint(lineNumber, newText, true); |
| } |
| |
| var config = new UI.InplaceEditor.Config(finishEditing.bind(this, true), finishEditing.bind(this, false)); |
| UI.InplaceEditor.startEditing(this._conditionEditorElement, config); |
| this._conditionEditorElement.value = breakpoint ? breakpoint.condition() : ''; |
| this._conditionEditorElement.select(); |
| } |
| |
| _createConditionElement(lineNumber) { |
| var conditionElement = createElementWithClass('div', 'source-frame-breakpoint-condition'); |
| |
| var labelElement = conditionElement.createChild('label', 'source-frame-breakpoint-message'); |
| labelElement.htmlFor = 'source-frame-breakpoint-condition'; |
| labelElement.createTextChild( |
| Common.UIString('The breakpoint on line %d will stop only if this expression is true:', lineNumber + 1)); |
| |
| var editorElement = conditionElement.createChild('input', 'monospace'); |
| editorElement.id = 'source-frame-breakpoint-condition'; |
| editorElement.type = 'text'; |
| this._conditionEditorElement = editorElement; |
| |
| return conditionElement; |
| } |
| |
| /** |
| * @param {!Workspace.UILocation} uiLocation |
| */ |
| setExecutionLocation(uiLocation) { |
| this._executionLocation = uiLocation; |
| if (!this.loaded) |
| return; |
| |
| this.textEditor.setExecutionLocation(uiLocation.lineNumber, uiLocation.columnNumber); |
| if (this.isShowing()) { |
| // We need SourcesTextEditor to be initialized prior to this call. @see crbug.com/506566 |
| setImmediate(this._generateValuesInSource.bind(this)); |
| } |
| } |
| |
| _generateValuesInSource() { |
| if (!Common.moduleSetting('inlineVariableValues').get()) |
| return; |
| var executionContext = UI.context.flavor(SDK.ExecutionContext); |
| if (!executionContext) |
| return; |
| var callFrame = UI.context.flavor(SDK.DebuggerModel.CallFrame); |
| if (!callFrame) |
| return; |
| |
| var localScope = callFrame.localScope(); |
| var functionLocation = callFrame.functionLocation(); |
| if (localScope && functionLocation) { |
| Sources.SourceMapNamesResolver.resolveScopeInObject(localScope) |
| .getAllProperties(false, this._prepareScopeVariables.bind(this, callFrame)); |
| } |
| |
| if (this._clearValueWidgetsTimer) { |
| clearTimeout(this._clearValueWidgetsTimer); |
| delete this._clearValueWidgetsTimer; |
| } |
| } |
| |
| /** |
| * @param {!SDK.DebuggerModel.CallFrame} callFrame |
| * @param {?Array.<!SDK.RemoteObjectProperty>} properties |
| * @param {?Array.<!SDK.RemoteObjectProperty>} internalProperties |
| */ |
| _prepareScopeVariables(callFrame, properties, internalProperties) { |
| if (!properties || !properties.length || properties.length > 500) { |
| this._clearValueWidgets(); |
| return; |
| } |
| |
| var functionUILocation = Bindings.debuggerWorkspaceBinding.rawLocationToUILocation( |
| /** @type {!SDK.DebuggerModel.Location} */ (callFrame.functionLocation())); |
| var executionUILocation = Bindings.debuggerWorkspaceBinding.rawLocationToUILocation(callFrame.location()); |
| if (functionUILocation.uiSourceCode !== this.uiSourceCode() || |
| executionUILocation.uiSourceCode !== this.uiSourceCode()) { |
| this._clearValueWidgets(); |
| return; |
| } |
| |
| var fromLine = functionUILocation.lineNumber; |
| var fromColumn = functionUILocation.columnNumber; |
| var toLine = executionUILocation.lineNumber; |
| |
| // Make sure we have a chance to update all existing widgets. |
| if (this._valueWidgets) { |
| for (var line of this._valueWidgets.keys()) |
| toLine = Math.max(toLine, line + 1); |
| } |
| if (fromLine >= toLine || toLine - fromLine > 500 || fromLine < 0 || toLine >= this.textEditor.linesCount) { |
| this._clearValueWidgets(); |
| return; |
| } |
| |
| var valuesMap = new Map(); |
| for (var property of properties) |
| valuesMap.set(property.name, property.value); |
| |
| /** @type {!Map.<number, !Set<string>>} */ |
| var namesPerLine = new Map(); |
| var skipObjectProperty = false; |
| var tokenizer = new TextEditor.CodeMirrorUtils.TokenizerFactory().createTokenizer('text/javascript'); |
| tokenizer(this.textEditor.line(fromLine).substring(fromColumn), processToken.bind(this, fromLine)); |
| for (var i = fromLine + 1; i < toLine; ++i) |
| tokenizer(this.textEditor.line(i), processToken.bind(this, i)); |
| |
| /** |
| * @param {number} lineNumber |
| * @param {string} tokenValue |
| * @param {?string} tokenType |
| * @param {number} column |
| * @param {number} newColumn |
| * @this {Sources.JavaScriptSourceFrame} |
| */ |
| function processToken(lineNumber, tokenValue, tokenType, column, newColumn) { |
| if (!skipObjectProperty && tokenType && this._isIdentifier(tokenType) && valuesMap.get(tokenValue)) { |
| var names = namesPerLine.get(lineNumber); |
| if (!names) { |
| names = new Set(); |
| namesPerLine.set(lineNumber, names); |
| } |
| names.add(tokenValue); |
| } |
| skipObjectProperty = tokenValue === '.'; |
| } |
| this.textEditor.operation(this._renderDecorations.bind(this, valuesMap, namesPerLine, fromLine, toLine)); |
| } |
| |
| /** |
| * @param {!Map.<string,!SDK.RemoteObject>} valuesMap |
| * @param {!Map.<number, !Set<string>>} namesPerLine |
| * @param {number} fromLine |
| * @param {number} toLine |
| */ |
| _renderDecorations(valuesMap, namesPerLine, fromLine, toLine) { |
| var formatter = new Components.RemoteObjectPreviewFormatter(); |
| for (var i = fromLine; i < toLine; ++i) { |
| var names = namesPerLine.get(i); |
| var oldWidget = this._valueWidgets.get(i); |
| if (!names) { |
| if (oldWidget) { |
| this._valueWidgets.delete(i); |
| this.textEditor.removeDecoration(oldWidget, i); |
| } |
| continue; |
| } |
| |
| var widget = createElementWithClass('div', 'text-editor-value-decoration'); |
| var base = this.textEditor.cursorPositionToCoordinates(i, 0); |
| var offset = this.textEditor.cursorPositionToCoordinates(i, this.textEditor.line(i).length); |
| var codeMirrorLinesLeftPadding = 4; |
| var left = offset.x - base.x + codeMirrorLinesLeftPadding; |
| widget.style.left = left + 'px'; |
| widget.__nameToToken = new Map(); |
| |
| var renderedNameCount = 0; |
| for (var name of names) { |
| if (renderedNameCount > 10) |
| break; |
| if (namesPerLine.get(i - 1) && namesPerLine.get(i - 1).has(name)) |
| continue; // Only render name once in the given continuous block. |
| if (renderedNameCount) |
| widget.createTextChild(', '); |
| var nameValuePair = widget.createChild('span'); |
| widget.__nameToToken.set(name, nameValuePair); |
| nameValuePair.createTextChild(name + ' = '); |
| var value = valuesMap.get(name); |
| var propertyCount = value.preview ? value.preview.properties.length : 0; |
| var entryCount = value.preview && value.preview.entries ? value.preview.entries.length : 0; |
| if (value.preview && propertyCount + entryCount < 10) |
| formatter.appendObjectPreview(nameValuePair, value.preview); |
| else |
| nameValuePair.appendChild(Components.ObjectPropertiesSection.createValueElement(value, false)); |
| ++renderedNameCount; |
| } |
| |
| var widgetChanged = true; |
| if (oldWidget) { |
| widgetChanged = false; |
| for (var name of widget.__nameToToken.keys()) { |
| var oldText = oldWidget.__nameToToken.get(name) ? oldWidget.__nameToToken.get(name).textContent : ''; |
| var newText = widget.__nameToToken.get(name) ? widget.__nameToToken.get(name).textContent : ''; |
| if (newText !== oldText) { |
| widgetChanged = true; |
| // value has changed, update it. |
| UI.runCSSAnimationOnce( |
| /** @type {!Element} */ (widget.__nameToToken.get(name)), 'source-frame-value-update-highlight'); |
| } |
| } |
| if (widgetChanged) { |
| this._valueWidgets.delete(i); |
| this.textEditor.removeDecoration(oldWidget, i); |
| } |
| } |
| if (widgetChanged) { |
| this._valueWidgets.set(i, widget); |
| this.textEditor.addDecoration(widget, i); |
| } |
| } |
| } |
| |
| clearExecutionLine() { |
| if (this.loaded && this._executionLocation) |
| this.textEditor.clearExecutionLine(); |
| delete this._executionLocation; |
| this._clearValueWidgetsTimer = setTimeout(this._clearValueWidgets.bind(this), 1000); |
| } |
| |
| _clearValueWidgets() { |
| delete this._clearValueWidgetsTimer; |
| for (var line of this._valueWidgets.keys()) |
| this.textEditor.removeDecoration(this._valueWidgets.get(line), line); |
| this._valueWidgets.clear(); |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| _shouldIgnoreExternalBreakpointEvents() { |
| if (this._supportsEnabledBreakpointsWhileEditing()) |
| return false; |
| if (this._muted) |
| return true; |
| var scriptFiles = this._scriptFileForTarget.valuesArray(); |
| for (var i = 0; i < scriptFiles.length; ++i) { |
| if (scriptFiles[i].isDivergingFromVM() || scriptFiles[i].isMergingToVM()) |
| return true; |
| } |
| return false; |
| } |
| |
| _breakpointAdded(event) { |
| var uiLocation = /** @type {!Workspace.UILocation} */ (event.data.uiLocation); |
| if (uiLocation.uiSourceCode !== this.uiSourceCode()) |
| return; |
| if (this._shouldIgnoreExternalBreakpointEvents()) |
| return; |
| |
| var breakpoint = /** @type {!Bindings.BreakpointManager.Breakpoint} */ (event.data.breakpoint); |
| if (this.loaded) { |
| this._addBreakpointDecoration( |
| uiLocation.lineNumber, uiLocation.columnNumber, breakpoint.condition(), breakpoint.enabled(), false); |
| } |
| } |
| |
| _breakpointRemoved(event) { |
| var uiLocation = /** @type {!Workspace.UILocation} */ (event.data.uiLocation); |
| if (uiLocation.uiSourceCode !== this.uiSourceCode()) |
| return; |
| if (this._shouldIgnoreExternalBreakpointEvents()) |
| return; |
| |
| var remainingBreakpoints = this._breakpointManager.findBreakpoints(this.uiSourceCode(), uiLocation.lineNumber); |
| if (!remainingBreakpoints.length && this.loaded) |
| this._removeBreakpointDecoration(uiLocation.lineNumber); |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _onSourceMappingChanged(event) { |
| var data = /** @type {{target: !SDK.Target}} */ (event.data); |
| this._updateScriptFile(data.target); |
| this._updateLinesWithoutMappingHighlight(); |
| } |
| |
| _updateLinesWithoutMappingHighlight() { |
| var linesCount = this.textEditor.linesCount; |
| for (var i = 0; i < linesCount; ++i) { |
| var lineHasMapping = Bindings.debuggerWorkspaceBinding.uiLineHasMapping(this.uiSourceCode(), i); |
| if (!lineHasMapping) |
| this._hasLineWithoutMapping = true; |
| if (this._hasLineWithoutMapping) |
| this.textEditor.toggleLineClass(i, 'cm-line-without-source-mapping', !lineHasMapping); |
| } |
| } |
| |
| /** |
| * @param {!SDK.Target} target |
| */ |
| _updateScriptFile(target) { |
| var oldScriptFile = this._scriptFileForTarget.get(target); |
| var newScriptFile = Bindings.debuggerWorkspaceBinding.scriptFile(this.uiSourceCode(), target); |
| this._scriptFileForTarget.remove(target); |
| if (oldScriptFile) { |
| oldScriptFile.removeEventListener(Bindings.ResourceScriptFile.Events.DidMergeToVM, this._didMergeToVM, this); |
| oldScriptFile.removeEventListener( |
| Bindings.ResourceScriptFile.Events.DidDivergeFromVM, this._didDivergeFromVM, this); |
| if (this._muted && !this.uiSourceCode().isDirty()) |
| this._restoreBreakpointsIfConsistentScripts(); |
| } |
| if (newScriptFile) |
| this._scriptFileForTarget.set(target, newScriptFile); |
| |
| this._updateDivergedInfobar(); |
| |
| if (newScriptFile) { |
| newScriptFile.addEventListener(Bindings.ResourceScriptFile.Events.DidMergeToVM, this._didMergeToVM, this); |
| newScriptFile.addEventListener(Bindings.ResourceScriptFile.Events.DidDivergeFromVM, this._didDivergeFromVM, this); |
| if (this.loaded) |
| newScriptFile.checkMapping(); |
| if (newScriptFile.hasSourceMapURL()) { |
| var sourceMapInfobar = UI.Infobar.create( |
| UI.Infobar.Type.Info, Common.UIString('Source Map detected.'), |
| Common.settings.createSetting('sourceMapInfobarDisabled', false)); |
| if (sourceMapInfobar) { |
| sourceMapInfobar.createDetailsRowMessage(Common.UIString( |
| 'Associated files should be added to the file tree. You can debug these resolved source files as regular JavaScript files.')); |
| sourceMapInfobar.createDetailsRowMessage(Common.UIString( |
| 'Associated files are available via file tree or %s.', |
| UI.shortcutRegistry.shortcutTitleForAction('sources.go-to-source'))); |
| this.attachInfobars([sourceMapInfobar]); |
| } |
| } |
| } |
| } |
| |
| /** |
| * @override |
| */ |
| onTextEditorContentSet() { |
| super.onTextEditorContentSet(); |
| if (this._executionLocation) |
| this.setExecutionLocation(this._executionLocation); |
| |
| var breakpointLocations = this._breakpointManager.breakpointLocationsForUISourceCode(this.uiSourceCode()); |
| for (var i = 0; i < breakpointLocations.length; ++i) |
| this._breakpointAdded({data: breakpointLocations[i]}); |
| |
| var scriptFiles = this._scriptFileForTarget.valuesArray(); |
| for (var i = 0; i < scriptFiles.length; ++i) |
| scriptFiles[i].checkMapping(); |
| |
| this._updateLinesWithoutMappingHighlight(); |
| this._detectMinified(); |
| } |
| |
| _detectMinified() { |
| if (this._prettyPrintInfobar) |
| return; |
| |
| var minified = false; |
| for (var i = 0; i < 10 && i < this.textEditor.linesCount; ++i) { |
| var line = this.textEditor.line(i); |
| if (line.startsWith('//#')) // mind source map. |
| continue; |
| if (line.length > 500) { |
| minified = true; |
| break; |
| } |
| } |
| if (!minified) |
| return; |
| |
| this._prettyPrintInfobar = UI.Infobar.create( |
| UI.Infobar.Type.Info, Common.UIString('Pretty-print this minified file?'), |
| Common.settings.createSetting('prettyPrintInfobarDisabled', false)); |
| if (!this._prettyPrintInfobar) |
| return; |
| |
| this._prettyPrintInfobar.setCloseCallback(() => delete this._prettyPrintInfobar); |
| var toolbar = new UI.Toolbar(''); |
| var button = new UI.ToolbarButton('', 'largeicon-pretty-print'); |
| toolbar.appendToolbarItem(button); |
| toolbar.element.style.display = 'inline-block'; |
| toolbar.element.style.verticalAlign = 'middle'; |
| toolbar.element.style.marginBottom = '3px'; |
| toolbar.element.style.pointerEvents = 'none'; |
| var element = this._prettyPrintInfobar.createDetailsRowMessage(); |
| element.appendChild(UI.formatLocalized( |
| 'You can click the %s button on the bottom status bar, and continue debugging with the new formatted source.', |
| [toolbar.element])); |
| this.attachInfobars([this._prettyPrintInfobar]); |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _handleGutterClick(event) { |
| if (this._muted) |
| return; |
| |
| var eventData = /** @type {!SourceFrame.SourcesTextEditor.GutterClickEventData} */ (event.data); |
| var lineNumber = eventData.lineNumber; |
| var eventObject = eventData.event; |
| |
| if (eventObject.button !== 0 || eventObject.altKey || eventObject.ctrlKey || eventObject.metaKey) |
| return; |
| |
| this._toggleBreakpoint(lineNumber, eventObject.shiftKey); |
| eventObject.consume(true); |
| } |
| |
| /** |
| * @param {number} lineNumber |
| * @param {boolean} onlyDisable |
| */ |
| _toggleBreakpoint(lineNumber, onlyDisable) { |
| var breakpoints = this._breakpointManager.findBreakpoints(this.uiSourceCode(), lineNumber); |
| if (breakpoints.length) { |
| if (onlyDisable) |
| breakpoints[0].setEnabled(!breakpoints[0].enabled()); |
| else |
| breakpoints[0].remove(); |
| } else { |
| this._createNewBreakpoint(lineNumber, '', true); |
| } |
| } |
| |
| /** |
| * @param {number} lineNumber |
| * @param {string} condition |
| * @param {boolean} enabled |
| */ |
| _createNewBreakpoint(lineNumber, condition, enabled) { |
| findPossibleBreakpoints.call(this, lineNumber) |
| .then(checkNextLineIfNeeded.bind(this, lineNumber, 4)) |
| .then(setBreakpoint.bind(this)); |
| |
| /** |
| * @this {!Sources.JavaScriptSourceFrame} |
| * @param {number} lineNumber |
| * @return {!Promise<?Array<!Workspace.UILocation>>} |
| */ |
| function findPossibleBreakpoints(lineNumber) { |
| const maxLengthToCheck = 1024; |
| if (lineNumber >= this._textEditor.linesCount) |
| return Promise.resolve(/** @type {?Array<!Workspace.UILocation>} */ ([])); |
| if (this._textEditor.line(lineNumber).length >= maxLengthToCheck) |
| return Promise.resolve(/** @type {?Array<!Workspace.UILocation>} */ ([])); |
| return this._breakpointManager |
| .possibleBreakpoints(this.uiSourceCode(), new Common.TextRange(lineNumber, 0, lineNumber + 1, 0)) |
| .then(locations => locations.length ? locations : null); |
| } |
| |
| /** |
| * @this {!Sources.JavaScriptSourceFrame} |
| * @param {number} currentLineNumber |
| * @param {number} linesToCheck |
| * @param {?Array<!Workspace.UILocation>} locations |
| * @return {!Promise<?Array<!Workspace.UILocation>>} |
| */ |
| function checkNextLineIfNeeded(currentLineNumber, linesToCheck, locations) { |
| if (locations || linesToCheck <= 0) |
| return Promise.resolve(locations); |
| return findPossibleBreakpoints.call(this, currentLineNumber + 1) |
| .then(checkNextLineIfNeeded.bind(this, currentLineNumber + 1, linesToCheck - 1)); |
| } |
| |
| /** |
| * @this {!Sources.JavaScriptSourceFrame} |
| * @param {?Array<!Workspace.UILocation>} locations |
| */ |
| function setBreakpoint(locations) { |
| if (!locations || !locations.length) |
| this._setBreakpoint(lineNumber, 0, condition, enabled); |
| else |
| this._setBreakpoint(locations[0].lineNumber, locations[0].columnNumber, condition, enabled); |
| Host.userMetrics.actionTaken(Host.UserMetrics.Action.ScriptsBreakpointSet); |
| } |
| } |
| |
| toggleBreakpointOnCurrentLine() { |
| if (this._muted) |
| return; |
| |
| var selection = this.textEditor.selection(); |
| if (!selection) |
| return; |
| this._toggleBreakpoint(selection.startLine, false); |
| } |
| |
| /** |
| * @param {number} lineNumber |
| * @param {number} columnNumber |
| * @param {string} condition |
| * @param {boolean} enabled |
| */ |
| _setBreakpoint(lineNumber, columnNumber, condition, enabled) { |
| if (!Bindings.debuggerWorkspaceBinding.uiLineHasMapping(this.uiSourceCode(), lineNumber)) |
| return; |
| |
| this._breakpointManager.setBreakpoint(this.uiSourceCode(), lineNumber, columnNumber, condition, enabled); |
| this._breakpointWasSetForTest(lineNumber, columnNumber, condition, enabled); |
| } |
| |
| /** |
| * @param {number} lineNumber |
| * @param {number} columnNumber |
| * @param {string} condition |
| * @param {boolean} enabled |
| */ |
| _breakpointWasSetForTest(lineNumber, columnNumber, condition, enabled) { |
| } |
| |
| /** |
| * @override |
| */ |
| dispose() { |
| this._breakpointManager.removeEventListener( |
| Bindings.BreakpointManager.Events.BreakpointAdded, this._breakpointAdded, this); |
| this._breakpointManager.removeEventListener( |
| Bindings.BreakpointManager.Events.BreakpointRemoved, this._breakpointRemoved, this); |
| this.uiSourceCode().removeEventListener( |
| Workspace.UISourceCode.Events.SourceMappingChanged, this._onSourceMappingChanged, this); |
| this.uiSourceCode().removeEventListener( |
| Workspace.UISourceCode.Events.WorkingCopyChanged, this._workingCopyChanged, this); |
| this.uiSourceCode().removeEventListener( |
| Workspace.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this); |
| this.uiSourceCode().removeEventListener( |
| Workspace.UISourceCode.Events.TitleChanged, this._showBlackboxInfobarIfNeeded, this); |
| Common.moduleSetting('skipStackFramesPattern').removeChangeListener(this._showBlackboxInfobarIfNeeded, this); |
| Common.moduleSetting('skipContentScripts').removeChangeListener(this._showBlackboxInfobarIfNeeded, this); |
| super.dispose(); |
| } |
| }; |