| /* |
| * Copyright (C) 2013-2018 Apple 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: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. 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. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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. |
| */ |
| |
| WI.SourceCodeTextEditor = class SourceCodeTextEditor extends WI.TextEditor |
| { |
| constructor(sourceCode) |
| { |
| console.assert(sourceCode instanceof WI.SourceCode); |
| |
| super(); |
| |
| this.delegate = this; |
| |
| this._sourceCode = sourceCode; |
| this._breakpointMap = {}; |
| this._issuesLineNumberMap = new Map; |
| this._widgetMap = new Map; |
| this._contentPopulated = false; |
| this._invalidLineNumbers = {0: true}; |
| this._requestingScriptContent = false; |
| this._activeCallFrameSourceCodeLocation = null; |
| |
| this._threadLineNumberMap = new Map; // line -> [targets] |
| this._threadWidgetMap = new Map; // line -> widget |
| this._threadTargetMap = new Map; // target -> line |
| |
| this._typeTokenScrollHandler = null; |
| this._typeTokenAnnotator = null; |
| this._basicBlockAnnotator = null; |
| this._editingController = null; |
| |
| this._autoFormat = false; |
| this._isProbablyMinified = false; |
| |
| this._ignoreContentDidChange = 0; |
| this._ignoreLocationUpdateBreakpoint = null; |
| this._ignoreBreakpointAddedBreakpoint = null; |
| this._ignoreBreakpointRemovedBreakpoint = null; |
| this._ignoreAllBreakpointLocationUpdates = false; |
| |
| // FIXME: Currently this just jumps between resources and related source map resources. It doesn't "jump to symbol" yet. |
| this._updateTokenTrackingControllerState(); |
| |
| this.element.classList.add("source-code"); |
| |
| if (this._supportsDebugging) { |
| WI.Breakpoint.addEventListener(WI.Breakpoint.Event.DisabledStateDidChange, this._breakpointStatusDidChange, this); |
| WI.Breakpoint.addEventListener(WI.Breakpoint.Event.AutoContinueDidChange, this._breakpointStatusDidChange, this); |
| WI.Breakpoint.addEventListener(WI.Breakpoint.Event.ResolvedStateDidChange, this._breakpointStatusDidChange, this); |
| WI.Breakpoint.addEventListener(WI.Breakpoint.Event.LocationDidChange, this._updateBreakpointLocation, this); |
| |
| WI.targetManager.addEventListener(WI.TargetManager.Event.TargetAdded, this._targetAdded, this); |
| WI.targetManager.addEventListener(WI.TargetManager.Event.TargetRemoved, this._targetRemoved, this); |
| |
| WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.BreakpointsEnabledDidChange, this._breakpointsEnabledDidChange, this); |
| WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.BreakpointAdded, this._breakpointAdded, this); |
| WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.BreakpointRemoved, this._breakpointRemoved, this); |
| WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.CallFramesDidChange, this._callFramesDidChange, this); |
| WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.ActiveCallFrameDidChange, this._activeCallFrameDidChange, this); |
| |
| WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.Paused, this._debuggerDidPause, this); |
| WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.Resumed, this._debuggerDidResume, this); |
| if (WI.debuggerManager.activeCallFrame) |
| this._debuggerDidPause(); |
| |
| this._activeCallFrameDidChange(); |
| } |
| |
| WI.consoleManager.addEventListener(WI.ConsoleManager.Event.IssueAdded, this._issueWasAdded, this); |
| |
| this._sourceCode.addEventListener(WI.SourceCode.Event.FormatterDidChange, this._handleFormatterDidChange, this); |
| if (this._sourceCode instanceof WI.SourceMapResource || this._sourceCode.sourceMaps.length > 0) |
| WI.notifications.addEventListener(WI.Notification.GlobalModifierKeysDidChange, this._updateTokenTrackingControllerState, this); |
| else |
| this._sourceCode.addEventListener(WI.SourceCode.Event.SourceMapAdded, this._sourceCodeSourceMapAdded, this); |
| |
| sourceCode.requestContent().then(this._contentAvailable.bind(this)); |
| |
| new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Control, "G", this.showGoToLineDialog.bind(this), this.element); |
| |
| WI.consoleManager.addEventListener(WI.ConsoleManager.Event.Cleared, this._logCleared, this); |
| } |
| |
| // Public |
| |
| get sourceCode() |
| { |
| return this._sourceCode; |
| } |
| |
| get target() |
| { |
| if (this._sourceCode instanceof WI.SourceMapResource) { |
| if (this._sourceCode.sourceMap.originalSourceCode instanceof WI.Script) |
| return this._sourceCode.sourceMap.originalSourceCode.target; |
| } |
| |
| if (this._sourceCode instanceof WI.Script) |
| return this._sourceCode.target; |
| |
| return WI.mainTarget; |
| } |
| |
| shown() |
| { |
| super.shown(); |
| |
| if (WI.settings.showJavaScriptTypeInformation.value) { |
| if (this._typeTokenAnnotator) |
| this._typeTokenAnnotator.resume(); |
| if (!this._typeTokenScrollHandler && this._typeTokenAnnotator) |
| this._enableScrollEventsForTypeTokenAnnotator(); |
| } else { |
| if (this._typeTokenAnnotator) |
| this._setTypeTokenAnnotatorEnabledState(false); |
| } |
| |
| if (WI.settings.enableControlFlowProfiler.value) { |
| if (this._basicBlockAnnotator) |
| this._basicBlockAnnotator.resume(); |
| |
| if (!this._controlFlowScrollHandler && this._basicBlockAnnotator) |
| this._enableScrollEventsForControlFlowAnnotator(); |
| } else { |
| this._basicBlockAnnotatorEnabled = false; |
| } |
| } |
| |
| hidden() |
| { |
| super.hidden(); |
| |
| this.tokenTrackingController.removeHighlightedRange(); |
| |
| this._dismissPopover(); |
| |
| this._dismissEditingController(true); |
| |
| if (this._typeTokenAnnotator) |
| this._typeTokenAnnotator.pause(); |
| if (this._basicBlockAnnotator) |
| this._basicBlockAnnotator.pause(); |
| } |
| |
| close() |
| { |
| if (this._supportsDebugging) { |
| WI.Breakpoint.removeEventListener(null, null, this); |
| WI.debuggerManager.removeEventListener(null, null, this); |
| WI.targetManager.removeEventListener(null, null, this); |
| |
| if (this._activeCallFrameSourceCodeLocation) { |
| this._activeCallFrameSourceCodeLocation.removeEventListener(WI.SourceCodeLocation.Event.LocationChanged, this._activeCallFrameSourceCodeLocationChanged, this); |
| this._activeCallFrameSourceCodeLocation = null; |
| } |
| } |
| |
| WI.consoleManager.removeEventListener(null, null, this); |
| WI.notifications.removeEventListener(null, null, this); |
| this._sourceCode.removeEventListener(null, null, this); |
| } |
| |
| canBeFormatted() |
| { |
| // Currently we assume that source map resources are formatted how the author wants it. |
| // We could allow source map resources to be formatted, we would then need to make |
| // SourceCodeLocation watch listen for mappedResource's formatting changes, and keep |
| // a formatted location alongside the regular mapped location. |
| if (this._sourceCode instanceof WI.SourceMapResource) |
| return false; |
| |
| return super.canBeFormatted(); |
| } |
| |
| canShowTypeAnnotations() |
| { |
| // Type annotations for modified scripts are currently unsupported. |
| return !!this._getAssociatedScript() && !this.hasModified; |
| } |
| |
| canShowCoverageHints() |
| { |
| // Code coverage hints for modified scripts are currently unsupported. |
| return !!this._getAssociatedScript() && !this.hasModified; |
| } |
| |
| customPerformSearch(query) |
| { |
| function searchResultCallback(error, matches) |
| { |
| // Bail if the query changed since we started. |
| if (this.currentSearchQuery !== query) |
| return; |
| |
| if (error || !matches || !matches.length) { |
| // Report zero matches. |
| this.dispatchEventToListeners(WI.TextEditor.Event.NumberOfSearchResultsDidChange); |
| return; |
| } |
| |
| let queryRegex = WI.SearchUtilities.regExpForString(query, WI.SearchUtilities.defaultSettings); |
| var searchResults = []; |
| |
| for (var i = 0; i < matches.length; ++i) { |
| var matchLineNumber = matches[i].lineNumber; |
| var line = this.line(matchLineNumber); |
| if (!line) |
| return; |
| |
| // Reset the last index to reuse the regex on a new line. |
| queryRegex.lastIndex = 0; |
| |
| // Search the line and mark the ranges. |
| var lineMatch = null; |
| while (queryRegex.lastIndex + query.length <= line.length && (lineMatch = queryRegex.exec(line))) { |
| var resultTextRange = new WI.TextRange(matchLineNumber, lineMatch.index, matchLineNumber, queryRegex.lastIndex); |
| searchResults.push(resultTextRange); |
| } |
| } |
| |
| this.addSearchResults(searchResults); |
| |
| this.dispatchEventToListeners(WI.TextEditor.Event.NumberOfSearchResultsDidChange); |
| } |
| |
| if (this.hasEdits()) |
| return false; |
| |
| if (this._sourceCode instanceof WI.SourceMapResource) |
| return false; |
| if (this._sourceCode instanceof WI.LocalResource) |
| return false; |
| |
| let caseSensitive = WI.SearchUtilities.defaultSettings.caseSensitive.value; |
| let isRegex = WI.SearchUtilities.defaultSettings.regularExpression.value; |
| |
| if (this._sourceCode instanceof WI.Resource) |
| PageAgent.searchInResource(this._sourceCode.parentFrame.id, this._sourceCode.url, query, caseSensitive, isRegex, searchResultCallback.bind(this)); |
| else if (this._sourceCode instanceof WI.Script) |
| this._sourceCode.target.DebuggerAgent.searchInContent(this._sourceCode.id, query, caseSensitive, isRegex, searchResultCallback.bind(this)); |
| return true; |
| } |
| |
| showGoToLineDialog() |
| { |
| if (!this._goToLineDialog) |
| this._goToLineDialog = new WI.GoToLineDialog(this); |
| |
| this._goToLineDialog.present(this.element); |
| } |
| |
| isDialogRepresentedObjectValid(goToLineDialog, lineNumber) |
| { |
| return !isNaN(lineNumber) && lineNumber > 0 && lineNumber <= this.lineCount; |
| } |
| |
| dialogWasDismissedWithRepresentedObject(goToLineDialog, lineNumber) |
| { |
| let position = new WI.SourceCodePosition(lineNumber - 1, 0); |
| let range = new WI.TextRange(lineNumber - 1, 0, lineNumber, 0); |
| this.revealPosition(position, range, false, true); |
| } |
| |
| contentDidChange(replacedRanges, newRanges) |
| { |
| super.contentDidChange(replacedRanges, newRanges); |
| |
| if (this._ignoreContentDidChange > 0) |
| return; |
| |
| for (var range of newRanges) |
| this._updateEditableMarkers(range); |
| |
| if (this._basicBlockAnnotator) { |
| this._basicBlockAnnotatorEnabled = false; |
| this._basicBlockAnnotator = null; |
| } |
| |
| if (this._typeTokenAnnotator) { |
| this._setTypeTokenAnnotatorEnabledState(false); |
| this._typeTokenAnnotator = null; |
| } |
| } |
| |
| toggleTypeAnnotations() |
| { |
| if (!this._typeTokenAnnotator) |
| return Promise.reject(new Error("TypeTokenAnnotator is not initialized.")); |
| |
| var newActivatedState = !this._typeTokenAnnotator.isActive(); |
| if (newActivatedState && this._isProbablyMinified && !this.formatted) { |
| return this.updateFormattedState(true).then(() => { |
| this._setTypeTokenAnnotatorEnabledState(newActivatedState); |
| }); |
| } |
| |
| this._setTypeTokenAnnotatorEnabledState(newActivatedState); |
| return Promise.resolve(); |
| } |
| |
| toggleUnexecutedCodeHighlights() |
| { |
| if (!this._basicBlockAnnotator) |
| return Promise.reject(new Error("BasicBlockAnnotator is not initialized.")); |
| |
| let newActivatedState = !this._basicBlockAnnotator.isActive(); |
| if (newActivatedState && this._isProbablyMinified && !this.formatted) { |
| return this.updateFormattedState(true).then(() => { |
| this._basicBlockAnnotatorEnabled = newActivatedState; |
| }); |
| } |
| |
| this._basicBlockAnnotatorEnabled = newActivatedState; |
| return Promise.resolve(); |
| } |
| |
| showPopoverForTypes(typeDescription, bounds, title) |
| { |
| var content = document.createElement("div"); |
| content.className = "object expandable"; |
| |
| var titleElement = document.createElement("div"); |
| titleElement.className = "title"; |
| titleElement.textContent = title; |
| content.appendChild(titleElement); |
| |
| var bodyElement = content.appendChild(document.createElement("div")); |
| bodyElement.className = "body"; |
| |
| var typeTreeView = new WI.TypeTreeView(typeDescription); |
| bodyElement.appendChild(typeTreeView.element); |
| |
| this._showPopover(content, bounds); |
| } |
| |
| // Protected |
| |
| prettyPrint(pretty) |
| { |
| // The annotators must be cleared before pretty printing takes place and resumed |
| // after so that they clear their annotations in a known state and insert new annotations |
| // in the new state. |
| |
| var shouldResumeBasicBlockAnnotator = this._basicBlockAnnotator && this._basicBlockAnnotator.isActive(); |
| if (shouldResumeBasicBlockAnnotator) |
| this._basicBlockAnnotatorEnabled = false; |
| |
| let shouldResumeTypeTokenAnnotator = this._typeTokenAnnotator && this._typeTokenAnnotator.isActive(); |
| if (shouldResumeTypeTokenAnnotator) |
| this._setTypeTokenAnnotatorEnabledState(false); |
| |
| return super.prettyPrint(pretty).then(() => { |
| if (pretty || !this._isProbablyMinified) { |
| if (shouldResumeBasicBlockAnnotator) |
| this._basicBlockAnnotatorEnabled = true; |
| |
| if (shouldResumeTypeTokenAnnotator) |
| this._setTypeTokenAnnotatorEnabledState(true); |
| } else { |
| console.assert(!pretty && this._isProbablyMinified); |
| if (this._basicBlockAnnotator) |
| this._basicBlockAnnotatorEnabled = false; |
| |
| this._setTypeTokenAnnotatorEnabledState(false); |
| } |
| }); |
| } |
| |
| // Private |
| |
| _unformattedLineInfoForEditorLineInfo(lineInfo) |
| { |
| if (this.formatterSourceMap) |
| return this.formatterSourceMap.formattedToOriginal(lineInfo.lineNumber, lineInfo.columnNumber); |
| return lineInfo; |
| } |
| |
| _sourceCodeLocationForEditorPosition(position) |
| { |
| var lineInfo = {lineNumber: position.line, columnNumber: position.ch}; |
| var unformattedLineInfo = this._unformattedLineInfoForEditorLineInfo(lineInfo); |
| return this.sourceCode.createSourceCodeLocation(unformattedLineInfo.lineNumber, unformattedLineInfo.columnNumber); |
| } |
| |
| _editorLineInfoForSourceCodeLocation(sourceCodeLocation) |
| { |
| if (this._sourceCode instanceof WI.SourceMapResource) |
| return {lineNumber: sourceCodeLocation.displayLineNumber, columnNumber: sourceCodeLocation.displayColumnNumber}; |
| return {lineNumber: sourceCodeLocation.formattedLineNumber, columnNumber: sourceCodeLocation.formattedColumnNumber}; |
| } |
| |
| _breakpointForEditorLineInfo(lineInfo) |
| { |
| if (!this._breakpointMap[lineInfo.lineNumber]) |
| return null; |
| return this._breakpointMap[lineInfo.lineNumber][lineInfo.columnNumber]; |
| } |
| |
| _addBreakpointWithEditorLineInfo(breakpoint, lineInfo) |
| { |
| if (!this._breakpointMap[lineInfo.lineNumber]) |
| this._breakpointMap[lineInfo.lineNumber] = {}; |
| |
| this._breakpointMap[lineInfo.lineNumber][lineInfo.columnNumber] = breakpoint; |
| } |
| |
| _removeBreakpointWithEditorLineInfo(breakpoint, lineInfo) |
| { |
| console.assert(breakpoint === this._breakpointMap[lineInfo.lineNumber][lineInfo.columnNumber]); |
| |
| delete this._breakpointMap[lineInfo.lineNumber][lineInfo.columnNumber]; |
| |
| if (isEmptyObject(this._breakpointMap[lineInfo.lineNumber])) |
| delete this._breakpointMap[lineInfo.lineNumber]; |
| } |
| |
| _populateWithContent(content) |
| { |
| content = content || ""; |
| |
| this._prepareEditorForInitialContent(content); |
| |
| // If we can auto format, format the TextEditor before showing it. |
| if (this._autoFormat) { |
| console.assert(!this.formatted); |
| this._autoFormat = false; |
| this.deferReveal = true; |
| this.string = content; |
| this.deferReveal = false; |
| this.updateFormattedState(true).then(() => { |
| this._proceedPopulateWithContent(this.string); |
| }); |
| return; |
| } |
| |
| this._proceedPopulateWithContent(content); |
| } |
| |
| _proceedPopulateWithContent(content) |
| { |
| this.dispatchEventToListeners(WI.SourceCodeTextEditor.Event.ContentWillPopulate); |
| |
| this.string = content; |
| |
| this._createBasicBlockAnnotator(); |
| if (WI.settings.enableControlFlowProfiler.value && this._basicBlockAnnotator) |
| this._basicBlockAnnotatorEnabled = true; |
| |
| this._createTypeTokenAnnotator(); |
| if (WI.settings.showJavaScriptTypeInformation.value) |
| this._setTypeTokenAnnotatorEnabledState(true); |
| |
| this._contentDidPopulate(); |
| } |
| |
| _contentDidPopulate() |
| { |
| this._contentPopulated = true; |
| |
| this.dispatchEventToListeners(WI.SourceCodeTextEditor.Event.ContentDidPopulate); |
| |
| // We add the issues each time content is populated. This is needed because lines might not exist |
| // if we tried added them before when the full content wasn't available. (When populating with |
| // partial script content this can be called multiple times.) |
| |
| this._reinsertAllIssues(); |
| this._reinsertAllThreadIndicators(); |
| |
| this._updateEditableMarkers(); |
| } |
| |
| _prepareEditorForInitialContent(content) |
| { |
| // Only do this work before the first populate. |
| if (this._contentPopulated) |
| return; |
| |
| if (this._supportsDebugging) { |
| this._breakpointMap = {}; |
| |
| for (let breakpoint of WI.debuggerManager.breakpointsForSourceCode(this._sourceCode)) { |
| console.assert(this._matchesBreakpoint(breakpoint)); |
| var lineInfo = this._editorLineInfoForSourceCodeLocation(breakpoint.sourceCodeLocation); |
| this._addBreakpointWithEditorLineInfo(breakpoint, lineInfo); |
| this.setBreakpointInfoForLineAndColumn(lineInfo.lineNumber, lineInfo.columnNumber, this._breakpointInfoForBreakpoint(breakpoint)); |
| } |
| } |
| |
| if (this._sourceCode instanceof WI.Resource) |
| this.mimeType = this._sourceCode.syntheticMIMEType; |
| else if (this._sourceCode instanceof WI.Script) |
| this.mimeType = "text/javascript"; |
| else if (this._sourceCode instanceof WI.CSSStyleSheet) |
| this.mimeType = "text/css"; |
| |
| // Decide to automatically format the content if it looks minified and it can be formatted. |
| console.assert(!this.formatted); |
| if (this.canBeFormatted() && isTextLikelyMinified(content)) { |
| this._autoFormat = true; |
| this._isProbablyMinified = true; |
| } |
| } |
| |
| _contentAvailable(parameters) |
| { |
| // Return if resource is not available. |
| if (parameters.error) |
| return; |
| |
| if (parameters.message) { |
| this._showMessage(parameters.message); |
| return; |
| } |
| |
| var sourceCode = parameters.sourceCode; |
| var content = sourceCode.content; |
| var base64Encoded = parameters.base64Encoded; |
| |
| console.assert(sourceCode === this._sourceCode); |
| console.assert(!base64Encoded); |
| |
| // Abort if the full content populated while waiting for this async callback. |
| if (this._fullContentPopulated) |
| return; |
| |
| this._fullContentPopulated = true; |
| this._invalidLineNumbers = {}; |
| |
| // If we had partial content (such as inline script content) before we had full content, we |
| // will want to re-restore the revealed position now that we are populating with full content. |
| this.repeatReveal = !!this.string; |
| |
| this._populateWithContent(content); |
| |
| this.repeatReveal = false; |
| } |
| |
| _showMessage(message) |
| { |
| this.element.removeChildren(); |
| this.element.appendChild(WI.createMessageTextView(message)); |
| } |
| |
| _breakpointStatusDidChange(event) |
| { |
| this._updateBreakpointStatus(event.target); |
| } |
| |
| _breakpointsEnabledDidChange() |
| { |
| console.assert(this._supportsDebugging); |
| |
| for (let breakpoint of WI.debuggerManager.breakpointsForSourceCode(this._sourceCode)) |
| this._updateBreakpointStatus(breakpoint); |
| } |
| |
| _updateBreakpointStatus(breakpoint) |
| { |
| console.assert(this._supportsDebugging); |
| |
| if (!this._contentPopulated) |
| return; |
| |
| if (!this._matchesBreakpoint(breakpoint)) |
| return; |
| |
| var lineInfo = this._editorLineInfoForSourceCodeLocation(breakpoint.sourceCodeLocation); |
| this.setBreakpointInfoForLineAndColumn(lineInfo.lineNumber, lineInfo.columnNumber, this._breakpointInfoForBreakpoint(breakpoint)); |
| } |
| |
| _updateBreakpointLocation(event) |
| { |
| console.assert(this._supportsDebugging); |
| |
| if (!this._contentPopulated) |
| return; |
| |
| var breakpoint = event.target; |
| if (!this._matchesBreakpoint(breakpoint)) |
| return; |
| |
| if (this._ignoreAllBreakpointLocationUpdates) |
| return; |
| |
| if (breakpoint === this._ignoreLocationUpdateBreakpoint) |
| return; |
| |
| var sourceCodeLocation = breakpoint.sourceCodeLocation; |
| |
| if (this._sourceCode instanceof WI.SourceMapResource) { |
| // Update our breakpoint location if the display location changed. |
| if (sourceCodeLocation.displaySourceCode !== this._sourceCode) |
| return; |
| var oldLineInfo = {lineNumber: event.data.oldDisplayLineNumber, columnNumber: event.data.oldDisplayColumnNumber}; |
| var newLineInfo = {lineNumber: sourceCodeLocation.displayLineNumber, columnNumber: sourceCodeLocation.displayColumnNumber}; |
| } else { |
| // Update our breakpoint location if the original location changed. |
| if (sourceCodeLocation.sourceCode !== this._sourceCode) |
| return; |
| var oldLineInfo = {lineNumber: event.data.oldFormattedLineNumber, columnNumber: event.data.oldFormattedColumnNumber}; |
| var newLineInfo = {lineNumber: sourceCodeLocation.formattedLineNumber, columnNumber: sourceCodeLocation.formattedColumnNumber}; |
| } |
| |
| var existingBreakpoint = this._breakpointForEditorLineInfo(oldLineInfo); |
| if (!existingBreakpoint) |
| return; |
| |
| console.assert(breakpoint === existingBreakpoint); |
| |
| this.setBreakpointInfoForLineAndColumn(oldLineInfo.lineNumber, oldLineInfo.columnNumber, null); |
| this.setBreakpointInfoForLineAndColumn(newLineInfo.lineNumber, newLineInfo.columnNumber, this._breakpointInfoForBreakpoint(breakpoint)); |
| |
| this._removeBreakpointWithEditorLineInfo(breakpoint, oldLineInfo); |
| this._addBreakpointWithEditorLineInfo(breakpoint, newLineInfo); |
| } |
| |
| _breakpointAdded(event) |
| { |
| console.assert(this._supportsDebugging); |
| |
| if (!this._contentPopulated) |
| return; |
| |
| var breakpoint = event.data.breakpoint; |
| if (!this._matchesBreakpoint(breakpoint)) |
| return; |
| |
| if (breakpoint === this._ignoreBreakpointAddedBreakpoint) |
| return; |
| |
| var lineInfo = this._editorLineInfoForSourceCodeLocation(breakpoint.sourceCodeLocation); |
| this._addBreakpointWithEditorLineInfo(breakpoint, lineInfo); |
| this.setBreakpointInfoForLineAndColumn(lineInfo.lineNumber, lineInfo.columnNumber, this._breakpointInfoForBreakpoint(breakpoint)); |
| } |
| |
| _breakpointRemoved(event) |
| { |
| console.assert(this._supportsDebugging); |
| |
| if (!this._contentPopulated) |
| return; |
| |
| var breakpoint = event.data.breakpoint; |
| if (!this._matchesBreakpoint(breakpoint)) |
| return; |
| |
| if (breakpoint === this._ignoreBreakpointRemovedBreakpoint) |
| return; |
| |
| var lineInfo = this._editorLineInfoForSourceCodeLocation(breakpoint.sourceCodeLocation); |
| this._removeBreakpointWithEditorLineInfo(breakpoint, lineInfo); |
| this.setBreakpointInfoForLineAndColumn(lineInfo.lineNumber, lineInfo.columnNumber, null); |
| } |
| |
| _targetAdded(event) |
| { |
| if (WI.targets.length === 2) |
| this._reinsertAllThreadIndicators(); |
| } |
| |
| _targetRemoved(event) |
| { |
| if (WI.targets.length === 1) { |
| // Back to one thread, remove thread indicators. |
| this._reinsertAllThreadIndicators(); |
| return; |
| } |
| |
| let target = event.data.target; |
| this._removeThreadIndicatorForTarget(target); |
| } |
| |
| _callFramesDidChange(event) |
| { |
| if (WI.targets.length === 1) |
| return; |
| |
| let target = event.data.target; |
| this._removeThreadIndicatorForTarget(target); |
| this._addThreadIndicatorForTarget(target); |
| } |
| |
| _addThreadIndicatorForTarget(target) |
| { |
| let targetData = WI.debuggerManager.dataForTarget(target); |
| let topCallFrame = targetData.callFrames[0]; |
| if (!topCallFrame) |
| return; |
| |
| let sourceCodeLocation = topCallFrame.sourceCodeLocation; |
| console.assert(sourceCodeLocation, "Expected source code location to place thread indicator."); |
| if (!sourceCodeLocation) |
| return; |
| |
| if (!this._looselyMatchesSourceCodeLocation(sourceCodeLocation)) |
| return; |
| |
| let lineNumberWithIndicator = sourceCodeLocation.formattedLineNumber; |
| this._threadTargetMap.set(target, lineNumberWithIndicator); |
| |
| let threads = this._threadLineNumberMap.get(lineNumberWithIndicator); |
| if (!threads) { |
| threads = []; |
| this._threadLineNumberMap.set(lineNumberWithIndicator, threads); |
| } |
| threads.push(target); |
| |
| let widget = this._threadIndicatorWidgetForLine(target, lineNumberWithIndicator); |
| this._updateThreadIndicatorWidget(widget, threads); |
| |
| this.addStyleClassToLine(lineNumberWithIndicator, "thread-indicator"); |
| } |
| |
| _removeThreadIndicatorForTarget(target) |
| { |
| let lineNumberWithIndicator = this._threadTargetMap.take(target); |
| if (lineNumberWithIndicator === undefined) |
| return; |
| |
| let threads = this._threadLineNumberMap.get(lineNumberWithIndicator); |
| threads.remove(target); |
| if (threads.length) { |
| let widget = this._threadWidgetMap.get(lineNumberWithIndicator); |
| this._updateThreadIndicatorWidget(widget, threads); |
| return; |
| } |
| |
| this._threadLineNumberMap.delete(lineNumberWithIndicator); |
| |
| let widget = this._threadWidgetMap.take(lineNumberWithIndicator); |
| if (widget) |
| widget.clear(); |
| |
| this.removeStyleClassFromLine(lineNumberWithIndicator, "thread-indicator"); |
| } |
| |
| _threadIndicatorWidgetForLine(target, lineNumber) |
| { |
| let widget = this._threadWidgetMap.get(lineNumber); |
| if (widget) |
| return widget; |
| |
| widget = this.createWidgetForLine(lineNumber); |
| if (!widget) |
| return null; |
| |
| let widgetElement = widget.widgetElement; |
| widgetElement.classList.add("line-indicator-widget", "thread-widget", "inline"); |
| widgetElement.addEventListener("click", this._handleThreadIndicatorWidgetClick.bind(this, widget, lineNumber)); |
| |
| this._threadWidgetMap.set(lineNumber, widget); |
| |
| return widget; |
| } |
| |
| _updateThreadIndicatorWidget(widget, threads) |
| { |
| if (!widget) |
| return; |
| |
| console.assert(WI.targets.length > 1); |
| |
| let widgetElement = widget.widgetElement; |
| widgetElement.removeChildren(); |
| |
| widget[WI.SourceCodeTextEditor.WidgetContainsMultipleThreadsSymbol] = threads.length > 1; |
| |
| if (widgetElement.classList.contains("inline") || threads.length === 1) { |
| let textElement = widgetElement.appendChild(document.createElement("span")); |
| textElement.className = "text"; |
| textElement.textContent = threads.length === 1 ? threads[0].displayName : WI.UIString("%d Threads").format(threads.length); |
| } else { |
| for (let target of threads) { |
| let textElement = widgetElement.appendChild(document.createElement("span")); |
| textElement.className = "text"; |
| textElement.textContent = target.displayName; |
| |
| widgetElement.appendChild(document.createElement("br")); |
| } |
| } |
| |
| widget.update(); |
| } |
| |
| _handleThreadIndicatorWidgetClick(widget, lineNumber, event) |
| { |
| if (!this._isWidgetToggleable(widget)) |
| return; |
| |
| widget.widgetElement.classList.toggle("inline"); |
| |
| let threads = this._threadLineNumberMap.get(lineNumber); |
| this._updateThreadIndicatorWidget(widget, threads); |
| } |
| |
| _activeCallFrameDidChange() |
| { |
| console.assert(this._supportsDebugging); |
| |
| if (this._activeCallFrameSourceCodeLocation) { |
| this._activeCallFrameSourceCodeLocation.removeEventListener(WI.SourceCodeLocation.Event.LocationChanged, this._activeCallFrameSourceCodeLocationChanged, this); |
| this._activeCallFrameSourceCodeLocation = null; |
| } |
| |
| let activeCallFrame = WI.debuggerManager.activeCallFrame; |
| if (!activeCallFrame || !this._matchesSourceCodeLocation(activeCallFrame.sourceCodeLocation)) { |
| this.setExecutionLineAndColumn(NaN, NaN); |
| return; |
| } |
| |
| this._dismissPopover(); |
| |
| this._activeCallFrameSourceCodeLocation = activeCallFrame.sourceCodeLocation; |
| this._activeCallFrameSourceCodeLocation.addEventListener(WI.SourceCodeLocation.Event.LocationChanged, this._activeCallFrameSourceCodeLocationChanged, this); |
| |
| // Don't return early if the line number didn't change. The execution state still |
| // could have changed (e.g. continuing in a loop with a breakpoint inside). |
| |
| let lineInfo = this._editorLineInfoForSourceCodeLocation(activeCallFrame.sourceCodeLocation); |
| this.setExecutionLineAndColumn(lineInfo.lineNumber, lineInfo.columnNumber); |
| |
| // If we have full content or this source code isn't a Resource we can return early. |
| // Script source code populates from the request started in the constructor. |
| if (this._fullContentPopulated || !(this._sourceCode instanceof WI.Resource) || this._requestingScriptContent) |
| return; |
| |
| // Since we are paused in the debugger we need to show some content, and since the Resource |
| // content hasn't populated yet we need to populate with content from the Scripts by URL. |
| // Document resources will attempt to populate the scripts as inline (in <script> tags.) |
| // Other resources are assumed to be full scripts (JavaScript resources). |
| if (this._sourceCode.type === WI.Resource.Type.Document) |
| this._populateWithInlineScriptContent(); |
| else |
| this._populateWithScriptContent(); |
| } |
| |
| _activeCallFrameSourceCodeLocationChanged(event) |
| { |
| console.assert(!isNaN(this.executionLineNumber)); |
| if (isNaN(this.executionLineNumber)) |
| return; |
| |
| console.assert(WI.debuggerManager.activeCallFrame); |
| console.assert(this._activeCallFrameSourceCodeLocation === WI.debuggerManager.activeCallFrame.sourceCodeLocation); |
| |
| var lineInfo = this._editorLineInfoForSourceCodeLocation(this._activeCallFrameSourceCodeLocation); |
| this.setExecutionLineAndColumn(lineInfo.lineNumber, lineInfo.columnNumber); |
| } |
| |
| _populateWithInlineScriptContent() |
| { |
| console.assert(this._sourceCode instanceof WI.Resource); |
| console.assert(!this._fullContentPopulated); |
| console.assert(!this._requestingScriptContent); |
| |
| var scripts = this._sourceCode.scripts; |
| console.assert(scripts.length); |
| if (!scripts.length) |
| return; |
| |
| var pendingRequestCount = scripts.length; |
| |
| // If the number of scripts hasn't change since the last populate, then there is nothing to do. |
| if (this._inlineScriptContentPopulated === pendingRequestCount) |
| return; |
| |
| this._inlineScriptContentPopulated = pendingRequestCount; |
| |
| function scriptContentAvailable(parameters) |
| { |
| // Return early if we are still waiting for content from other scripts. |
| if (--pendingRequestCount) |
| return; |
| |
| this._requestingScriptContent = false; |
| |
| // Abort if the full content populated while waiting for these async callbacks. |
| if (this._fullContentPopulated) |
| return; |
| |
| var scriptOpenTag = "<script>"; |
| var scriptCloseTag = "</script>"; |
| |
| var content = ""; |
| var lineNumber = 0; |
| var columnNumber = 0; |
| |
| this._invalidLineNumbers = {}; |
| |
| for (var i = 0; i < scripts.length; ++i) { |
| // Fill the line gap with newline characters. |
| for (var newLinesCount = scripts[i].range.startLine - lineNumber; newLinesCount > 0; --newLinesCount) { |
| if (!columnNumber) |
| this._invalidLineNumbers[scripts[i].range.startLine - newLinesCount] = true; |
| columnNumber = 0; |
| content += "\n"; |
| } |
| |
| // Fill the column gap with space characters. |
| for (var spacesCount = scripts[i].range.startColumn - columnNumber - scriptOpenTag.length; spacesCount > 0; --spacesCount) |
| content += " "; |
| |
| // Add script tags and content. |
| content += scriptOpenTag; |
| content += scripts[i].content; |
| content += scriptCloseTag; |
| |
| lineNumber = scripts[i].range.endLine; |
| columnNumber = scripts[i].range.endColumn + scriptCloseTag.length; |
| } |
| |
| this._populateWithContent(content); |
| } |
| |
| this._requestingScriptContent = true; |
| |
| var boundScriptContentAvailable = scriptContentAvailable.bind(this); |
| for (var i = 0; i < scripts.length; ++i) |
| scripts[i].requestContent().then(boundScriptContentAvailable); |
| } |
| |
| _populateWithScriptContent() |
| { |
| console.assert(this._sourceCode instanceof WI.Resource); |
| console.assert(!this._fullContentPopulated); |
| console.assert(!this._requestingScriptContent); |
| |
| // We can assume this resource only has one script that starts at line/column 0. |
| var scripts = this._sourceCode.scripts; |
| console.assert(scripts.length === 1); |
| if (!scripts.length) |
| return; |
| |
| console.assert(scripts[0].range.startLine === 0); |
| console.assert(scripts[0].range.startColumn === 0); |
| |
| function scriptContentAvailable(parameters) |
| { |
| var content = parameters.content; |
| this._requestingScriptContent = false; |
| |
| // Abort if the full content populated while waiting for this async callback. |
| if (this._fullContentPopulated) |
| return; |
| |
| // This is the full content. |
| this._fullContentPopulated = true; |
| |
| this._populateWithContent(content); |
| } |
| |
| this._requestingScriptContent = true; |
| |
| scripts[0].requestContent().then(scriptContentAvailable.bind(this)); |
| } |
| |
| _looselyMatchesSourceCodeLocation(sourceCodeLocation) |
| { |
| if (this._sourceCode instanceof WI.SourceMapResource) |
| return sourceCodeLocation.displaySourceCode === this._sourceCode; |
| if (this._sourceCode instanceof WI.Resource || this._sourceCode instanceof WI.Script || this._sourceCode instanceof WI.CSSStyleSheet) |
| return sourceCodeLocation.sourceCode.url === this._sourceCode.url; |
| return false; |
| } |
| |
| _matchesSourceCodeLocation(sourceCodeLocation) |
| { |
| if (this._sourceCode instanceof WI.SourceMapResource) |
| return sourceCodeLocation.displaySourceCode === this._sourceCode; |
| if (this._sourceCode instanceof WI.Resource || this._sourceCode instanceof WI.CSSStyleSheet) |
| return sourceCodeLocation.sourceCode.url === this._sourceCode.url; |
| if (this._sourceCode instanceof WI.Script) |
| return sourceCodeLocation.sourceCode === this._sourceCode; |
| return false; |
| } |
| |
| _matchesBreakpoint(breakpoint) |
| { |
| console.assert(this._supportsDebugging); |
| if (this._sourceCode instanceof WI.SourceMapResource) |
| return breakpoint.sourceCodeLocation.displaySourceCode === this._sourceCode; |
| if (this._sourceCode instanceof WI.Resource) |
| return breakpoint.contentIdentifier === this._sourceCode.contentIdentifier; |
| if (this._sourceCode instanceof WI.Script) |
| return breakpoint.contentIdentifier === this._sourceCode.contentIdentifier || breakpoint.scriptIdentifier === this._sourceCode.id; |
| return false; |
| } |
| |
| _issueWasAdded(event) |
| { |
| var issue = event.data.issue; |
| if (!WI.ConsoleManager.issueMatchSourceCode(issue, this._sourceCode)) |
| return; |
| |
| this._addIssue(issue); |
| } |
| |
| _addIssue(issue) |
| { |
| var sourceCodeLocation = issue.sourceCodeLocation; |
| console.assert(sourceCodeLocation, "Expected source code location to place issue."); |
| if (!sourceCodeLocation) |
| return; |
| |
| var lineNumber = sourceCodeLocation.formattedLineNumber; |
| |
| var lineNumberIssues = this._issuesLineNumberMap.get(lineNumber); |
| if (!lineNumberIssues) { |
| lineNumberIssues = []; |
| this._issuesLineNumberMap.set(lineNumber, lineNumberIssues); |
| } |
| |
| // Avoid displaying duplicate issues on the same line. |
| for (var existingIssue of lineNumberIssues) { |
| if (existingIssue.sourceCodeLocation.columnNumber === sourceCodeLocation.columnNumber && existingIssue.text === issue.text) |
| return; |
| } |
| |
| lineNumberIssues.push(issue); |
| |
| if (issue.level === WI.IssueMessage.Level.Error) |
| this.addStyleClassToLine(lineNumber, WI.SourceCodeTextEditor.LineErrorStyleClassName); |
| else if (issue.level === WI.IssueMessage.Level.Warning) |
| this.addStyleClassToLine(lineNumber, WI.SourceCodeTextEditor.LineWarningStyleClassName); |
| else |
| console.error("Unknown issue level"); |
| |
| var widget = this._issueWidgetForLine(lineNumber); |
| if (widget) { |
| if (issue.level === WI.IssueMessage.Level.Error) |
| widget.widgetElement.classList.add(WI.SourceCodeTextEditor.LineErrorStyleClassName); |
| else if (issue.level === WI.IssueMessage.Level.Warning) |
| widget.widgetElement.classList.add(WI.SourceCodeTextEditor.LineWarningStyleClassName); |
| |
| this._updateIssueWidgetForIssues(widget, lineNumberIssues); |
| } |
| } |
| |
| _issueWidgetForLine(lineNumber) |
| { |
| var widget = this._widgetMap.get(lineNumber); |
| if (widget) |
| return widget; |
| |
| widget = this.createWidgetForLine(lineNumber); |
| if (!widget) |
| return null; |
| |
| var widgetElement = widget.widgetElement; |
| widgetElement.classList.add("line-indicator-widget", "issue-widget", "inline"); |
| widgetElement.addEventListener("click", this._handleWidgetClick.bind(this, widget, lineNumber)); |
| |
| this._widgetMap.set(lineNumber, widget); |
| |
| return widget; |
| } |
| |
| _iconClassNameForIssueLevel(level) |
| { |
| if (level === WI.IssueMessage.Level.Warning) |
| return "icon-warning"; |
| |
| console.assert(level === WI.IssueMessage.Level.Error); |
| return "icon-error"; |
| } |
| |
| _updateIssueWidgetForIssues(widget, issues) |
| { |
| var widgetElement = widget.widgetElement; |
| widgetElement.removeChildren(); |
| |
| if (widgetElement.classList.contains("inline") || issues.length === 1) { |
| var iconElement = widgetElement.appendChild(document.createElement("span")); |
| iconElement.className = "icon"; |
| |
| var textElement = widgetElement.appendChild(document.createElement("span")); |
| textElement.className = "text"; |
| |
| if (issues.length === 1) { |
| iconElement.classList.add(this._iconClassNameForIssueLevel(issues[0].level)); |
| textElement.textContent = issues[0].text; |
| } else { |
| var errorsCount = 0; |
| var warningsCount = 0; |
| for (var issue of issues) { |
| if (issue.level === WI.IssueMessage.Level.Error) |
| ++errorsCount; |
| else if (issue.level === WI.IssueMessage.Level.Warning) |
| ++warningsCount; |
| } |
| |
| if (warningsCount && errorsCount) { |
| iconElement.classList.add(this._iconClassNameForIssueLevel(issue.level)); |
| textElement.textContent = WI.UIString("%d Errors, %d Warnings").format(errorsCount, warningsCount); |
| } else if (errorsCount) { |
| iconElement.classList.add(this._iconClassNameForIssueLevel(issue.level)); |
| textElement.textContent = WI.UIString("%d Errors").format(errorsCount); |
| } else if (warningsCount) { |
| iconElement.classList.add(this._iconClassNameForIssueLevel(issue.level)); |
| textElement.textContent = WI.UIString("%d Warnings").format(warningsCount); |
| } |
| |
| widget[WI.SourceCodeTextEditor.WidgetContainsMultipleIssuesSymbol] = true; |
| } |
| } else { |
| for (var issue of issues) { |
| var iconElement = widgetElement.appendChild(document.createElement("span")); |
| iconElement.className = "icon"; |
| iconElement.classList.add(this._iconClassNameForIssueLevel(issue.level)); |
| |
| var textElement = widgetElement.appendChild(document.createElement("span")); |
| textElement.className = "text"; |
| textElement.textContent = issue.text; |
| |
| widgetElement.appendChild(document.createElement("br")); |
| } |
| } |
| |
| widget.update(); |
| } |
| |
| _isWidgetToggleable(widget) |
| { |
| if (widget[WI.SourceCodeTextEditor.WidgetContainsMultipleIssuesSymbol]) |
| return true; |
| |
| if (widget[WI.SourceCodeTextEditor.WidgetContainsMultipleThreadsSymbol]) |
| return true; |
| |
| if (!widget.widgetElement.classList.contains("inline")) |
| return true; |
| |
| var textElement = widget.widgetElement.lastChild; |
| if (textElement.offsetWidth !== textElement.scrollWidth) |
| return true; |
| |
| return false; |
| } |
| |
| _handleWidgetClick(widget, lineNumber, event) |
| { |
| if (!this._isWidgetToggleable(widget)) |
| return; |
| |
| widget.widgetElement.classList.toggle("inline"); |
| |
| var lineNumberIssues = this._issuesLineNumberMap.get(lineNumber); |
| this._updateIssueWidgetForIssues(widget, lineNumberIssues); |
| } |
| |
| _breakpointInfoForBreakpoint(breakpoint) |
| { |
| return {resolved: breakpoint.resolved, disabled: breakpoint.disabled, autoContinue: breakpoint.autoContinue}; |
| } |
| |
| get _supportsDebugging() |
| { |
| if (this._sourceCode instanceof WI.Resource) { |
| if (this._sourceCode.isLocalResourceOverride) |
| return false; |
| return this._sourceCode.type === WI.Resource.Type.Document || this._sourceCode.type === WI.Resource.Type.Script; |
| } |
| if (this._sourceCode instanceof WI.Script) |
| return !(this._sourceCode instanceof WI.LocalScript); |
| return false; |
| } |
| |
| // TextEditor Delegate |
| |
| textEditorBaseURL(textEditor) |
| { |
| return this._sourceCode.url; |
| } |
| |
| textEditorScriptSourceType(textEditor) |
| { |
| let script = this._getAssociatedScript(); |
| return script ? script.sourceType : WI.Script.SourceType.Program; |
| } |
| |
| textEditorShouldHideLineNumber(textEditor, lineNumber) |
| { |
| return lineNumber in this._invalidLineNumbers; |
| } |
| |
| textEditorGutterContextMenu(textEditor, lineNumber, columnNumber, editorBreakpoints, event) |
| { |
| if (!this._supportsDebugging) |
| return; |
| |
| event.preventDefault(); |
| |
| let addBreakpoint = () => { |
| let data = this.textEditorBreakpointAdded(this, lineNumber, columnNumber); |
| this.setBreakpointInfoForLineAndColumn(data.lineNumber, data.columnNumber, data.breakpointInfo); |
| }; |
| |
| let contextMenu = WI.ContextMenu.createFromEvent(event); |
| |
| // Paused. Add Continue to Here option only if we have a script identifier for the location. |
| if (WI.debuggerManager.paused) { |
| let editorLineInfo = {lineNumber, columnNumber}; |
| let unformattedLineInfo = this._unformattedLineInfoForEditorLineInfo(editorLineInfo); |
| let sourceCodeLocation = this._sourceCode.createSourceCodeLocation(unformattedLineInfo.lineNumber, unformattedLineInfo.columnNumber); |
| |
| let script; |
| if (sourceCodeLocation.sourceCode instanceof WI.Script) |
| script = sourceCodeLocation.sourceCode; |
| else if (sourceCodeLocation.sourceCode instanceof WI.Resource) |
| script = sourceCodeLocation.sourceCode.scriptForLocation(sourceCodeLocation); |
| |
| if (script) { |
| contextMenu.appendItem(WI.UIString("Continue to Here"), () => { |
| WI.debuggerManager.continueToLocation(script, sourceCodeLocation.lineNumber, sourceCodeLocation.columnNumber); |
| }); |
| contextMenu.appendSeparator(); |
| } |
| } |
| |
| let breakpoints = []; |
| for (let lineInfo of editorBreakpoints) { |
| let breakpoint = this._breakpointForEditorLineInfo(lineInfo); |
| console.assert(breakpoint); |
| if (breakpoint) |
| breakpoints.push(breakpoint); |
| } |
| |
| // No breakpoints. |
| if (!breakpoints.length) { |
| contextMenu.appendItem(WI.UIString("Add Breakpoint"), addBreakpoint.bind(this)); |
| return; |
| } |
| |
| // Single breakpoint. |
| if (breakpoints.length === 1) { |
| WI.breakpointPopoverController.appendContextMenuItems(contextMenu, breakpoints[0], event.target); |
| |
| if (WI.settings.experimentalEnableSourcesTab.value) { |
| if (!WI.isShowingSourcesTab()) { |
| contextMenu.appendSeparator(); |
| contextMenu.appendItem(WI.UIString("Reveal in Sources Tab"), () => { |
| WI.showSourcesTab({breakpointToSelect: breakpoints[0]}); |
| }); |
| } |
| } else { |
| if (!WI.isShowingDebuggerTab()) { |
| contextMenu.appendSeparator(); |
| contextMenu.appendItem(WI.UIString("Reveal in Debugger Tab"), () => { |
| WI.showDebuggerTab({breakpointToSelect: breakpoints[0]}); |
| }); |
| } |
| } |
| |
| return; |
| } |
| |
| let shouldDisable = breakpoints.some((breakpoint) => !breakpoint.disabled); |
| contextMenu.appendItem(shouldDisable ? WI.UIString("Disable Breakpoints") : WI.UIString("Enable Breakpoints"), () => { |
| for (let breakpoint of breakpoints) |
| breakpoint.disabled = shouldDisable; |
| }); |
| |
| contextMenu.appendItem(WI.UIString("Delete Breakpoints"), () => { |
| for (let breakpoint of breakpoints) |
| WI.debuggerManager.removeBreakpoint(breakpoint); |
| }); |
| } |
| |
| textEditorBreakpointAdded(textEditor, lineNumber, columnNumber) |
| { |
| if (!this._supportsDebugging) |
| return null; |
| |
| var editorLineInfo = {lineNumber, columnNumber}; |
| var unformattedLineInfo = this._unformattedLineInfoForEditorLineInfo(editorLineInfo); |
| var sourceCodeLocation = this._sourceCode.createSourceCodeLocation(unformattedLineInfo.lineNumber, unformattedLineInfo.columnNumber); |
| var breakpoint = new WI.Breakpoint(sourceCodeLocation); |
| |
| var lineInfo = this._editorLineInfoForSourceCodeLocation(breakpoint.sourceCodeLocation); |
| this._addBreakpointWithEditorLineInfo(breakpoint, lineInfo); |
| |
| this._ignoreBreakpointAddedBreakpoint = breakpoint; |
| WI.debuggerManager.addBreakpoint(breakpoint); |
| this._ignoreBreakpointAddedBreakpoint = null; |
| |
| // Return the more accurate location and breakpoint info. |
| return { |
| breakpointInfo: this._breakpointInfoForBreakpoint(breakpoint), |
| lineNumber: lineInfo.lineNumber, |
| columnNumber: lineInfo.columnNumber |
| }; |
| } |
| |
| textEditorBreakpointRemoved(textEditor, lineNumber, columnNumber) |
| { |
| console.assert(this._supportsDebugging); |
| if (!this._supportsDebugging) |
| return; |
| |
| var lineInfo = {lineNumber, columnNumber}; |
| var breakpoint = this._breakpointForEditorLineInfo(lineInfo); |
| console.assert(breakpoint); |
| if (!breakpoint) |
| return; |
| |
| this._removeBreakpointWithEditorLineInfo(breakpoint, lineInfo); |
| |
| this._ignoreBreakpointRemovedBreakpoint = breakpoint; |
| WI.debuggerManager.removeBreakpoint(breakpoint); |
| this._ignoreBreakpointRemovedBreakpoint = null; |
| } |
| |
| textEditorBreakpointMoved(textEditor, oldLineNumber, oldColumnNumber, newLineNumber, newColumnNumber) |
| { |
| console.assert(this._supportsDebugging); |
| if (!this._supportsDebugging) |
| return; |
| |
| var oldLineInfo = {lineNumber: oldLineNumber, columnNumber: oldColumnNumber}; |
| var breakpoint = this._breakpointForEditorLineInfo(oldLineInfo); |
| console.assert(breakpoint); |
| if (!breakpoint) |
| return; |
| |
| this._removeBreakpointWithEditorLineInfo(breakpoint, oldLineInfo); |
| |
| var newLineInfo = {lineNumber: newLineNumber, columnNumber: newColumnNumber}; |
| var unformattedNewLineInfo = this._unformattedLineInfoForEditorLineInfo(newLineInfo); |
| this._ignoreLocationUpdateBreakpoint = breakpoint; |
| breakpoint.sourceCodeLocation.update(this._sourceCode, unformattedNewLineInfo.lineNumber, unformattedNewLineInfo.columnNumber); |
| this._ignoreLocationUpdateBreakpoint = null; |
| |
| var accurateNewLineInfo = this._editorLineInfoForSourceCodeLocation(breakpoint.sourceCodeLocation); |
| this._addBreakpointWithEditorLineInfo(breakpoint, accurateNewLineInfo); |
| |
| if (accurateNewLineInfo.lineNumber !== newLineInfo.lineNumber || accurateNewLineInfo.columnNumber !== newLineInfo.columnNumber) |
| this.updateBreakpointLineAndColumn(newLineInfo.lineNumber, newLineInfo.columnNumber, accurateNewLineInfo.lineNumber, accurateNewLineInfo.columnNumber); |
| } |
| |
| textEditorBreakpointClicked(textEditor, lineNumber, columnNumber) |
| { |
| console.assert(this._supportsDebugging); |
| if (!this._supportsDebugging) |
| return; |
| |
| var breakpoint = this._breakpointForEditorLineInfo({lineNumber, columnNumber}); |
| console.assert(breakpoint); |
| if (!breakpoint) |
| return; |
| |
| breakpoint.cycleToNextMode(); |
| } |
| |
| textEditorUpdatedFormatting(textEditor) |
| { |
| this._ignoreAllBreakpointLocationUpdates = true; |
| this._sourceCode.formatterSourceMap = this.formatterSourceMap; |
| this._ignoreAllBreakpointLocationUpdates = false; |
| |
| // Always put the source map on both the Script and Resource if both exist. For example, |
| // if this SourceCode is a Resource, then there might also be a Script. In the debugger, |
| // the backend identifies call frames with Script line and column information, and the |
| // Script needs the formatter source map to produce the proper display line and column. |
| if (this._sourceCode instanceof WI.Resource && !(this._sourceCode instanceof WI.SourceMapResource)) { |
| var scripts = this._sourceCode.scripts; |
| for (var i = 0; i < scripts.length; ++i) |
| scripts[i].formatterSourceMap = this.formatterSourceMap; |
| } else if (this._sourceCode instanceof WI.Script) { |
| if (this._sourceCode.resource) |
| this._sourceCode.resource.formatterSourceMap = this.formatterSourceMap; |
| } |
| |
| this._handleFormatterDidChange(); |
| } |
| |
| textEditorExecutionHighlightRange(currentPosition, callback) |
| { |
| let position = this.currentPositionToOriginalPosition(currentPosition); |
| |
| let script = this._getAssociatedScript(position); |
| if (!script) { |
| callback(null); |
| return; |
| } |
| |
| let {startLine, startColumn} = script.range; |
| |
| function toInlineScriptPosition(position) { |
| let columnNumber = position.lineNumber === startLine ? position.columnNumber - startColumn : position.columnNumber; |
| return new WI.SourceCodePosition(position.lineNumber - startLine, columnNumber); |
| } |
| |
| function fromInlineScriptPosition(position) { |
| let columnNumber = position.lineNumber ? position.columnNumber : position.columnNumber + startColumn; |
| return new WI.SourceCodePosition(position.lineNumber + startLine, columnNumber); |
| } |
| |
| // When returning positions, convert to positions relative to the TextEditor content. |
| let highlightSourceCodeRange = (startPosition, endPosition) => { |
| startPosition = this.originalPositionToCurrentPosition(fromInlineScriptPosition(startPosition)); |
| endPosition = this.originalPositionToCurrentPosition(fromInlineScriptPosition(endPosition)); |
| callback({startPosition, endPosition}); |
| }; |
| |
| script.requestScriptSyntaxTree((syntaxTree) => { |
| // After requesting the tree, we still might get a null tree from a parse error. |
| if (!syntaxTree) { |
| callback(null); |
| return; |
| } |
| |
| // Convert to the position within the inline script before querying the AST. |
| position = toInlineScriptPosition(position); |
| let nodes = syntaxTree.containersOfPosition(position); |
| if (!nodes.length) { |
| callback(null); |
| return; |
| } |
| |
| // Find a node starting at this offset. |
| // Avoid highlighting the entire program if this is the start of the first statement. |
| // Special case the assignment expression inside of a for..of and for..in to highlight a larger range. |
| for (let node of nodes) { |
| if (node.startPosition.equals(position) && node.type !== WI.ScriptSyntaxTree.NodeType.Program) { |
| highlightSourceCodeRange(node.startPosition, node.endPosition); |
| return; |
| } |
| if (node.type === WI.ScriptSyntaxTree.NodeType.ForInStatement || node.type === WI.ScriptSyntaxTree.NodeType.ForOfStatement) { |
| if (node.left.startPosition.equals(position)) { |
| highlightSourceCodeRange(node.left.startPosition, node.right.endPosition); |
| return; |
| } |
| } |
| if (node.startPosition.isAfter(position)) |
| break; |
| } |
| |
| // Find a node ending at this offset. (Leaving a block). |
| // We check this after ensuring nothing starts with this offset, |
| // as that would be more important. |
| for (let node of nodes) { |
| if (node.endPosition.equals(position)) { |
| if (node.type === WI.ScriptSyntaxTree.NodeType.BlockStatement) { |
| // Closing brace of a block, only highlight the closing brace character. |
| highlightSourceCodeRange(position.offsetColumn(-1), position); |
| return; |
| } |
| } |
| if (node.startPosition.isAfter(position)) |
| break; |
| } |
| |
| // Find the best container node for this expression. |
| // Sort by the tightest bounds so we can walk from specific to general nodes. |
| nodes.sort((a, b) => { |
| let aLength = a.range[1] - a.range[0]; |
| let bLength = b.range[1] - b.range[0]; |
| return aLength - bLength; |
| }); |
| |
| let characterAtPosition = this.getTextInRange(currentPosition, currentPosition.offsetColumn(1)); |
| let characterAtPositionIsDotOrBracket = characterAtPosition === "." || characterAtPosition === "["; |
| |
| for (let i = 0; i < nodes.length; ++i) { |
| let node = nodes[i]; |
| |
| // In a function call. |
| if (node.type === WI.ScriptSyntaxTree.NodeType.CallExpression |
| || node.type === WI.ScriptSyntaxTree.NodeType.NewExpression |
| || node.type === WI.ScriptSyntaxTree.NodeType.ThrowStatement) { |
| highlightSourceCodeRange(node.startPosition, node.endPosition); |
| return; |
| } |
| |
| // In the middle of a member expression we want to highlight the best |
| // member expression range. We can end up in the middle when we are |
| // paused inside of a getter and select the parent call frame. For |
| // these cases we may be at a '.' or '[' and we can find the best member |
| // expression from there. |
| // |
| // Examples: |
| // |
| // foo*.x.y.z => inside x looking at parent call frame => |foo.x|.y.z |
| // foo.x*.y.z => inside y looking at parent call frame => |foo.x.y|.z |
| // |
| // foo*["x"]["y"]["z"] => inside x looking at parent call frame => |foo["x"]|["y"]["z"] |
| // foo["x"]*["y"]["z"] => inside y looking at parent call frame => |foo["x"]["y"]|["z"] |
| // |
| if (node.type === WI.ScriptSyntaxTree.NodeType.ThisExpression |
| || (characterAtPositionIsDotOrBracket && (node.type === WI.ScriptSyntaxTree.NodeType.Identifier || node.type === WI.ScriptSyntaxTree.NodeType.MemberExpression))) { |
| let memberExpressionNode = null; |
| for (let j = i + 1; j < nodes.length; ++j) { |
| let nextNode = nodes[j]; |
| if (nextNode.type === WI.ScriptSyntaxTree.NodeType.MemberExpression) { |
| memberExpressionNode = nextNode; |
| if (position.equals(memberExpressionNode.endPosition)) |
| continue; |
| } |
| break; |
| } |
| |
| if (memberExpressionNode) { |
| highlightSourceCodeRange(memberExpressionNode.startPosition, memberExpressionNode.endPosition); |
| return; |
| } |
| |
| highlightSourceCodeRange(node.startPosition, node.endPosition); |
| return; |
| } |
| } |
| |
| // No matches, just highlight the line. |
| callback(null); |
| }); |
| } |
| |
| _clearIssueWidgets() |
| { |
| for (var widget of this._widgetMap.values()) |
| widget.clear(); |
| |
| this._widgetMap.clear(); |
| } |
| |
| _reinsertAllIssues() |
| { |
| this._issuesLineNumberMap.clear(); |
| this._clearIssueWidgets(); |
| |
| let issues = WI.consoleManager.issuesForSourceCode(this._sourceCode); |
| for (let issue of issues) |
| this._addIssue(issue); |
| } |
| |
| _reinsertAllThreadIndicators() |
| { |
| // Clear line styles. |
| for (let lineNumber of this._threadLineNumberMap.keys()) |
| this.removeStyleClassFromLine(lineNumber, "thread-indicator"); |
| this._threadLineNumberMap.clear(); |
| |
| // Clear widgets. |
| for (let widget of this._threadWidgetMap.values()) |
| widget.clear(); |
| this._threadWidgetMap.clear(); |
| |
| // Clear other maps. |
| this._threadTargetMap.clear(); |
| |
| let debuggableTargets = WI.targets; |
| if (debuggableTargets.length > 1) { |
| for (let target of debuggableTargets) |
| this._addThreadIndicatorForTarget(target); |
| } |
| } |
| |
| _debuggerDidPause(event) |
| { |
| this._updateTokenTrackingControllerState(); |
| if (this._typeTokenAnnotator && this._typeTokenAnnotator.isActive()) |
| this._typeTokenAnnotator.refresh(); |
| if (this._basicBlockAnnotator && this._basicBlockAnnotator.isActive()) |
| this._basicBlockAnnotator.refresh(); |
| } |
| |
| _debuggerDidResume(event) |
| { |
| this._updateTokenTrackingControllerState(); |
| this._dismissPopover(); |
| if (this._typeTokenAnnotator && this._typeTokenAnnotator.isActive()) |
| this._typeTokenAnnotator.refresh(); |
| if (this._basicBlockAnnotator && this._basicBlockAnnotator.isActive()) |
| this._basicBlockAnnotator.refresh(); |
| } |
| |
| _handleFormatterDidChange(event) |
| { |
| if (this._ignoreAllBreakpointLocationUpdates) |
| return; |
| |
| // Some breakpoints / issues may have moved, some might not have. Just go through |
| // and remove and reinsert all the breakpoints / issues. |
| |
| var oldBreakpointMap = this._breakpointMap; |
| this._breakpointMap = {}; |
| |
| for (var lineNumber in oldBreakpointMap) { |
| for (var columnNumber in oldBreakpointMap[lineNumber]) { |
| var breakpoint = oldBreakpointMap[lineNumber][columnNumber]; |
| var newLineInfo = this._editorLineInfoForSourceCodeLocation(breakpoint.sourceCodeLocation); |
| this._addBreakpointWithEditorLineInfo(breakpoint, newLineInfo); |
| this.setBreakpointInfoForLineAndColumn(lineNumber, columnNumber, null); |
| this.setBreakpointInfoForLineAndColumn(newLineInfo.lineNumber, newLineInfo.columnNumber, this._breakpointInfoForBreakpoint(breakpoint)); |
| } |
| } |
| |
| this._reinsertAllIssues(); |
| this._reinsertAllThreadIndicators(); |
| } |
| |
| _sourceCodeSourceMapAdded(event) |
| { |
| WI.notifications.addEventListener(WI.Notification.GlobalModifierKeysDidChange, this._updateTokenTrackingControllerState, this); |
| this._sourceCode.removeEventListener(WI.SourceCode.Event.SourceMapAdded, this._sourceCodeSourceMapAdded, this); |
| |
| this._updateTokenTrackingControllerState(); |
| } |
| |
| _updateTokenTrackingControllerState() |
| { |
| var mode = WI.CodeMirrorTokenTrackingController.Mode.None; |
| if (WI.debuggerManager.paused) |
| mode = WI.CodeMirrorTokenTrackingController.Mode.JavaScriptExpression; |
| else if (this._typeTokenAnnotator && this._typeTokenAnnotator.isActive()) |
| mode = WI.CodeMirrorTokenTrackingController.Mode.JavaScriptTypeInformation; |
| else if (this._hasColorMarkers()) |
| mode = WI.CodeMirrorTokenTrackingController.Mode.MarkedTokens; |
| else if ((this._sourceCode instanceof WI.SourceMapResource || this._sourceCode.sourceMaps.length !== 0) && WI.modifierKeys.metaKey && !WI.modifierKeys.altKey && !WI.modifierKeys.shiftKey) |
| mode = WI.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens; |
| |
| this.tokenTrackingController.enabled = mode !== WI.CodeMirrorTokenTrackingController.Mode.None; |
| |
| if (mode === this.tokenTrackingController.mode) |
| return; |
| |
| switch (mode) { |
| case WI.CodeMirrorTokenTrackingController.Mode.MarkedTokens: |
| this.tokenTrackingController.mouseOverDelayDuration = 0; |
| this.tokenTrackingController.mouseOutReleaseDelayDuration = 0; |
| break; |
| case WI.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens: |
| this.tokenTrackingController.mouseOverDelayDuration = 0; |
| this.tokenTrackingController.mouseOutReleaseDelayDuration = 0; |
| this.tokenTrackingController.classNameForHighlightedRange = WI.CodeMirrorTokenTrackingController.JumpToSymbolHighlightStyleClassName; |
| this._dismissPopover(); |
| break; |
| case WI.CodeMirrorTokenTrackingController.Mode.JavaScriptExpression: |
| case WI.CodeMirrorTokenTrackingController.Mode.JavaScriptTypeInformation: |
| this.tokenTrackingController.mouseOverDelayDuration = WI.SourceCodeTextEditor.DurationToMouseOverTokenToMakeHoveredToken; |
| this.tokenTrackingController.mouseOutReleaseDelayDuration = WI.SourceCodeTextEditor.DurationToMouseOutOfHoveredTokenToRelease; |
| this.tokenTrackingController.classNameForHighlightedRange = WI.SourceCodeTextEditor.HoveredExpressionHighlightStyleClassName; |
| break; |
| } |
| |
| this.tokenTrackingController.mode = mode; |
| } |
| |
| _hasColorMarkers() |
| { |
| for (var marker of this.markers) { |
| if (marker.type === WI.TextMarker.Type.Color) |
| return true; |
| } |
| return false; |
| } |
| |
| // CodeMirrorTokenTrackingController Delegate |
| |
| tokenTrackingControllerCanReleaseHighlightedRange(tokenTrackingController, element) |
| { |
| if (!this._popover) |
| return true; |
| |
| if (!window.getSelection().isCollapsed && this._popover.element.contains(window.getSelection().anchorNode)) |
| return false; |
| |
| return true; |
| } |
| |
| tokenTrackingControllerHighlightedRangeReleased(tokenTrackingController, forceHide = false) |
| { |
| if (forceHide || !this._mouseIsOverPopover) |
| this._dismissPopover(); |
| } |
| |
| tokenTrackingControllerHighlightedRangeWasClicked(tokenTrackingController) |
| { |
| if (this.tokenTrackingController.mode !== WI.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens) |
| return; |
| |
| // Links are handled by TextEditor. |
| if (/\blink\b/.test(this.tokenTrackingController.candidate.hoveredToken.type)) |
| return; |
| |
| const options = { |
| ignoreNetworkTab: true, |
| ignoreSearchTab: true, |
| }; |
| |
| var sourceCodeLocation = this._sourceCodeLocationForEditorPosition(this.tokenTrackingController.candidate.hoveredTokenRange.start); |
| if (this.sourceCode instanceof WI.SourceMapResource) |
| WI.showOriginalOrFormattedSourceCodeLocation(sourceCodeLocation, options); |
| else |
| WI.showSourceCodeLocation(sourceCodeLocation, options); |
| } |
| |
| tokenTrackingControllerNewHighlightCandidate(tokenTrackingController, candidate) |
| { |
| if (this.tokenTrackingController.mode === WI.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens) { |
| this.tokenTrackingController.highlightRange(candidate.hoveredTokenRange); |
| return; |
| } |
| |
| if (this.tokenTrackingController.mode === WI.CodeMirrorTokenTrackingController.Mode.JavaScriptExpression) { |
| this._tokenTrackingControllerHighlightedJavaScriptExpression(candidate); |
| return; |
| } |
| |
| if (this.tokenTrackingController.mode === WI.CodeMirrorTokenTrackingController.Mode.JavaScriptTypeInformation) { |
| this._tokenTrackingControllerHighlightedJavaScriptTypeInformation(candidate); |
| return; |
| } |
| |
| if (this.tokenTrackingController.mode === WI.CodeMirrorTokenTrackingController.Mode.MarkedTokens) { |
| var markers = this.markersAtPosition(candidate.hoveredTokenRange.start); |
| if (markers.length > 0) |
| this._tokenTrackingControllerHighlightedMarkedExpression(candidate, markers); |
| else |
| this._dismissEditingController(); |
| } |
| } |
| |
| tokenTrackingControllerMouseOutOfHoveredMarker(tokenTrackingController, hoveredMarker) |
| { |
| this._dismissEditingController(); |
| } |
| |
| _tokenTrackingControllerHighlightedJavaScriptExpression(candidate) |
| { |
| console.assert(candidate.expression); |
| |
| let target = WI.debuggerManager.activeCallFrame ? WI.debuggerManager.activeCallFrame.target : this.target; |
| let expression = appendWebInspectorSourceURL(candidate.expression); |
| |
| function populate(error, result, wasThrown) |
| { |
| if (error || wasThrown) |
| return; |
| |
| if (candidate !== this.tokenTrackingController.candidate) |
| return; |
| |
| let data = WI.RemoteObject.fromPayload(result, target); |
| switch (data.type) { |
| case "function": |
| this._showPopoverForFunction(data); |
| break; |
| case "object": |
| if (data.subtype === "null" || data.subtype === "regexp") |
| this._showPopoverWithFormattedValue(data); |
| else |
| this._showPopoverForObject(data); |
| break; |
| case "string": |
| case "number": |
| case "boolean": |
| case "symbol": |
| case "bigint": |
| case "undefined": |
| this._showPopoverWithFormattedValue(data); |
| break; |
| } |
| } |
| |
| |
| if (WI.debuggerManager.activeCallFrame) { |
| target.DebuggerAgent.evaluateOnCallFrame.invoke({callFrameId: WI.debuggerManager.activeCallFrame.id, expression, objectGroup: "popover", doNotPauseOnExceptionsAndMuteConsole: true}, populate.bind(this), target.DebuggerAgent); |
| return; |
| } |
| |
| // No call frame available. Use the SourceCode's page's context. |
| target.RuntimeAgent.evaluate.invoke({expression, objectGroup: "popover", doNotPauseOnExceptionsAndMuteConsole: true}, populate.bind(this), target.RuntimeAgent); |
| } |
| |
| _tokenTrackingControllerHighlightedJavaScriptTypeInformation(candidate) |
| { |
| console.assert(candidate.expression); |
| |
| var sourceCode = this._sourceCode; |
| var sourceID = sourceCode instanceof WI.Script ? sourceCode.id : sourceCode.scripts[0].id; |
| var range = candidate.hoveredTokenRange; |
| var offset = this.currentPositionToOriginalOffset(range.start); |
| |
| var allRequests = [{ |
| typeInformationDescriptor: WI.ScriptSyntaxTree.TypeProfilerSearchDescriptor.NormalExpression, |
| sourceID, |
| divot: offset |
| }]; |
| |
| function handler(error, allTypes) { |
| if (error) |
| return; |
| |
| if (candidate !== this.tokenTrackingController.candidate) |
| return; |
| |
| console.assert(allTypes.length === 1); |
| if (!allTypes.length) |
| return; |
| |
| var typeDescription = WI.TypeDescription.fromPayload(allTypes[0]); |
| if (typeDescription.valid) { |
| var popoverTitle = WI.TypeTokenView.titleForPopover(WI.TypeTokenView.TitleType.Variable, candidate.expression); |
| this.showPopoverForTypes(typeDescription, null, popoverTitle); |
| } |
| } |
| |
| this.target.RuntimeAgent.getRuntimeTypesForVariablesAtOffsets(allRequests, handler.bind(this)); |
| } |
| |
| _showPopover(content, bounds) |
| { |
| console.assert(this.tokenTrackingController.candidate || bounds); |
| |
| var shouldHighlightRange = false; |
| var candidate = this.tokenTrackingController.candidate; |
| // If bounds is falsey, this is a popover introduced from a hover event. |
| // Otherwise, this is called from TypeTokenAnnotator. |
| if (!bounds) { |
| if (!candidate) |
| return; |
| |
| var rects = this.rectsForRange(candidate.hoveredTokenRange); |
| bounds = WI.Rect.unionOfRects(rects); |
| |
| if (this._popover && this._popover.visible) { |
| let intersection = bounds.intersectionWithRect(this._popover.frame); |
| if (intersection.size.width && intersection.size.height) |
| return; |
| } |
| |
| shouldHighlightRange = true; |
| } |
| |
| content.classList.add(WI.SourceCodeTextEditor.PopoverDebuggerContentStyleClassName); |
| |
| this._popover = this._popover || new WI.Popover(this); |
| this._popover.presentNewContentWithFrame(content, bounds.pad(5), [WI.RectEdge.MIN_Y, WI.RectEdge.MAX_Y, WI.RectEdge.MAX_X]); |
| if (shouldHighlightRange) |
| this.tokenTrackingController.highlightRange(candidate.expressionRange); |
| |
| this._trackPopoverEvents(); |
| } |
| |
| _showPopoverForFunction(data) |
| { |
| let candidate = this.tokenTrackingController.candidate; |
| |
| function didGetDetails(error, response) |
| { |
| if (error) { |
| console.error(error); |
| this._dismissPopover(); |
| return; |
| } |
| |
| // Nothing to do if the token has changed since the time we |
| // asked for the function details from the backend. |
| if (candidate !== this.tokenTrackingController.candidate) |
| return; |
| |
| let content = document.createElement("div"); |
| content.classList.add("function"); |
| |
| let title = document.createElement("div"); |
| title.classList.add("title"); |
| title.textContent = response.name || response.displayName || WI.UIString("(anonymous function)"); |
| content.appendChild(title); |
| |
| let location = response.location; |
| let sourceCode = WI.debuggerManager.scriptForIdentifier(location.scriptId, this.target); |
| let sourceCodeLocation = sourceCode.createSourceCodeLocation(location.lineNumber, location.columnNumber); |
| let functionSourceCodeLink = WI.createSourceCodeLocationLink(sourceCodeLocation); |
| title.appendChild(functionSourceCodeLink); |
| |
| let wrapper = document.createElement("div"); |
| wrapper.classList.add("body"); |
| content.appendChild(wrapper); |
| |
| let codeMirror = WI.CodeMirrorEditor.create(wrapper, { |
| mode: "text/javascript", |
| readOnly: "nocursor", |
| }); |
| |
| const isModule = false; |
| const indentString = WI.indentString(); |
| const includeSourceMapData = false; |
| let workerProxy = WI.FormatterWorkerProxy.singleton(); |
| workerProxy.formatJavaScript(data.description, isModule, indentString, includeSourceMapData, ({formattedText}) => { |
| if (candidate !== this.tokenTrackingController.candidate) |
| return; |
| |
| this._showPopover(content); |
| codeMirror.setValue(formattedText || data.description); |
| this._popover.update(); |
| }); |
| } |
| |
| data.target.DebuggerAgent.getFunctionDetails(data.objectId, didGetDetails.bind(this)); |
| } |
| |
| _showPopoverForObject(data) |
| { |
| var content = document.createElement("div"); |
| content.className = "object expandable"; |
| |
| var titleElement = document.createElement("div"); |
| titleElement.className = "title"; |
| titleElement.textContent = data.description; |
| content.appendChild(titleElement); |
| |
| if (data.subtype === "node") { |
| data.pushNodeToFrontend(function(nodeId) { |
| if (!nodeId) |
| return; |
| |
| var domNode = WI.domManager.nodeForId(nodeId); |
| if (!domNode.ownerDocument) |
| return; |
| |
| var goToButton = titleElement.appendChild(WI.createGoToArrowButton()); |
| goToButton.addEventListener("click", function() { |
| WI.domManager.inspectElement(nodeId); |
| }); |
| }); |
| } |
| |
| // FIXME: If this is a variable, it would be nice to put the variable name in the PropertyPath. |
| var objectTree = new WI.ObjectTreeView(data); |
| objectTree.showOnlyProperties(); |
| objectTree.expand(); |
| |
| var bodyElement = content.appendChild(document.createElement("div")); |
| bodyElement.className = "body"; |
| bodyElement.appendChild(objectTree.element); |
| |
| // Show the popover once we have the first set of properties for the object. |
| var candidate = this.tokenTrackingController.candidate; |
| objectTree.addEventListener(WI.ObjectTreeView.Event.Updated, function() { |
| if (candidate === this.tokenTrackingController.candidate) |
| this._showPopover(content); |
| objectTree.removeEventListener(null, null, this); |
| }, this); |
| } |
| |
| _showPopoverWithFormattedValue(remoteObject) |
| { |
| var content = WI.FormattedValue.createElementForRemoteObject(remoteObject); |
| this._showPopover(content); |
| } |
| |
| willDismissPopover(popover) |
| { |
| this.tokenTrackingController.removeHighlightedRange(); |
| |
| this.target.RuntimeAgent.releaseObjectGroup("popover"); |
| } |
| |
| _dismissPopover() |
| { |
| if (!this._popover) |
| return; |
| |
| this._popover.dismiss(); |
| |
| if (this._popoverEventListeners && this._popoverEventListenersAreRegistered) { |
| this._popoverEventListenersAreRegistered = false; |
| this._popoverEventListeners.unregister(); |
| } |
| } |
| |
| _trackPopoverEvents() |
| { |
| if (!this._popoverEventListeners) |
| this._popoverEventListeners = new WI.EventListenerSet(this, "Popover listeners"); |
| if (!this._popoverEventListenersAreRegistered) { |
| this._popoverEventListenersAreRegistered = true; |
| this._popoverEventListeners.register(this._popover.element, "mouseover", this._popoverMouseover); |
| this._popoverEventListeners.register(this._popover.element, "mouseout", this._popoverMouseout); |
| this._popoverEventListeners.install(); |
| } |
| } |
| |
| _popoverMouseover(event) |
| { |
| this._mouseIsOverPopover = true; |
| } |
| |
| _popoverMouseout(event) |
| { |
| this._mouseIsOverPopover = this._popover.element.contains(event.relatedTarget); |
| } |
| |
| _hasStyleSheetContents() |
| { |
| let mimeType = this.mimeType; |
| return mimeType === "text/css" |
| || mimeType === "text/x-less" |
| || mimeType === "text/x-sass" |
| || mimeType === "text/x-scss"; |
| } |
| |
| _updateEditableMarkers(range) |
| { |
| if (this._hasStyleSheetContents()) { |
| this.createColorMarkers(range); |
| this.createGradientMarkers(range); |
| this.createCubicBezierMarkers(range); |
| this.createSpringMarkers(range); |
| } |
| |
| this._updateTokenTrackingControllerState(); |
| } |
| |
| _tokenTrackingControllerHighlightedMarkedExpression(candidate, markers) |
| { |
| // Look for the outermost editable marker. |
| var editableMarker; |
| for (var marker of markers) { |
| if (!marker.range || !Object.values(WI.TextMarker.Type).includes(marker.type)) |
| continue; |
| |
| if (!editableMarker || (marker.range.startLine < editableMarker.range.startLine || (marker.range.startLine === editableMarker.range.startLine && marker.range.startColumn < editableMarker.range.startColumn))) |
| editableMarker = marker; |
| } |
| |
| if (!editableMarker) { |
| this.tokenTrackingController.hoveredMarker = null; |
| return; |
| } |
| |
| if (this.tokenTrackingController.hoveredMarker === editableMarker) |
| return; |
| |
| this._dismissEditingController(); |
| |
| this.tokenTrackingController.hoveredMarker = editableMarker; |
| |
| this._editingController = this.editingControllerForMarker(editableMarker); |
| |
| if (marker.type === WI.TextMarker.Type.Color) { |
| var color = this._editingController.value; |
| if (!color || !color.valid) { |
| editableMarker.clear(); |
| this._editingController = null; |
| return; |
| } |
| } |
| |
| this._editingController.delegate = this; |
| this._editingController.presentHoverMenu(); |
| } |
| |
| _dismissEditingController(discrete) |
| { |
| if (this._editingController) |
| this._editingController.dismissHoverMenu(discrete); |
| |
| this.tokenTrackingController.hoveredMarker = null; |
| this._editingController = null; |
| } |
| |
| // CodeMirrorEditingController Delegate |
| |
| editingControllerDidStartEditing(editingController) |
| { |
| // We can pause the token tracking controller during editing, it will be reset |
| // to the expected state by calling _updateEditableMarkers() in the |
| // editingControllerDidFinishEditing delegate. |
| this.tokenTrackingController.enabled = false; |
| |
| // We clear the marker since we'll reset it after editing. |
| editingController.marker.clear(); |
| |
| // We ignore content changes made as a result of color editing. |
| this._ignoreContentDidChange++; |
| } |
| |
| editingControllerDidFinishEditing(editingController) |
| { |
| this._updateEditableMarkers(editingController.range); |
| |
| this._ignoreContentDidChange--; |
| |
| this._editingController = null; |
| } |
| |
| _setTypeTokenAnnotatorEnabledState(shouldActivate) |
| { |
| if (!this._typeTokenAnnotator) |
| return; |
| |
| if (shouldActivate) { |
| console.assert(this.visible, "Annotators should not be enabled if the TextEditor is not visible"); |
| |
| this._typeTokenAnnotator.reset(); |
| |
| if (!this._typeTokenScrollHandler) |
| this._enableScrollEventsForTypeTokenAnnotator(); |
| } else { |
| this._typeTokenAnnotator.clear(); |
| |
| if (this._typeTokenScrollHandler) |
| this._disableScrollEventsForTypeTokenAnnotator(); |
| } |
| |
| WI.settings.showJavaScriptTypeInformation.value = shouldActivate; |
| |
| this._updateTokenTrackingControllerState(); |
| } |
| |
| set _basicBlockAnnotatorEnabled(shouldActivate) |
| { |
| if (!this._basicBlockAnnotator) |
| return; |
| |
| if (shouldActivate) { |
| console.assert(this.visible, "Annotators should not be enabled if the TextEditor is not visible"); |
| |
| console.assert(!this._basicBlockAnnotator.isActive()); |
| this._basicBlockAnnotator.reset(); |
| |
| if (!this._controlFlowScrollHandler) |
| this._enableScrollEventsForControlFlowAnnotator(); |
| } else { |
| this._basicBlockAnnotator.clear(); |
| |
| if (this._controlFlowScrollHandler) |
| this._disableScrollEventsForControlFlowAnnotator(); |
| } |
| |
| WI.settings.enableControlFlowProfiler.value = shouldActivate; |
| } |
| |
| _getAssociatedScript(position) |
| { |
| let script = null; |
| |
| if (this._sourceCode instanceof WI.Script) |
| script = this._sourceCode; |
| else if (this._sourceCode instanceof WI.Resource && this._sourceCode.scripts.length) { |
| if (this._sourceCode.type === WI.Resource.Type.Script) |
| script = this._sourceCode.scripts[0]; |
| else if (this._sourceCode.type === WI.Resource.Type.Document && position) { |
| for (let inlineScript of this._sourceCode.scripts) { |
| if (inlineScript.range.contains(position.lineNumber, position.columnNumber)) { |
| if (isNaN(inlineScript.range.startOffset)) |
| inlineScript.range.resolveOffsets(this._sourceCode.content); |
| script = inlineScript; |
| break; |
| } |
| } |
| } |
| } |
| |
| return script; |
| } |
| |
| _createTypeTokenAnnotator() |
| { |
| // COMPATIBILITY (iOS 8): Runtime.getRuntimeTypesForVariablesAtOffsets did not exist yet. |
| if (!this.target.RuntimeAgent.getRuntimeTypesForVariablesAtOffsets) |
| return; |
| |
| var script = this._getAssociatedScript(); |
| if (!script) |
| return; |
| |
| this._typeTokenAnnotator = new WI.TypeTokenAnnotator(this, script); |
| } |
| |
| _createBasicBlockAnnotator() |
| { |
| // COMPATIBILITY (iOS 8): Runtime.getBasicBlocks did not exist yet. |
| if (!this.target.RuntimeAgent.getBasicBlocks) |
| return; |
| |
| var script = this._getAssociatedScript(); |
| if (!script) |
| return; |
| |
| this._basicBlockAnnotator = new WI.BasicBlockAnnotator(this, script); |
| } |
| |
| _enableScrollEventsForTypeTokenAnnotator() |
| { |
| // Pause updating type tokens while scrolling to prevent frame loss. |
| console.assert(!this._typeTokenScrollHandler); |
| this._typeTokenScrollHandler = this._createTypeTokenScrollEventHandler(); |
| this.addScrollHandler(this._typeTokenScrollHandler); |
| } |
| |
| _enableScrollEventsForControlFlowAnnotator() |
| { |
| console.assert(!this._controlFlowScrollHandler); |
| this._controlFlowScrollHandler = this._createControlFlowScrollEventHandler(); |
| this.addScrollHandler(this._controlFlowScrollHandler); |
| } |
| |
| _disableScrollEventsForTypeTokenAnnotator() |
| { |
| console.assert(this._typeTokenScrollHandler); |
| this.removeScrollHandler(this._typeTokenScrollHandler); |
| this._typeTokenScrollHandler = null; |
| } |
| |
| _disableScrollEventsForControlFlowAnnotator() |
| { |
| console.assert(this._controlFlowScrollHandler); |
| this.removeScrollHandler(this._controlFlowScrollHandler); |
| this._controlFlowScrollHandler = null; |
| } |
| |
| _createTypeTokenScrollEventHandler() |
| { |
| let timeoutIdentifier = null; |
| let scrollHandler = () => { |
| if (timeoutIdentifier) |
| clearTimeout(timeoutIdentifier); |
| else { |
| if (this._typeTokenAnnotator) |
| this._typeTokenAnnotator.pause(); |
| } |
| |
| timeoutIdentifier = setTimeout(() => { |
| timeoutIdentifier = null; |
| if (this._typeTokenAnnotator) |
| this._typeTokenAnnotator.resume(); |
| }, WI.SourceCodeTextEditor.DurationToUpdateTypeTokensAfterScrolling); |
| }; |
| |
| return scrollHandler; |
| } |
| |
| _createControlFlowScrollEventHandler() |
| { |
| let timeoutIdentifier = null; |
| let scrollHandler = () => { |
| if (timeoutIdentifier) |
| clearTimeout(timeoutIdentifier); |
| else if (this._basicBlockAnnotator) |
| this._basicBlockAnnotator.pause(); |
| |
| timeoutIdentifier = setTimeout(() => { |
| timeoutIdentifier = null; |
| if (this._basicBlockAnnotator) |
| this._basicBlockAnnotator.resume(); |
| }, WI.SourceCodeTextEditor.DurationToUpdateTypeTokensAfterScrolling); |
| }; |
| |
| return scrollHandler; |
| } |
| |
| _logCleared(event) |
| { |
| for (let lineNumber of this._issuesLineNumberMap.keys()) { |
| this.removeStyleClassFromLine(lineNumber, WI.SourceCodeTextEditor.LineErrorStyleClassName); |
| this.removeStyleClassFromLine(lineNumber, WI.SourceCodeTextEditor.LineWarningStyleClassName); |
| } |
| |
| this._issuesLineNumberMap.clear(); |
| this._clearIssueWidgets(); |
| } |
| }; |
| |
| WI.SourceCodeTextEditor.LineErrorStyleClassName = "error"; |
| WI.SourceCodeTextEditor.LineWarningStyleClassName = "warning"; |
| WI.SourceCodeTextEditor.PopoverDebuggerContentStyleClassName = "debugger-popover-content"; |
| WI.SourceCodeTextEditor.HoveredExpressionHighlightStyleClassName = "hovered-expression-highlight"; |
| WI.SourceCodeTextEditor.DurationToMouseOverTokenToMakeHoveredToken = 500; |
| WI.SourceCodeTextEditor.DurationToMouseOutOfHoveredTokenToRelease = 1000; |
| WI.SourceCodeTextEditor.DurationToUpdateTypeTokensAfterScrolling = 100; |
| WI.SourceCodeTextEditor.WidgetContainsMultipleIssuesSymbol = Symbol("source-code-widget-contains-multiple-issues"); |
| WI.SourceCodeTextEditor.WidgetContainsMultipleThreadsSymbol = Symbol("source-code-widget-contains-multiple-threads"); |
| |
| WI.SourceCodeTextEditor.Event = { |
| ContentWillPopulate: "source-code-text-editor-content-will-populate", |
| ContentDidPopulate: "source-code-text-editor-content-did-populate" |
| }; |