/* * Copyright (C) 2013 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. */ WebInspector.SourceCodeTextEditor = function(sourceCode) { console.assert(sourceCode instanceof WebInspector.SourceCode); this._sourceCode = sourceCode; this._breakpointMap = {}; this._issuesLineNumberMap = {}; this._contentPopulated = false; this._invalidLineNumbers = {0: true}; this._ignoreContentDidChange = 0; WebInspector.TextEditor.call(this, null, null, this); // 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(WebInspector.SourceCodeTextEditor.StyleClassName); if (this._supportsDebugging) { WebInspector.Breakpoint.addEventListener(WebInspector.Breakpoint.Event.DisabledStateDidChange, this._updateBreakpointStatus, this); WebInspector.Breakpoint.addEventListener(WebInspector.Breakpoint.Event.AutoContinueDidChange, this._updateBreakpointStatus, this); WebInspector.Breakpoint.addEventListener(WebInspector.Breakpoint.Event.ResolvedStateDidChange, this._updateBreakpointStatus, this); WebInspector.Breakpoint.addEventListener(WebInspector.Breakpoint.Event.LocationDidChange, this._updateBreakpointLocation, this); WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.BreakpointAdded, this._breakpointAdded, this); WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.BreakpointRemoved, this._breakpointRemoved, this); WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.ActiveCallFrameDidChange, this._activeCallFrameDidChange, this); WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.Paused, this._debuggerDidPause, this); WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.Resumed, this._debuggerDidResume, this); if (WebInspector.debuggerManager.activeCallFrame) this._debuggerDidPause(); this._activeCallFrameDidChange(); } WebInspector.issueManager.addEventListener(WebInspector.IssueManager.Event.IssueWasAdded, this._issueWasAdded, this); if (this._sourceCode instanceof WebInspector.SourceMapResource || this._sourceCode.sourceMaps.length > 0) WebInspector.notifications.addEventListener(WebInspector.Notification.GlobalModifierKeysDidChange, this._updateTokenTrackingControllerState, this); else this._sourceCode.addEventListener(WebInspector.SourceCode.Event.SourceMapAdded, this._sourceCodeSourceMapAdded, this); sourceCode.requestContent(this._contentAvailable.bind(this)); // FIXME: Cmd+L shorcut doesn't actually work. new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Command, "L", this.showGoToLineDialog.bind(this), this.element); new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Control, "G", this.showGoToLineDialog.bind(this), this.element); }; WebInspector.Object.addConstructorFunctions(WebInspector.SourceCodeTextEditor); WebInspector.SourceCodeTextEditor.StyleClassName = "source-code"; WebInspector.SourceCodeTextEditor.LineErrorStyleClassName = "error"; WebInspector.SourceCodeTextEditor.LineWarningStyleClassName = "warning"; WebInspector.SourceCodeTextEditor.PopoverDebuggerContentStyleClassName = "debugger-popover-content"; WebInspector.SourceCodeTextEditor.HoveredExpressionHighlightStyleClassName = "hovered-expression-highlight"; WebInspector.SourceCodeTextEditor.DurationToMouseOverTokenToMakeHoveredToken = 500; WebInspector.SourceCodeTextEditor.DurationToMouseOutOfHoveredTokenToRelease = 1000; WebInspector.SourceCodeTextEditor.AutoFormatMinimumLineLength = 500; WebInspector.SourceCodeTextEditor.Event = { ContentWillPopulate: "source-code-text-editor-content-will-populate", ContentDidPopulate: "source-code-text-editor-content-did-populate" }; WebInspector.SourceCodeTextEditor.prototype = { constructor: WebInspector.SourceCodeTextEditor, // Public get sourceCode() { return this._sourceCode; }, hidden: function() { WebInspector.TextEditor.prototype.hidden.call(this); this.tokenTrackingController.removeHighlightedRange(); this._dismissPopover(); }, close: function() { if (this._supportsDebugging) { WebInspector.Breakpoint.removeEventListener(WebInspector.Breakpoint.Event.DisabledStateDidChange, this._updateBreakpointStatus, this); WebInspector.Breakpoint.removeEventListener(WebInspector.Breakpoint.Event.AutoContinueDidChange, this._updateBreakpointStatus, this); WebInspector.Breakpoint.removeEventListener(WebInspector.Breakpoint.Event.ResolvedStateDidChange, this._updateBreakpointStatus, this); WebInspector.Breakpoint.removeEventListener(WebInspector.Breakpoint.Event.LocationDidChange, this._updateBreakpointLocation, this); WebInspector.debuggerManager.removeEventListener(WebInspector.DebuggerManager.Event.BreakpointAdded, this._breakpointAdded, this); WebInspector.debuggerManager.removeEventListener(WebInspector.DebuggerManager.Event.BreakpointRemoved, this._breakpointRemoved, this); WebInspector.debuggerManager.removeEventListener(WebInspector.DebuggerManager.Event.ActiveCallFrameDidChange, this._activeCallFrameDidChange, this); if (this._activeCallFrameSourceCodeLocation) { this._activeCallFrameSourceCodeLocation.removeEventListener(WebInspector.SourceCodeLocation.Event.LocationChanged, this._activeCallFrameSourceCodeLocationChanged, this); delete this._activeCallFrameSourceCodeLocation; } } WebInspector.issueManager.removeEventListener(WebInspector.IssueManager.Event.IssueWasAdded, this._issueWasAdded, this); WebInspector.notifications.removeEventListener(WebInspector.Notification.GlobalModifierKeysDidChange, this._updateTokenTrackingControllerState, this); this._sourceCode.removeEventListener(WebInspector.SourceCode.Event.SourceMapAdded, this._sourceCodeSourceMapAdded, this); }, canBeFormatted: function() { // 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 WebInspector.SourceMapResource) return false; return WebInspector.TextEditor.prototype.canBeFormatted.call(this); }, customPerformSearch: function(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(WebInspector.TextEditor.Event.NumberOfSearchResultsDidChange); return; } var queryRegex = new RegExp(query.escapeForRegExp(), "gi"); var searchResults = []; for (var i = 0; i < matches.length; ++i) { var matchLineNumber = matches[i].lineNumber; var line = this.line(matchLineNumber); // 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 WebInspector.TextRange(matchLineNumber, lineMatch.index, matchLineNumber, queryRegex.lastIndex); searchResults.push(resultTextRange); } } this.addSearchResults(searchResults); this.dispatchEventToListeners(WebInspector.TextEditor.Event.NumberOfSearchResultsDidChange); } if (this._sourceCode instanceof WebInspector.SourceMapResource) return false; if (this._sourceCode instanceof WebInspector.Resource) PageAgent.searchInResource(this._sourceCode.parentFrame.id, this._sourceCode.url, query, false, false, searchResultCallback.bind(this)); else if (this._sourceCode instanceof WebInspector.Script) DebuggerAgent.searchInContent(this._sourceCode.id, query, false, false, searchResultCallback.bind(this)); return true; }, showGoToLineDialog: function() { if (!this._goToLineDialog) { this._goToLineDialog = new WebInspector.GoToLineDialog; this._goToLineDialog.delegate = this; } this._goToLineDialog.present(this.element); }, isGoToLineDialogValueValid: function(goToLineDialog, lineNumber) { return !isNaN(lineNumber) && lineNumber > 0 && lineNumber <= this.lineCount; }, goToLineDialogValueWasValidated: function(goToLineDialog, lineNumber) { var position = new WebInspector.SourceCodePosition(lineNumber - 1, 0); var range = new WebInspector.TextRange(lineNumber - 1, 0, lineNumber, 0); this.revealPosition(position, range, false, true); }, goToLineDialogWasDismissed: function() { this.focus(); }, contentDidChange: function(replacedRanges, newRanges) { WebInspector.TextEditor.prototype.contentDidChange.call(this, replacedRanges, newRanges); if (this._ignoreContentDidChange > 0) return; // Gather all lines containing new text. var lines = new Set; for (var range of newRanges) { // If the range is on a single line, only add the line if the range is not empty. if (range.startLine === range.endLine) { if (range.endColumn > range.startColumn) lines.add(range.startLine); } else { // Only add the last line if the range has characters on this line. for (var line = range.startLine; line < range.endLine || range.endColumn > 0; ++line) lines.add(line); } } // Consider all new lines for new color markers. for (var line of lines) this._updateColorMarkers(line); }, // Private _unformattedLineInfoForEditorLineInfo: function(lineInfo) { if (this.formatterSourceMap) return this.formatterSourceMap.formattedToOriginal(lineInfo.lineNumber, lineInfo.columnNumber); return lineInfo; }, _sourceCodeLocationForEditorPosition: function(position) { var lineInfo = {lineNumber: position.line, columnNumber: position.ch}; var unformattedLineInfo = this._unformattedLineInfoForEditorLineInfo(lineInfo); return this.sourceCode.createSourceCodeLocation(unformattedLineInfo.lineNumber, unformattedLineInfo.columnNumber); }, _editorLineInfoForSourceCodeLocation: function(sourceCodeLocation) { if (this._sourceCode instanceof WebInspector.SourceMapResource) return {lineNumber: sourceCodeLocation.displayLineNumber, columnNumber: sourceCodeLocation.displayColumnNumber}; return {lineNumber: sourceCodeLocation.formattedLineNumber, columnNumber: sourceCodeLocation.formattedColumnNumber}; }, _breakpointForEditorLineInfo: function(lineInfo) { if (!this._breakpointMap[lineInfo.lineNumber]) return null; return this._breakpointMap[lineInfo.lineNumber][lineInfo.columnNumber]; }, _addBreakpointWithEditorLineInfo: function(breakpoint, lineInfo) { if (!this._breakpointMap[lineInfo.lineNumber]) this._breakpointMap[lineInfo.lineNumber] = {}; this._breakpointMap[lineInfo.lineNumber][lineInfo.columnNumber] = breakpoint; }, _removeBreakpointWithEditorLineInfo: function(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]; }, _contentWillPopulate: function(content) { this.dispatchEventToListeners(WebInspector.SourceCodeTextEditor.Event.ContentWillPopulate); // We only do the rest of this work before the first populate. if (this._contentPopulated) return; if (this._supportsDebugging) { this._breakpointMap = {}; var breakpoints = WebInspector.debuggerManager.breakpointsForSourceCode(this._sourceCode); for (var i = 0; i < breakpoints.length; ++i) { var breakpoint = breakpoints[i]; 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 WebInspector.Resource) this.mimeType = this._sourceCode.syntheticMIMEType; else if (this._sourceCode instanceof WebInspector.Script) this.mimeType = "text/javascript"; // Automatically format the content if it looks minified and it can be formatted. console.assert(!this.formatted); if (this.canBeFormatted()) { var lastNewlineIndex = 0; while (true) { var nextNewlineIndex = content.indexOf("\n", lastNewlineIndex); if (nextNewlineIndex === -1) { if (content.length - lastNewlineIndex > WebInspector.SourceCodeTextEditor.AutoFormatMinimumLineLength) this.autoFormat = true; break; } if (nextNewlineIndex - lastNewlineIndex > WebInspector.SourceCodeTextEditor.AutoFormatMinimumLineLength) { this.autoFormat = true; break; } lastNewlineIndex = nextNewlineIndex + 1; } } }, _contentDidPopulate: function() { this._contentPopulated = true; this.dispatchEventToListeners(WebInspector.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 avaiable. (When populating with // partial script content this can be called multiple times.) this._issuesLineNumberMap = {}; var issues = WebInspector.issueManager.issuesForSourceCode(this._sourceCode); for (var i = 0; i < issues.length; ++i) { var issue = issues[i]; console.assert(this._matchesIssue(issue)); this._addIssue(issue); } this._updateTokenTrackingControllerState(); this._updateColorMarkers(); }, _populateWithContent: function(content) { content = content || ""; this._contentWillPopulate(content); this.string = content; this._contentDidPopulate(); }, _contentAvailable: function(sourceCode, content, 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 = {}; this._populateWithContent(content); }, _updateBreakpointStatus: function(event) { console.assert(this._supportsDebugging); if (!this._contentPopulated) return; var breakpoint = event.target; if (!this._matchesBreakpoint(breakpoint)) return; var lineInfo = this._editorLineInfoForSourceCodeLocation(breakpoint.sourceCodeLocation); this.setBreakpointInfoForLineAndColumn(lineInfo.lineNumber, lineInfo.columnNumber, this._breakpointInfoForBreakpoint(breakpoint)); }, _updateBreakpointLocation: function(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 WebInspector.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: function(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: function(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); }, _activeCallFrameDidChange: function() { console.assert(this._supportsDebugging); if (this._activeCallFrameSourceCodeLocation) { this._activeCallFrameSourceCodeLocation.removeEventListener(WebInspector.SourceCodeLocation.Event.LocationChanged, this._activeCallFrameSourceCodeLocationChanged, this); delete this._activeCallFrameSourceCodeLocation; } var activeCallFrame = WebInspector.debuggerManager.activeCallFrame; if (!activeCallFrame || !this._matchesSourceCodeLocation(activeCallFrame.sourceCodeLocation)) { this.executionLineNumber = NaN; this.executionColumnNumber = NaN; return; } this._dismissPopover(); this._activeCallFrameSourceCodeLocation = activeCallFrame.sourceCodeLocation; this._activeCallFrameSourceCodeLocation.addEventListener(WebInspector.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). var lineInfo = this._editorLineInfoForSourceCodeLocation(activeCallFrame.sourceCodeLocation); this.executionLineNumber = lineInfo.lineNumber; this.executionColumnNumber = 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 WebInspector.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 "; 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(boundScriptContentAvailable); }, _populateWithScriptContent: function() { console.assert(this._sourceCode instanceof WebInspector.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(error, content) { delete this._requestingScriptContent; // 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(scriptContentAvailable.bind(this)); }, _matchesSourceCodeLocation: function(sourceCodeLocation) { if (this._sourceCode instanceof WebInspector.SourceMapResource) return sourceCodeLocation.displaySourceCode === this._sourceCode; if (this._sourceCode instanceof WebInspector.Resource) return sourceCodeLocation.sourceCode.url === this._sourceCode.url; if (this._sourceCode instanceof WebInspector.Script) return sourceCodeLocation.sourceCode === this._sourceCode; return false; }, _matchesBreakpoint: function(breakpoint) { console.assert(this._supportsDebugging); if (this._sourceCode instanceof WebInspector.SourceMapResource) return breakpoint.sourceCodeLocation.displaySourceCode === this._sourceCode; if (this._sourceCode instanceof WebInspector.Resource) return breakpoint.url === this._sourceCode.url; if (this._sourceCode instanceof WebInspector.Script) return breakpoint.url === this._sourceCode.url || breakpoint.scriptIdentifier === this._sourceCode.id; return false; }, _matchesIssue: function(issue) { if (this._sourceCode instanceof WebInspector.Resource) return issue.url === this._sourceCode.url; // FIXME: Support issues for Scripts based on id, not only by URL. if (this._sourceCode instanceof WebInspector.Script) return issue.url === this._sourceCode.url; return false; }, _issueWasAdded: function(event) { var issue = event.data.issue; if (!this._matchesIssue(issue)) return; this._addIssue(issue); }, _addIssue: function(issue) { var lineNumberIssues = this._issuesLineNumberMap[issue.lineNumber]; if (!lineNumberIssues) lineNumberIssues = this._issuesLineNumberMap[issue.lineNumber] = []; lineNumberIssues.push(issue); if (issue.level === WebInspector.IssueMessage.Level.Error) this.addStyleClassToLine(issue.lineNumber, WebInspector.SourceCodeTextEditor.LineErrorStyleClassName); else if (issue.level === WebInspector.IssueMessage.Level.Warning) this.addStyleClassToLine(issue.lineNumber, WebInspector.SourceCodeTextEditor.LineWarningStyleClassName); else console.error("Unknown issue level"); // FIXME : Show the issue message on the line as a bubble. }, _breakpointInfoForBreakpoint: function(breakpoint) { return {resolved: breakpoint.resolved, disabled: breakpoint.disabled, autoContinue: breakpoint.autoContinue}; }, get _supportsDebugging() { if (this._sourceCode instanceof WebInspector.Resource) return this._sourceCode.type === WebInspector.Resource.Type.Document || this._sourceCode.type === WebInspector.Resource.Type.Script; if (this._sourceCode instanceof WebInspector.Script) return true; return false; }, // TextEditor Delegate textEditorBaseURL: function(textEditor) { return this._sourceCode.url; }, textEditorShouldHideLineNumber: function(textEditor, lineNumber) { return lineNumber in this._invalidLineNumbers; }, textEditorGutterContextMenu: function(textEditor, lineNumber, columnNumber, editorBreakpoints, event) { if (!this._supportsDebugging) return; event.preventDefault(); var contextMenu = new WebInspector.ContextMenu(event); // Paused. Add Continue to Here option only if we have a script identifier for the location. if (WebInspector.debuggerManager.paused) { var editorLineInfo = {lineNumber:lineNumber, columnNumber:columnNumber}; var unformattedLineInfo = this._unformattedLineInfoForEditorLineInfo(editorLineInfo); var sourceCodeLocation = this._sourceCode.createSourceCodeLocation(unformattedLineInfo.lineNumber, unformattedLineInfo.columnNumber); if (sourceCodeLocation.sourceCode instanceof WebInspector.Script) var script = sourceCodeLocation.sourceCode; else if (sourceCodeLocation.sourceCode instanceof WebInspector.Resource) var script = sourceCodeLocation.sourceCode.scriptForLocation(sourceCodeLocation); if (script) { function continueToLocation() { WebInspector.debuggerManager.continueToLocation(script.id, sourceCodeLocation.lineNumber, sourceCodeLocation.columnNumber); } contextMenu.appendItem(WebInspector.UIString("Continue to Here"), continueToLocation); contextMenu.appendSeparator(); } } var breakpoints = []; for (var i = 0; i < editorBreakpoints.length; ++i) { var lineInfo = editorBreakpoints[i]; var breakpoint = this._breakpointForEditorLineInfo(lineInfo); console.assert(breakpoint); if (breakpoint) breakpoints.push(breakpoint); } // No breakpoints. if (!breakpoints.length) { function addBreakpoint() { var data = this.textEditorBreakpointAdded(this, lineNumber, columnNumber); this.setBreakpointInfoForLineAndColumn(data.lineNumber, data.columnNumber, data.breakpointInfo); } contextMenu.appendItem(WebInspector.UIString("Add Breakpoint"), addBreakpoint.bind(this)); contextMenu.show(); return; } // Single breakpoint. if (breakpoints.length === 1) { var breakpoint = breakpoints[0]; function revealInSidebar() { WebInspector.debuggerSidebarPanel.show(); var treeElement = WebInspector.debuggerSidebarPanel.treeElementForRepresentedObject(breakpoint); if (treeElement) treeElement.revealAndSelect(); } breakpoint.appendContextMenuItems(contextMenu, event.target); contextMenu.appendSeparator(); contextMenu.appendItem(WebInspector.UIString("Reveal in Debugger Navigation Sidebar"), revealInSidebar); contextMenu.show(); return; } // Multiple breakpoints. var shouldDisable = false; for (var i = 0; i < breakpoints.length; ++i) { if (!breakpoints[i].disabled) { shouldDisable = true; break; } } function removeBreakpoints() { for (var i = 0; i < breakpoints.length; ++i) { var breakpoint = breakpoints[i]; if (WebInspector.debuggerManager.isBreakpointRemovable(breakpoint)) WebInspector.debuggerManager.removeBreakpoint(breakpoint); } } function toggleBreakpoints() { for (var i = 0; i < breakpoints.length; ++i) breakpoints[i].disabled = shouldDisable; } if (shouldDisable) contextMenu.appendItem(WebInspector.UIString("Disable Breakpoints"), toggleBreakpoints.bind(this)); else contextMenu.appendItem(WebInspector.UIString("Enable Breakpoints"), toggleBreakpoints.bind(this)); contextMenu.appendItem(WebInspector.UIString("Delete Breakpoints"), removeBreakpoints.bind(this)); contextMenu.show(); }, textEditorBreakpointAdded: function(textEditor, lineNumber, columnNumber) { if (!this._supportsDebugging) return null; var editorLineInfo = {lineNumber:lineNumber, columnNumber:columnNumber}; var unformattedLineInfo = this._unformattedLineInfoForEditorLineInfo(editorLineInfo); var sourceCodeLocation = this._sourceCode.createSourceCodeLocation(unformattedLineInfo.lineNumber, unformattedLineInfo.columnNumber); var breakpoint = new WebInspector.Breakpoint(sourceCodeLocation); var lineInfo = this._editorLineInfoForSourceCodeLocation(breakpoint.sourceCodeLocation); this._addBreakpointWithEditorLineInfo(breakpoint, lineInfo); this._ignoreBreakpointAddedBreakpoint = breakpoint; WebInspector.debuggerManager.addBreakpoint(breakpoint); delete this._ignoreBreakpointAddedBreakpoint; // Return the more accurate location and breakpoint info. return { breakpointInfo: this._breakpointInfoForBreakpoint(breakpoint), lineNumber: lineInfo.lineNumber, columnNumber: lineInfo.columnNumber }; }, textEditorBreakpointRemoved: function(textEditor, lineNumber, columnNumber) { console.assert(this._supportsDebugging); if (!this._supportsDebugging) return; var lineInfo = {lineNumber: lineNumber, columnNumber: columnNumber}; var breakpoint = this._breakpointForEditorLineInfo(lineInfo); console.assert(breakpoint); if (!breakpoint) return; this._removeBreakpointWithEditorLineInfo(breakpoint, lineInfo); this._ignoreBreakpointRemovedBreakpoint = breakpoint; WebInspector.debuggerManager.removeBreakpoint(breakpoint); delete this._ignoreBreakpointAddedBreakpoint; }, textEditorBreakpointMoved: function(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); delete this._ignoreLocationUpdateBreakpoint; 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: function(textEditor, lineNumber, columnNumber) { console.assert(this._supportsDebugging); if (!this._supportsDebugging) return; var breakpoint = this._breakpointForEditorLineInfo({lineNumber: lineNumber, columnNumber: columnNumber}); console.assert(breakpoint); if (!breakpoint) return; breakpoint.cycleToNextMode(); }, textEditorUpdatedFormatting: function(textEditor) { this._ignoreAllBreakpointLocationUpdates = true; this._sourceCode.formatterSourceMap = this.formatterSourceMap; delete this._ignoreAllBreakpointLocationUpdates; // 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 WebInspector.Resource && !(this._sourceCode instanceof WebInspector.SourceMapResource)) { var scripts = this._sourceCode.scripts; for (var i = 0; i < scripts.length; ++i) scripts[i].formatterSourceMap = this.formatterSourceMap; } else if (this._sourceCode instanceof WebInspector.Script) { if (this._sourceCode.resource) this._sourceCode.resource.formatterSourceMap = this.formatterSourceMap; } // Some breakpoints may have moved, some might not have. Just go through // and remove and reinsert all the breakpoints. 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)); } } }, _debuggerDidPause: function(event) { this._updateTokenTrackingControllerState(); }, _debuggerDidResume: function(event) { this._updateTokenTrackingControllerState(); this._dismissPopover(); }, _sourceCodeSourceMapAdded: function(event) { WebInspector.notifications.addEventListener(WebInspector.Notification.GlobalModifierKeysDidChange, this._updateTokenTrackingControllerState, this); this._sourceCode.removeEventListener(WebInspector.SourceCode.Event.SourceMapAdded, this._sourceCodeSourceMapAdded, this); this._updateTokenTrackingControllerState(); }, _updateTokenTrackingControllerState: function() { var mode = WebInspector.CodeMirrorTokenTrackingController.Mode.None; if (WebInspector.debuggerManager.paused) mode = WebInspector.CodeMirrorTokenTrackingController.Mode.JavaScriptExpression; else if (this._hasColorMarkers()) mode = WebInspector.CodeMirrorTokenTrackingController.Mode.MarkedTokens; else if ((this._sourceCode instanceof WebInspector.SourceMapResource || this._sourceCode.sourceMaps.length !== 0) && WebInspector.modifierKeys.metaKey && !WebInspector.modifierKeys.altKey && !WebInspector.modifierKeys.shiftKey) mode = WebInspector.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens; this.tokenTrackingController.enabled = mode !== WebInspector.CodeMirrorTokenTrackingController.Mode.None; if (mode === this.tokenTrackingController.mode) return; switch (mode) { case WebInspector.CodeMirrorTokenTrackingController.Mode.MarkedTokens: this.tokenTrackingController.mouseOverDelayDuration = 0; this.tokenTrackingController.mouseOutReleaseDelayDuration = 0; break; case WebInspector.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens: this.tokenTrackingController.mouseOverDelayDuration = 0; this.tokenTrackingController.mouseOutReleaseDelayDuration = 0; this.tokenTrackingController.classNameForHighlightedRange = WebInspector.CodeMirrorTokenTrackingController.JumpToSymbolHighlightStyleClassName; this._dismissPopover(); break; case WebInspector.CodeMirrorTokenTrackingController.Mode.JavaScriptExpression: this.tokenTrackingController.mouseOverDelayDuration = WebInspector.SourceCodeTextEditor.DurationToMouseOverTokenToMakeHoveredToken; this.tokenTrackingController.mouseOutReleaseDelayDuration = WebInspector.SourceCodeTextEditor.DurationToMouseOutOfHoveredTokenToRelease; this.tokenTrackingController.classNameForHighlightedRange = WebInspector.SourceCodeTextEditor.HoveredExpressionHighlightStyleClassName; break; } this.tokenTrackingController.mode = mode; }, _hasColorMarkers: function() { for (var marker of this.markers) { if (marker.type === WebInspector.TextMarker.Type.Color) return true; } return false; }, // CodeMirrorTokenTrackingController Delegate tokenTrackingControllerCanReleaseHighlightedRange: function(tokenTrackingController, element) { if (!this._popover) return true; if (!window.getSelection().isCollapsed && this._popover.element.contains(window.getSelection().anchorNode)) return false; return true; }, tokenTrackingControllerHighlightedRangeReleased: function(tokenTrackingController) { if (!this._mouseIsOverPopover) this._dismissPopover(); }, tokenTrackingControllerHighlightedRangeWasClicked: function(tokenTrackingController) { if (this.tokenTrackingController.mode !== WebInspector.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens) return; // Links are handled by TextEditor. if (/\blink\b/.test(this.tokenTrackingController.candidate.hoveredToken.type)) return; var sourceCodeLocation = this._sourceCodeLocationForEditorPosition(this.tokenTrackingController.candidate.hoveredTokenRange.start); if (this.sourceCode instanceof WebInspector.SourceMapResource) WebInspector.resourceSidebarPanel.showOriginalOrFormattedSourceCodeLocation(sourceCodeLocation); else WebInspector.resourceSidebarPanel.showSourceCodeLocation(sourceCodeLocation); }, tokenTrackingControllerNewHighlightCandidate: function(tokenTrackingController, candidate) { if (this.tokenTrackingController.mode === WebInspector.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens) { this.tokenTrackingController.highlightRange(candidate.hoveredTokenRange); return; } if (this.tokenTrackingController.mode === WebInspector.CodeMirrorTokenTrackingController.Mode.JavaScriptExpression) { this._tokenTrackingControllerHighlightedJavaScriptExpression(candidate); return; } if (this.tokenTrackingController.mode === WebInspector.CodeMirrorTokenTrackingController.Mode.MarkedTokens) { var markers = this.markersAtPosition(candidate.hoveredTokenRange.start); if (markers.length > 0) this._tokenTrackingControllerHighlightedMarkedExpression(candidate, markers); else this._dismissCodeMirrorColorEditingController(); } }, tokenTrackingControllerMouseOutOfHoveredMarker: function(tokenTrackingController, hoveredMarker) { this._dismissCodeMirrorColorEditingController(); }, _tokenTrackingControllerHighlightedJavaScriptExpression: function(candidate) { console.assert(candidate.expression); function populate(error, result, wasThrown) { if (error || wasThrown) return; if (candidate !== this.tokenTrackingController.candidate) return; var data = WebInspector.RemoteObject.fromPayload(result); switch (data.type) { case "function": this._showPopoverForFunction(data); break; case "object": this._showPopoverForObject(data); break; case "string": this._showPopoverForString(data); break; case "number": this._showPopoverForNumber(data); break; case "boolean": this._showPopoverForBoolean(data); break; case "undefined": this._showPopoverForUndefined(data); break; } } DebuggerAgent.evaluateOnCallFrame.invoke({callFrameId: WebInspector.debuggerManager.activeCallFrame.id, expression: candidate.expression, objectGroup: "popover", doNotPauseOnExceptionsAndMuteConsole: true}, populate.bind(this)); }, _showPopover: function(content) { console.assert(this.tokenTrackingController.candidate); var candidate = this.tokenTrackingController.candidate; if (!candidate) return; var bounds = this.boundsForRange(candidate.hoveredTokenRange); if (!bounds) return; content.classList.add(WebInspector.SourceCodeTextEditor.PopoverDebuggerContentStyleClassName); this._popover = this._popover || new WebInspector.Popover(this); this._popover.content = content; this._popover.present(bounds.pad(5), [WebInspector.RectEdge.MIN_Y, WebInspector.RectEdge.MAX_Y, WebInspector.RectEdge.MAX_X]); this._trackPopoverEvents(); this.tokenTrackingController.highlightRange(candidate.expressionRange); }, _showPopoverForFunction: function(data) { var 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; var wrapper = document.createElement("div"); wrapper.className = "body console-formatted-function"; wrapper.textContent = data.description; var content = document.createElement("div"); content.className = "function"; var title = content.appendChild(document.createElement("div")); title.className = "title"; title.textContent = response.name || response.inferredName || response.displayName || WebInspector.UIString("(anonymous function)"); content.appendChild(wrapper); this._showPopover(content); } DebuggerAgent.getFunctionDetails(data.objectId, didGetDetails.bind(this)); }, _showPopoverForObject: function(data) { if (data.subtype === "null") { this._showPopoverForNull(data); return; } var content = document.createElement("div"); content.className = "object expandable"; var titleElement = document.createElement("div"); titleElement.className = "title"; titleElement.textContent = data.description; content.appendChild(titleElement); var section = new WebInspector.ObjectPropertiesSection(data); section.expanded = true; section.element.classList.add("body"); content.appendChild(section.element); this._showPopover(content); }, _showPopoverForString: function(data) { var content = document.createElement("div"); content.className = "string console-formatted-string"; content.textContent = "\"" + data.description + "\""; this._showPopover(content); }, _showPopoverForNumber: function(data) { var content = document.createElement("span"); content.className = "number console-formatted-number"; content.textContent = data.description; this._showPopover(content); }, _showPopoverForBoolean: function(data) { var content = document.createElement("span"); content.className = "boolean console-formatted-boolean"; content.textContent = data.description; this._showPopover(content); }, _showPopoverForNull: function(data) { var content = document.createElement("span"); content.className = "boolean console-formatted-null"; content.textContent = data.description; this._showPopover(content); }, _showPopoverForUndefined: function(data) { var content = document.createElement("span"); content.className = "boolean console-formatted-undefined"; content.textContent = data.description; this._showPopover(content); }, willDismissPopover: function(popover) { this.tokenTrackingController.removeHighlightedRange(); RuntimeAgent.releaseObjectGroup("popover"); }, _dismissPopover: function() { if (!this._popover) return; this._popover.dismiss(); if (this._popoverEventHandler) this._popoverEventHandler.stopTrackingEvents(); }, _trackPopoverEvents: function() { if (!this._popoverEventHandler) { this._popoverEventHandler = new WebInspector.EventHandler(this, { "mouseover": this._popoverMouseover, "mouseout": this._popoverMouseout, }); } this._popoverEventHandler.trackEvents(this._popover.element); }, _popoverMouseover: function(event) { this._mouseIsOverPopover = true; }, _popoverMouseout: function(event) { this._mouseIsOverPopover = this._popover.element.contains(event.relatedTarget); }, _updateColorMarkers: function(lineNumber) { this.createColorMarkers(lineNumber); this._updateTokenTrackingControllerState(); }, _tokenTrackingControllerHighlightedMarkedExpression: function(candidate, markers) { var colorMarker; for (var marker of markers) { if (marker.type === WebInspector.TextMarker.Type.Color) { colorMarker = marker; break; } } if (!colorMarker) { this.tokenTrackingController.hoveredMarker = null; return; } if (this.tokenTrackingController.hoveredMarker === colorMarker) return; this._dismissCodeMirrorColorEditingController(); this.tokenTrackingController.hoveredMarker = colorMarker; this._colorEditingController = this.colorEditingControllerForMarker(colorMarker); var color = this._colorEditingController.color; if (!color || !color.valid) { colorMarker.clear(); delete this._colorEditingController; return; } this._colorEditingController.delegate = this; this._colorEditingController.presentHoverMenu(); }, _dismissCodeMirrorColorEditingController: function() { if (this._colorEditingController) this._colorEditingController.dismissHoverMenu(); this.tokenTrackingController.hoveredMarker = null; delete this._colorEditingController; }, // CodeMirrorColorEditingController Delegate colorEditingControllerDidStartEditing: function(colorEditingController) { // We can pause the token tracking controller during editing, it will be reset // to the expected state by calling _updateColorMarkers() in the // colorEditingControllerDidFinishEditing delegate. this.tokenTrackingController.enabled = false; // We clear the marker since we'll reset it after editing. colorEditingController.marker.clear(); // We ignore content changes made as a result of color editing. this._ignoreContentDidChange++; }, colorEditingControllerDidFinishEditing: function(colorEditingController) { this._updateColorMarkers(colorEditingController.range.startLine); this._ignoreContentDidChange--; delete this._colorEditingController; } }; WebInspector.SourceCodeTextEditor.prototype.__proto__ = WebInspector.TextEditor.prototype;