diff options
author | Lorry Tar Creator <lorry-tar-importer@lorry> | 2017-06-27 06:07:23 +0000 |
---|---|---|
committer | Lorry Tar Creator <lorry-tar-importer@lorry> | 2017-06-27 06:07:23 +0000 |
commit | 1bf1084f2b10c3b47fd1a588d85d21ed0eb41d0c (patch) | |
tree | 46dcd36c86e7fbc6e5df36deb463b33e9967a6f7 /Source/WebInspectorUI/UserInterface/Views/TextEditor.js | |
parent | 32761a6cee1d0dee366b885b7b9c777e67885688 (diff) | |
download | WebKitGtk-tarball-master.tar.gz |
webkitgtk-2.16.5HEADwebkitgtk-2.16.5master
Diffstat (limited to 'Source/WebInspectorUI/UserInterface/Views/TextEditor.js')
-rw-r--r-- | Source/WebInspectorUI/UserInterface/Views/TextEditor.js | 1663 |
1 files changed, 1663 insertions, 0 deletions
diff --git a/Source/WebInspectorUI/UserInterface/Views/TextEditor.js b/Source/WebInspectorUI/UserInterface/Views/TextEditor.js new file mode 100644 index 000000000..7754f32ef --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Views/TextEditor.js @@ -0,0 +1,1663 @@ +/* + * Copyright (C) 2013, 2015 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.TextEditor = class TextEditor extends WebInspector.View +{ + constructor(element, mimeType, delegate) + { + super(element); + + this.element.classList.add("text-editor", WebInspector.SyntaxHighlightedStyleClassName); + + this._codeMirror = WebInspector.CodeMirrorEditor.create(this.element, { + readOnly: true, + indentWithTabs: WebInspector.settings.indentWithTabs.value, + indentUnit: WebInspector.settings.indentUnit.value, + tabSize: WebInspector.settings.tabSize.value, + lineNumbers: true, + lineWrapping: WebInspector.settings.enableLineWrapping.value, + matchBrackets: true, + autoCloseBrackets: true, + showWhitespaceCharacters: WebInspector.settings.showWhitespaceCharacters.value, + styleSelectedText: true, + }); + + WebInspector.settings.indentWithTabs.addEventListener(WebInspector.Setting.Event.Changed, (event) => { + this._codeMirror.setOption("indentWithTabs", WebInspector.settings.indentWithTabs.value); + }); + + WebInspector.settings.indentUnit.addEventListener(WebInspector.Setting.Event.Changed, (event) => { + this._codeMirror.setOption("indentUnit", WebInspector.settings.indentUnit.value); + }); + + WebInspector.settings.tabSize.addEventListener(WebInspector.Setting.Event.Changed, (event) => { + this._codeMirror.setOption("tabSize", WebInspector.settings.tabSize.value); + }); + + WebInspector.settings.enableLineWrapping.addEventListener(WebInspector.Setting.Event.Changed, (event) => { + this._codeMirror.setOption("lineWrapping", WebInspector.settings.enableLineWrapping.value); + }); + + WebInspector.settings.showWhitespaceCharacters.addEventListener(WebInspector.Setting.Event.Changed, (event) => { + this._codeMirror.setOption("showWhitespaceCharacters", WebInspector.settings.showWhitespaceCharacters.value); + }); + + this._codeMirror.on("change", this._contentChanged.bind(this)); + this._codeMirror.on("gutterClick", this._gutterMouseDown.bind(this)); + this._codeMirror.on("gutterContextMenu", this._gutterContextMenu.bind(this)); + this._codeMirror.getScrollerElement().addEventListener("click", this._openClickedLinks.bind(this), true); + + this._completionController = new WebInspector.CodeMirrorCompletionController(this._codeMirror, this); + this._tokenTrackingController = new WebInspector.CodeMirrorTokenTrackingController(this._codeMirror, this); + + this._initialStringNotSet = true; + + this.mimeType = mimeType; + + this._breakpoints = {}; + this._executionLineNumber = NaN; + this._executionColumnNumber = NaN; + + this._executionLineHandle = null; + this._executionMultilineHandles = []; + this._executionRangeHighlightMarker = null; + + this._searchQuery = null; + this._searchResults = []; + this._currentSearchResultIndex = -1; + this._ignoreCodeMirrorContentDidChangeEvent = 0; + + this._formatted = false; + this._formattingPromise = null; + this._formatterSourceMap = null; + this._deferReveal = false; + + this._delegate = delegate || null; + } + + // Public + + get visible() + { + return this._visible; + } + + get string() + { + return this._codeMirror.getValue(); + } + + set string(newString) + { + function update() + { + // Clear any styles that may have been set on the empty line before content loaded. + if (this._initialStringNotSet) + this._codeMirror.removeLineClass(0, "wrap"); + + if (this._codeMirror.getValue() !== newString) { + this._codeMirror.setValue(newString); + console.assert(this.string.length === newString.length, "A lot of our code depends on precise text offsets, so the string should remain the same."); + } else { + // Ensure we at display content even if the value did not change. This often happens when auto formatting. + this.layout(); + } + + if (this._initialStringNotSet) { + this._codeMirror.clearHistory(); + this._codeMirror.markClean(); + this._initialStringNotSet = false; + } + + // Update the execution line now that we might have content for that line. + this._updateExecutionLine(); + this._updateExecutionRangeHighlight(); + + // Set the breakpoint styles now that we might have content for those lines. + for (var lineNumber in this._breakpoints) + this._setBreakpointStylesOnLine(lineNumber); + + // Try revealing the pending line now that we might have content with enough lines. + this._revealPendingPositionIfPossible(); + } + + this._ignoreCodeMirrorContentDidChangeEvent++; + this._codeMirror.operation(update.bind(this)); + this._ignoreCodeMirrorContentDidChangeEvent--; + console.assert(this._ignoreCodeMirrorContentDidChangeEvent >= 0); + } + + get readOnly() + { + return this._codeMirror.getOption("readOnly") || false; + } + + set readOnly(readOnly) + { + this._codeMirror.setOption("readOnly", readOnly); + } + + get formatted() + { + return this._formatted; + } + + get hasModified() + { + let historySize = this._codeMirror.historySize().undo; + + // Formatting code creates a history item. + if (this._formatted) + historySize--; + + return historySize > 0; + } + + updateFormattedState(formatted) + { + return this._format(formatted).catch(handlePromiseException); + } + + hasFormatter() + { + let mode = this._codeMirror.getMode().name; + return mode === "javascript" || mode === "css"; + } + + canBeFormatted() + { + // Can be overriden by subclasses. + return this.hasFormatter(); + } + + canShowTypeAnnotations() + { + return false; + } + + canShowCoverageHints() + { + return false; + } + + get selectedTextRange() + { + var start = this._codeMirror.getCursor(true); + var end = this._codeMirror.getCursor(false); + return this._textRangeFromCodeMirrorPosition(start, end); + } + + set selectedTextRange(textRange) + { + var position = this._codeMirrorPositionFromTextRange(textRange); + this._codeMirror.setSelection(position.start, position.end); + } + + get mimeType() + { + return this._mimeType; + } + + set mimeType(newMIMEType) + { + newMIMEType = parseMIMEType(newMIMEType).type; + + this._mimeType = newMIMEType; + this._codeMirror.setOption("mode", {name: newMIMEType, globalVars: true}); + } + + get executionLineNumber() + { + return this._executionLineNumber; + } + + get executionColumnNumber() + { + return this._executionColumnNumber; + } + + get formatterSourceMap() + { + return this._formatterSourceMap; + } + + get tokenTrackingController() + { + return this._tokenTrackingController; + } + + get delegate() + { + return this._delegate; + } + + set delegate(newDelegate) + { + this._delegate = newDelegate || null; + } + + get numberOfSearchResults() + { + return this._searchResults.length; + } + + get currentSearchQuery() + { + return this._searchQuery; + } + + set automaticallyRevealFirstSearchResult(reveal) + { + this._automaticallyRevealFirstSearchResult = reveal; + + // If we haven't shown a search result yet, reveal one now. + if (this._automaticallyRevealFirstSearchResult && this._searchResults.length > 0) { + if (this._currentSearchResultIndex === -1) + this._revealFirstSearchResultAfterCursor(); + } + } + + set deferReveal(defer) + { + this._deferReveal = defer; + } + + performSearch(query) + { + if (this._searchQuery === query) + return; + + this.searchCleared(); + + this._searchQuery = query; + + // Allow subclasses to handle the searching if they have a better way. + // If we are formatted, just use CodeMirror's search. + if (typeof this.customPerformSearch === "function" && !this.formatted) { + if (this.customPerformSearch(query)) + return; + } + + // Go down the slow patch for all other text content. + var queryRegex = new RegExp(query.escapeForRegExp(), "gi"); + var searchCursor = this._codeMirror.getSearchCursor(queryRegex, {line: 0, ch: 0}, false); + var boundBatchSearch = batchSearch.bind(this); + var numberOfSearchResultsDidChangeTimeout = null; + + function reportNumberOfSearchResultsDidChange() + { + if (numberOfSearchResultsDidChangeTimeout) { + clearTimeout(numberOfSearchResultsDidChangeTimeout); + numberOfSearchResultsDidChangeTimeout = null; + } + + this.dispatchEventToListeners(WebInspector.TextEditor.Event.NumberOfSearchResultsDidChange); + } + + function batchSearch() + { + // Bail if the query changed since we started. + if (this._searchQuery !== query) + return; + + var newSearchResults = []; + var foundResult = false; + for (var i = 0; i < WebInspector.TextEditor.NumberOfFindsPerSearchBatch && (foundResult = searchCursor.findNext()); ++i) { + var textRange = this._textRangeFromCodeMirrorPosition(searchCursor.from(), searchCursor.to()); + newSearchResults.push(textRange); + } + + this.addSearchResults(newSearchResults); + + // Don't report immediately, coalesce updates so they come in no faster than half a second. + if (!numberOfSearchResultsDidChangeTimeout) + numberOfSearchResultsDidChangeTimeout = setTimeout(reportNumberOfSearchResultsDidChange.bind(this), 500); + + if (foundResult) { + // More lines to search, set a timeout so we don't block the UI long. + setTimeout(boundBatchSearch, 50); + } else { + // Report immediately now that we are finished, canceling any pending update. + reportNumberOfSearchResultsDidChange.call(this); + } + } + + // Start the search. + boundBatchSearch(); + } + + setExecutionLineAndColumn(lineNumber, columnNumber) + { + // Only return early if there isn't a line handle and that isn't changing. + if (!this._executionLineHandle && isNaN(lineNumber)) + return; + + this._executionLineNumber = lineNumber; + this._executionColumnNumber = columnNumber; + + if (!this._initialStringNotSet) { + this._updateExecutionLine(); + this._updateExecutionRangeHighlight(); + } + + // Still dispatch the event even if the number didn't change. The execution state still + // could have changed (e.g. continuing in a loop with a breakpoint inside). + this.dispatchEventToListeners(WebInspector.TextEditor.Event.ExecutionLineNumberDidChange); + } + + addSearchResults(textRanges) + { + console.assert(textRanges); + if (!textRanges || !textRanges.length) + return; + + function markRanges() + { + for (var i = 0; i < textRanges.length; ++i) { + var position = this._codeMirrorPositionFromTextRange(textRanges[i]); + var mark = this._codeMirror.markText(position.start, position.end, {className: WebInspector.TextEditor.SearchResultStyleClassName}); + this._searchResults.push(mark); + } + + // If we haven't shown a search result yet, reveal one now. + if (this._automaticallyRevealFirstSearchResult) { + if (this._currentSearchResultIndex === -1) + this._revealFirstSearchResultAfterCursor(); + } + } + + this._codeMirror.operation(markRanges.bind(this)); + } + + searchCleared() + { + function clearResults() { + for (var i = 0; i < this._searchResults.length; ++i) + this._searchResults[i].clear(); + } + + this._codeMirror.operation(clearResults.bind(this)); + + this._searchQuery = null; + this._searchResults = []; + this._currentSearchResultIndex = -1; + } + + searchQueryWithSelection() + { + if (!this._codeMirror.somethingSelected()) + return null; + + return this._codeMirror.getSelection(); + } + + revealPreviousSearchResult(changeFocus) + { + if (!this._searchResults.length) + return; + + if (this._currentSearchResultIndex === -1 || this._cursorDoesNotMatchLastRevealedSearchResult()) { + this._revealFirstSearchResultBeforeCursor(changeFocus); + return; + } + + if (this._currentSearchResultIndex > 0) + --this._currentSearchResultIndex; + else + this._currentSearchResultIndex = this._searchResults.length - 1; + + this._revealSearchResult(this._searchResults[this._currentSearchResultIndex], changeFocus, -1); + } + + revealNextSearchResult(changeFocus) + { + if (!this._searchResults.length) + return; + + if (this._currentSearchResultIndex === -1 || this._cursorDoesNotMatchLastRevealedSearchResult()) { + this._revealFirstSearchResultAfterCursor(changeFocus); + return; + } + + if (this._currentSearchResultIndex + 1 < this._searchResults.length) + ++this._currentSearchResultIndex; + else + this._currentSearchResultIndex = 0; + + this._revealSearchResult(this._searchResults[this._currentSearchResultIndex], changeFocus, 1); + } + + line(lineNumber) + { + return this._codeMirror.getLine(lineNumber); + } + + getTextInRange(startPosition, endPosition) + { + return this._codeMirror.getRange(startPosition, endPosition); + } + + addStyleToTextRange(startPosition, endPosition, styleClassName) + { + endPosition.ch += 1; + return this._codeMirror.getDoc().markText(startPosition, endPosition, {className: styleClassName, inclusiveLeft: true, inclusiveRight: true}); + } + + revealPosition(position, textRangeToSelect, forceUnformatted, noHighlight) + { + console.assert(position === undefined || position instanceof WebInspector.SourceCodePosition, "revealPosition called without a SourceCodePosition"); + if (!(position instanceof WebInspector.SourceCodePosition)) + return; + + if (!this._visible || this._initialStringNotSet || this._deferReveal) { + // If we can't get a line handle or are not visible then we wait to do the reveal. + this._positionToReveal = position; + this._textRangeToSelect = textRangeToSelect; + this._forceUnformatted = forceUnformatted; + return; + } + + // Delete now that the reveal is happening. + delete this._positionToReveal; + delete this._textRangeToSelect; + delete this._forceUnformatted; + + // If we need to unformat, reveal the line after a wait. + // Otherwise the line highlight doesn't work properly. + if (this._formatted && forceUnformatted) { + this.updateFormattedState(false).then(() => { + setTimeout(this.revealPosition.bind(this), 0, position, textRangeToSelect); + }); + return; + } + + let line = Number.constrain(position.lineNumber, 0, this._codeMirror.lineCount() - 1); + let lineHandle = this._codeMirror.getLineHandle(line); + + if (!textRangeToSelect) { + let column = Number.constrain(position.columnNumber, 0, this._codeMirror.getLine(line).length - 1); + textRangeToSelect = new WebInspector.TextRange(line, column, line, column); + } + + function removeStyleClass() + { + this._codeMirror.removeLineClass(lineHandle, "wrap", WebInspector.TextEditor.HighlightedStyleClassName); + } + + function revealAndHighlightLine() + { + // If the line is not visible, reveal it as the center line in the editor. + var position = this._codeMirrorPositionFromTextRange(textRangeToSelect); + if (!this._isPositionVisible(position.start)) + this._scrollIntoViewCentered(position.start); + + this.selectedTextRange = textRangeToSelect; + + if (noHighlight) + return; + + // Avoid highlighting the execution line while debugging. + if (WebInspector.debuggerManager.paused && (!this._executionLineNumber || line === this._executionLineNumber)) + return; + + this._codeMirror.addLineClass(lineHandle, "wrap", WebInspector.TextEditor.HighlightedStyleClassName); + + // Use a timeout instead of a animationEnd event listener because the line element might + // be removed if the user scrolls during the animation. In that case animationEnd isn't + // fired, and the line would highlight again the next time it scrolls into view. + setTimeout(removeStyleClass.bind(this), WebInspector.TextEditor.HighlightAnimationDuration); + } + + this._codeMirror.operation(revealAndHighlightLine.bind(this)); + } + + shown() + { + this._visible = true; + + // Refresh since our size might have changed. + this._codeMirror.refresh(); + + // Try revealing the pending line now that we are visible. + // This needs to be done as a separate operation from the refresh + // so that the scrollInfo coordinates are correct. + this._revealPendingPositionIfPossible(); + } + + hidden() + { + this._visible = false; + } + + close() + { + WebInspector.settings.indentWithTabs.removeEventListener(null, null, this); + WebInspector.settings.indentUnit.removeEventListener(null, null, this); + WebInspector.settings.tabSize.removeEventListener(null, null, this); + WebInspector.settings.enableLineWrapping.removeEventListener(null, null, this); + WebInspector.settings.showWhitespaceCharacters.removeEventListener(null, null, this); + } + + setBreakpointInfoForLineAndColumn(lineNumber, columnNumber, breakpointInfo) + { + if (this._ignoreSetBreakpointInfoCalls) + return; + + if (breakpointInfo) + this._addBreakpointToLineAndColumnWithInfo(lineNumber, columnNumber, breakpointInfo); + else + this._removeBreakpointFromLineAndColumn(lineNumber, columnNumber); + } + + updateBreakpointLineAndColumn(oldLineNumber, oldColumnNumber, newLineNumber, newColumnNumber) + { + console.assert(this._breakpoints[oldLineNumber]); + if (!this._breakpoints[oldLineNumber]) + return; + + console.assert(this._breakpoints[oldLineNumber][oldColumnNumber]); + if (!this._breakpoints[oldLineNumber][oldColumnNumber]) + return; + + var breakpointInfo = this._breakpoints[oldLineNumber][oldColumnNumber]; + this._removeBreakpointFromLineAndColumn(oldLineNumber, oldColumnNumber); + this._addBreakpointToLineAndColumnWithInfo(newLineNumber, newColumnNumber, breakpointInfo); + } + + addStyleClassToLine(lineNumber, styleClassName) + { + var lineHandle = this._codeMirror.getLineHandle(lineNumber); + if (!lineHandle) + return null; + + return this._codeMirror.addLineClass(lineHandle, "wrap", styleClassName); + } + + removeStyleClassFromLine(lineNumber, styleClassName) + { + var lineHandle = this._codeMirror.getLineHandle(lineNumber); + console.assert(lineHandle); + if (!lineHandle) + return null; + + return this._codeMirror.removeLineClass(lineHandle, "wrap", styleClassName); + } + + toggleStyleClassForLine(lineNumber, styleClassName) + { + var lineHandle = this._codeMirror.getLineHandle(lineNumber); + console.assert(lineHandle); + if (!lineHandle) + return false; + + return this._codeMirror.toggleLineClass(lineHandle, "wrap", styleClassName); + } + + createWidgetForLine(lineNumber) + { + var lineHandle = this._codeMirror.getLineHandle(lineNumber); + if (!lineHandle) + return null; + + var widgetElement = document.createElement("div"); + var lineWidget = this._codeMirror.addLineWidget(lineHandle, widgetElement, {coverGutter: false, noHScroll: true}); + return new WebInspector.LineWidget(lineWidget, widgetElement); + } + + get lineCount() + { + return this._codeMirror.lineCount(); + } + + focus() + { + this._codeMirror.focus(); + } + + contentDidChange(replacedRanges, newRanges) + { + // Implemented by subclasses. + } + + rectsForRange(range) + { + return this._codeMirror.rectsForRange(range); + } + + get markers() + { + return this._codeMirror.getAllMarks().map(WebInspector.TextMarker.textMarkerForCodeMirrorTextMarker); + } + + markersAtPosition(position) + { + return this._codeMirror.findMarksAt(position).map(WebInspector.TextMarker.textMarkerForCodeMirrorTextMarker); + } + + createColorMarkers(range) + { + return createCodeMirrorColorTextMarkers(this._codeMirror, range); + } + + createGradientMarkers(range) + { + return createCodeMirrorGradientTextMarkers(this._codeMirror, range); + } + + createCubicBezierMarkers(range) + { + return createCodeMirrorCubicBezierTextMarkers(this._codeMirror, range); + } + + createSpringMarkers(range) + { + return createCodeMirrorSpringTextMarkers(this._codeMirror, range); + } + + editingControllerForMarker(editableMarker) + { + switch (editableMarker.type) { + case WebInspector.TextMarker.Type.Color: + return new WebInspector.CodeMirrorColorEditingController(this._codeMirror, editableMarker); + case WebInspector.TextMarker.Type.Gradient: + return new WebInspector.CodeMirrorGradientEditingController(this._codeMirror, editableMarker); + case WebInspector.TextMarker.Type.CubicBezier: + return new WebInspector.CodeMirrorBezierEditingController(this._codeMirror, editableMarker); + case WebInspector.TextMarker.Type.Spring: + return new WebInspector.CodeMirrorSpringEditingController(this._codeMirror, editableMarker); + default: + return new WebInspector.CodeMirrorEditingController(this._codeMirror, editableMarker); + } + } + + visibleRangeOffsets() + { + var startOffset = null; + var endOffset = null; + var visibleRange = this._codeMirror.getViewport(); + + if (this._formatterSourceMap) { + startOffset = this._formatterSourceMap.formattedToOriginalOffset(Math.max(visibleRange.from - 1, 0), 0); + endOffset = this._formatterSourceMap.formattedToOriginalOffset(visibleRange.to - 1, 0); + } else { + startOffset = this._codeMirror.getDoc().indexFromPos({line: visibleRange.from, ch: 0}); + endOffset = this._codeMirror.getDoc().indexFromPos({line: visibleRange.to, ch: 0}); + } + + return {startOffset, endOffset}; + } + + originalOffsetToCurrentPosition(offset) + { + var position = null; + if (this._formatterSourceMap) { + var location = this._formatterSourceMap.originalPositionToFormatted(offset); + position = {line: location.lineNumber, ch: location.columnNumber}; + } else + position = this._codeMirror.getDoc().posFromIndex(offset); + + return position; + } + + currentOffsetToCurrentPosition(offset) + { + return this._codeMirror.getDoc().posFromIndex(offset); + } + + currentPositionToOriginalOffset(position) + { + let offset = null; + + if (this._formatterSourceMap) + offset = this._formatterSourceMap.formattedToOriginalOffset(position.line, position.ch); + else + offset = this._codeMirror.getDoc().indexFromPos(position); + + return offset; + } + + currentPositionToOriginalPosition(position) + { + if (!this._formatterSourceMap) + return position; + + let location = this._formatterSourceMap.formattedToOriginal(position.line, position.ch); + return {line: location.lineNumber, ch: location.columnNumber}; + } + + currentPositionToCurrentOffset(position) + { + return this._codeMirror.getDoc().indexFromPos(position); + } + + setInlineWidget(position, inlineElement) + { + return this._codeMirror.setUniqueBookmark(position, {widget: inlineElement}); + } + + addScrollHandler(handler) + { + this._codeMirror.on("scroll", handler); + } + + removeScrollHandler(handler) + { + this._codeMirror.off("scroll", handler); + } + + // Protected + + layout() + { + // FIXME: <https://webkit.org/b/146256> Web Inspector: Nested ContentBrowsers / ContentViewContainers cause too many ContentView updates + // Ideally we would not get an updateLayout call if we are not visible. We should restructure ContentView + // show/hide restoration to reduce duplicated work and solve this in the process. + + // FIXME: visible check can be removed once <https://webkit.org/b/150741> is fixed. + if (this._visible) + this._codeMirror.refresh(); + } + + _format(formatted) + { + if (this._formatted === formatted) + return Promise.resolve(this._formatted); + + console.assert(!formatted || this.canBeFormatted()); + if (formatted && !this.canBeFormatted()) + return Promise.resolve(this._formatted); + + if (this._formattingPromise) + return this._formattingPromise; + + this._ignoreCodeMirrorContentDidChangeEvent++; + this._formattingPromise = this.prettyPrint(formatted).then(() => { + this._ignoreCodeMirrorContentDidChangeEvent--; + console.assert(this._ignoreCodeMirrorContentDidChangeEvent >= 0); + + this._formattingPromise = null; + + let originalFormatted = this._formatted; + this._formatted = !!this._formatterSourceMap; + + if (this._formatted !== originalFormatted) + this.dispatchEventToListeners(WebInspector.TextEditor.Event.FormattingDidChange); + + return this._formatted; + }); + + return this._formattingPromise; + } + + prettyPrint(pretty) + { + return new Promise((resolve, reject) => { + let beforePrettyPrintState = { + selectionAnchor: this._codeMirror.getCursor("anchor"), + selectionHead: this._codeMirror.getCursor("head"), + }; + + if (!pretty) + this._undoFormatting(beforePrettyPrintState, resolve); + else if (this._canUseFormatterWorker()) + this._startWorkerPrettyPrint(beforePrettyPrintState, resolve); + else + this._startCodeMirrorPrettyPrint(beforePrettyPrintState, resolve); + }); + } + + _canUseFormatterWorker() + { + return this._codeMirror.getMode().name === "javascript"; + } + + _startWorkerPrettyPrint(beforePrettyPrintState, callback) + { + let sourceText = this._codeMirror.getValue(); + let indentString = WebInspector.indentString(); + const includeSourceMapData = true; + + let sourceType = this._delegate.textEditorScriptSourceType(this); + const isModule = sourceType === WebInspector.Script.SourceType.Module; + + let workerProxy = WebInspector.FormatterWorkerProxy.singleton(); + workerProxy.formatJavaScript(sourceText, isModule, indentString, includeSourceMapData, ({formattedText, sourceMapData}) => { + // Handle if formatting failed, which is possible for invalid programs. + if (formattedText === null) { + callback(); + return; + } + this._finishPrettyPrint(beforePrettyPrintState, formattedText, sourceMapData, callback); + }); + } + + _startCodeMirrorPrettyPrint(beforePrettyPrintState, callback) + { + let indentString = WebInspector.indentString(); + let start = {line: 0, ch: 0}; + let end = {line: this._codeMirror.lineCount() - 1}; + let builder = new FormatterContentBuilder(indentString); + let formatter = new WebInspector.Formatter(this._codeMirror, builder); + formatter.format(start, end); + + let formattedText = builder.formattedContent; + let sourceMapData = builder.sourceMapData; + this._finishPrettyPrint(beforePrettyPrintState, formattedText, sourceMapData, callback); + } + + _finishPrettyPrint(beforePrettyPrintState, formattedText, sourceMapData, callback) + { + this._codeMirror.operation(() => { + this._formatterSourceMap = WebInspector.FormatterSourceMap.fromSourceMapData(sourceMapData); + this._codeMirror.setValue(formattedText); + this._updateAfterFormatting(true, beforePrettyPrintState); + }); + + callback(); + } + + _undoFormatting(beforePrettyPrintState, callback) + { + this._codeMirror.operation(() => { + this._codeMirror.undo(); + this._updateAfterFormatting(false, beforePrettyPrintState); + }); + + callback(); + } + + _updateAfterFormatting(pretty, beforePrettyPrintState) + { + let oldSelectionAnchor = beforePrettyPrintState.selectionAnchor; + let oldSelectionHead = beforePrettyPrintState.selectionHead; + let newSelectionAnchor, newSelectionHead; + let newExecutionLocation = null; + + if (pretty) { + if (this._positionToReveal) { + let newRevealPosition = this._formatterSourceMap.originalToFormatted(this._positionToReveal.lineNumber, this._positionToReveal.columnNumber); + this._positionToReveal = new WebInspector.SourceCodePosition(newRevealPosition.lineNumber, newRevealPosition.columnNumber); + } + + if (this._textRangeToSelect) { + let mappedRevealSelectionStart = this._formatterSourceMap.originalToFormatted(this._textRangeToSelect.startLine, this._textRangeToSelect.startColumn); + let mappedRevealSelectionEnd = this._formatterSourceMap.originalToFormatted(this._textRangeToSelect.endLine, this._textRangeToSelect.endColumn); + this._textRangeToSelect = new WebInspector.TextRange(mappedRevealSelectionStart.lineNumber, mappedRevealSelectionStart.columnNumber, mappedRevealSelectionEnd.lineNumber, mappedRevealSelectionEnd.columnNumber); + } + + if (!isNaN(this._executionLineNumber)) { + console.assert(!isNaN(this._executionColumnNumber)); + newExecutionLocation = this._formatterSourceMap.originalToFormatted(this._executionLineNumber, this._executionColumnNumber); + } + + let mappedAnchorLocation = this._formatterSourceMap.originalToFormatted(oldSelectionAnchor.line, oldSelectionAnchor.ch); + let mappedHeadLocation = this._formatterSourceMap.originalToFormatted(oldSelectionHead.line, oldSelectionHead.ch); + newSelectionAnchor = {line: mappedAnchorLocation.lineNumber, ch: mappedAnchorLocation.columnNumber}; + newSelectionHead = {line: mappedHeadLocation.lineNumber, ch: mappedHeadLocation.columnNumber}; + } else { + if (this._positionToReveal) { + let newRevealPosition = this._formatterSourceMap.formattedToOriginal(this._positionToReveal.lineNumber, this._positionToReveal.columnNumber); + this._positionToReveal = new WebInspector.SourceCodePosition(newRevealPosition.lineNumber, newRevealPosition.columnNumber); + } + + if (this._textRangeToSelect) { + let mappedRevealSelectionStart = this._formatterSourceMap.formattedToOriginal(this._textRangeToSelect.startLine, this._textRangeToSelect.startColumn); + let mappedRevealSelectionEnd = this._formatterSourceMap.formattedToOriginal(this._textRangeToSelect.endLine, this._textRangeToSelect.endColumn); + this._textRangeToSelect = new WebInspector.TextRange(mappedRevealSelectionStart.lineNumber, mappedRevealSelectionStart.columnNumber, mappedRevealSelectionEnd.lineNumber, mappedRevealSelectionEnd.columnNumber); + } + + if (!isNaN(this._executionLineNumber)) { + console.assert(!isNaN(this._executionColumnNumber)); + newExecutionLocation = this._formatterSourceMap.formattedToOriginal(this._executionLineNumber, this._executionColumnNumber); + } + + let mappedAnchorLocation = this._formatterSourceMap.formattedToOriginal(oldSelectionAnchor.line, oldSelectionAnchor.ch); + let mappedHeadLocation = this._formatterSourceMap.formattedToOriginal(oldSelectionHead.line, oldSelectionHead.ch); + newSelectionAnchor = {line: mappedAnchorLocation.lineNumber, ch: mappedAnchorLocation.columnNumber}; + newSelectionHead = {line: mappedHeadLocation.lineNumber, ch: mappedHeadLocation.columnNumber}; + + this._formatterSourceMap = null; + } + + this._scrollIntoViewCentered(newSelectionAnchor); + this._codeMirror.setSelection(newSelectionAnchor, newSelectionHead); + + if (newExecutionLocation) { + this._executionLineHandle = null; + this._executionMultilineHandles = []; + this.setExecutionLineAndColumn(newExecutionLocation.lineNumber, newExecutionLocation.columnNumber); + } + + // FIXME: <rdar://problem/13129955> FindBanner: New searches should not lose search position (start from current selection/caret) + if (this.currentSearchQuery) { + let searchQuery = this.currentSearchQuery; + this.searchCleared(); + // Set timeout so that this happens after the current CodeMirror operation. + // The editor has to update for the value and selection changes. + setTimeout(() => { this.performSearch(searchQuery); }, 0); + } + + if (this._delegate && typeof this._delegate.textEditorUpdatedFormatting === "function") + this._delegate.textEditorUpdatedFormatting(this); + } + + // Private + + hasEdits() + { + return !this._codeMirror.isClean(); + } + + _contentChanged(codeMirror, change) + { + if (this._ignoreCodeMirrorContentDidChangeEvent > 0) + return; + + var replacedRanges = []; + var newRanges = []; + while (change) { + replacedRanges.push(new WebInspector.TextRange( + change.from.line, + change.from.ch, + change.to.line, + change.to.ch + )); + newRanges.push(new WebInspector.TextRange( + change.from.line, + change.from.ch, + change.from.line + change.text.length - 1, + change.text.length === 1 ? change.from.ch + change.text[0].length : change.text.lastValue.length + )); + change = change.next; + } + this.contentDidChange(replacedRanges, newRanges); + + if (this._formatted) { + this._formatterSourceMap = null; + this._formatted = false; + + if (this._delegate && typeof this._delegate.textEditorUpdatedFormatting === "function") + this._delegate.textEditorUpdatedFormatting(this); + + this.dispatchEventToListeners(WebInspector.TextEditor.Event.FormattingDidChange); + } + + this.dispatchEventToListeners(WebInspector.TextEditor.Event.ContentDidChange); + } + + _textRangeFromCodeMirrorPosition(start, end) + { + console.assert(start); + console.assert(end); + + return new WebInspector.TextRange(start.line, start.ch, end.line, end.ch); + } + + _codeMirrorPositionFromTextRange(textRange) + { + console.assert(textRange); + + var start = {line: textRange.startLine, ch: textRange.startColumn}; + var end = {line: textRange.endLine, ch: textRange.endColumn}; + return {start, end}; + } + + _revealPendingPositionIfPossible() + { + // Nothing to do if we don't have a pending position. + if (!this._positionToReveal) + return; + + // Don't try to reveal unless we are visible. + if (!this._visible) + return; + + this.revealPosition(this._positionToReveal, this._textRangeToSelect, this._forceUnformatted); + } + + _revealSearchResult(result, changeFocus, directionInCaseOfRevalidation) + { + var position = result.find(); + + // Check for a valid position, it might have been removed from editing by the user. + // If the position is invalide, revalidate all positions reveal as needed. + if (!position) { + this._revalidateSearchResults(directionInCaseOfRevalidation); + return; + } + + // If the line is not visible, reveal it as the center line in the editor. + if (!this._isPositionVisible(position.from)) + this._scrollIntoViewCentered(position.from); + + // Update the text selection to select the search result. + this.selectedTextRange = this._textRangeFromCodeMirrorPosition(position.from, position.to); + + // Remove the automatically reveal state now that we have revealed a search result. + this._automaticallyRevealFirstSearchResult = false; + + // Focus the editor if requested. + if (changeFocus) + this._codeMirror.focus(); + + // Remove the bouncy highlight if it is still around. The animation will not + // start unless we remove it and add it back to the document. + if (this._bouncyHighlightElement) + this._bouncyHighlightElement.remove(); + + // Create the bouncy highlight. + this._bouncyHighlightElement = document.createElement("div"); + this._bouncyHighlightElement.className = WebInspector.TextEditor.BouncyHighlightStyleClassName; + + // Collect info for the bouncy highlight. + var textContent = this._codeMirror.getSelection(); + var coordinates = this._codeMirror.cursorCoords(true, "page"); + + // Adjust the coordinates to be based in the text editor's space. + let textEditorRect = this.element.getBoundingClientRect(); + coordinates.top -= textEditorRect.top; + coordinates.left -= textEditorRect.left; + + // Position and show the bouncy highlight. + this._bouncyHighlightElement.textContent = textContent; + this._bouncyHighlightElement.style.top = coordinates.top + "px"; + this._bouncyHighlightElement.style.left = coordinates.left + "px"; + this.element.appendChild(this._bouncyHighlightElement); + + let scrollHandler = () => { + if (this._bouncyHighlightElement) + this._bouncyHighlightElement.remove(); + }; + + this.addScrollHandler(scrollHandler); + + function animationEnded() + { + if (!this._bouncyHighlightElement) + return; + + this._bouncyHighlightElement.remove(); + delete this._bouncyHighlightElement; + + this.removeScrollHandler(scrollHandler); + } + + // Listen for the end of the animation so we can remove the element. + this._bouncyHighlightElement.addEventListener("animationend", animationEnded.bind(this)); + } + + _binarySearchInsertionIndexInSearchResults(object, comparator) + { + // It is possible that markers in the search results array may have been deleted. + // In those cases the comparator will return "null" and we immediately stop + // the binary search and return null. The search results list needs to be updated. + var array = this._searchResults; + + var first = 0; + var last = array.length - 1; + + while (first <= last) { + var mid = (first + last) >> 1; + var c = comparator(object, array[mid]); + if (c === null) + return null; + if (c > 0) + first = mid + 1; + else if (c < 0) + last = mid - 1; + else + return mid; + } + + return first - 1; + } + + _revealFirstSearchResultBeforeCursor(changeFocus) + { + console.assert(this._searchResults.length); + + var currentCursorPosition = this._codeMirror.getCursor("start"); + if (currentCursorPosition.line === 0 && currentCursorPosition.ch === 0) { + this._currentSearchResultIndex = this._searchResults.length - 1; + this._revealSearchResult(this._searchResults[this._currentSearchResultIndex], changeFocus, -1); + return; + } + + var index = this._binarySearchInsertionIndexInSearchResults(currentCursorPosition, function(current, searchResult) { + var searchResultMarker = searchResult.find(); + if (!searchResultMarker) + return null; + return WebInspector.compareCodeMirrorPositions(current, searchResultMarker.from); + }); + + if (index === null) { + this._revalidateSearchResults(-1); + return; + } + + this._currentSearchResultIndex = index; + this._revealSearchResult(this._searchResults[this._currentSearchResultIndex], changeFocus); + } + + _revealFirstSearchResultAfterCursor(changeFocus) + { + console.assert(this._searchResults.length); + + var currentCursorPosition = this._codeMirror.getCursor("start"); + if (currentCursorPosition.line === 0 && currentCursorPosition.ch === 0) { + this._currentSearchResultIndex = 0; + this._revealSearchResult(this._searchResults[this._currentSearchResultIndex], changeFocus, 1); + return; + } + + var index = this._binarySearchInsertionIndexInSearchResults(currentCursorPosition, function(current, searchResult) { + var searchResultMarker = searchResult.find(); + if (!searchResultMarker) + return null; + return WebInspector.compareCodeMirrorPositions(current, searchResultMarker.from); + }); + + if (index === null) { + this._revalidateSearchResults(1); + return; + } + + if (index + 1 < this._searchResults.length) + ++index; + else + index = 0; + + this._currentSearchResultIndex = index; + this._revealSearchResult(this._searchResults[this._currentSearchResultIndex], changeFocus); + } + + _cursorDoesNotMatchLastRevealedSearchResult() + { + console.assert(this._currentSearchResultIndex !== -1); + console.assert(this._searchResults.length); + + var lastRevealedSearchResultMarker = this._searchResults[this._currentSearchResultIndex].find(); + if (!lastRevealedSearchResultMarker) + return true; + + var currentCursorPosition = this._codeMirror.getCursor("start"); + var lastRevealedSearchResultPosition = lastRevealedSearchResultMarker.from; + + return WebInspector.compareCodeMirrorPositions(currentCursorPosition, lastRevealedSearchResultPosition) !== 0; + } + + _revalidateSearchResults(direction) + { + console.assert(direction !== undefined); + + this._currentSearchResultIndex = -1; + + var updatedSearchResults = []; + for (var i = 0; i < this._searchResults.length; ++i) { + if (this._searchResults[i].find()) + updatedSearchResults.push(this._searchResults[i]); + } + + console.assert(updatedSearchResults.length !== this._searchResults.length); + + this._searchResults = updatedSearchResults; + this.dispatchEventToListeners(WebInspector.TextEditor.Event.NumberOfSearchResultsDidChange); + + if (this._searchResults.length) { + if (direction > 0) + this._revealFirstSearchResultAfterCursor(); + else + this._revealFirstSearchResultBeforeCursor(); + } + } + + _clearMultilineExecutionLineHighlights() + { + if (this._executionMultilineHandles.length) { + for (let lineHandle of this._executionMultilineHandles) + this._codeMirror.removeLineClass(lineHandle, "wrap", WebInspector.TextEditor.ExecutionLineStyleClassName); + this._executionMultilineHandles = []; + } + } + + _updateExecutionLine() + { + this._codeMirror.operation(() => { + if (this._executionLineHandle) { + this._codeMirror.removeLineClass(this._executionLineHandle, "wrap", WebInspector.TextEditor.ExecutionLineStyleClassName); + this._codeMirror.removeLineClass(this._executionLineHandle, "wrap", "primary"); + } + + this._clearMultilineExecutionLineHighlights(); + + this._executionLineHandle = !isNaN(this._executionLineNumber) ? this._codeMirror.getLineHandle(this._executionLineNumber) : null; + + if (this._executionLineHandle) { + this._codeMirror.addLineClass(this._executionLineHandle, "wrap", WebInspector.TextEditor.ExecutionLineStyleClassName); + this._codeMirror.addLineClass(this._executionLineHandle, "wrap", "primary"); + this._codeMirror.removeLineClass(this._executionLineHandle, "wrap", WebInspector.TextEditor.HighlightedStyleClassName); + } + }); + } + + _updateExecutionRangeHighlight() + { + if (this._executionRangeHighlightMarker) { + this._executionRangeHighlightMarker.clear(); + this._executionRangeHighlightMarker = null; + } + + if (isNaN(this._executionLineNumber)) + return; + + let currentPosition = {line: this._executionLineNumber, ch: this._executionColumnNumber}; + let originalOffset = this.currentPositionToOriginalOffset(currentPosition); + let originalCodeMirrorPosition = this.currentPositionToOriginalPosition(currentPosition); + let originalPosition = new WebInspector.SourceCodePosition(originalCodeMirrorPosition.line, originalCodeMirrorPosition.ch); + let characterAtOffset = this._codeMirror.getRange(currentPosition, {line: this._executionLineNumber, ch: this._executionColumnNumber + 1}); + + this._delegate.textEditorExecutionHighlightRange(originalOffset, originalPosition, characterAtOffset, (range) => { + let start, end; + if (!range) { + // Highlight the rest of the line. + start = {line: this._executionLineNumber, ch: this._executionColumnNumber}; + end = {line: this._executionLineNumber}; + } else { + // Highlight the range. + start = this.originalOffsetToCurrentPosition(range[0]); + end = this.originalOffsetToCurrentPosition(range[1]); + } + + // Ensure the marker is cleared in case there were multiple updates very quickly. + if (this._executionRangeHighlightMarker) { + this._executionRangeHighlightMarker.clear(); + this._executionRangeHighlightMarker = null; + } + + // Avoid highlighting trailing whitespace. + let text = this._codeMirror.getRange(start, end); + let trailingWhitespace = text.match(/\s+$/); + if (trailingWhitespace) + end.ch = Math.max(0, end.ch - trailingWhitespace[0].length); + + // Give each line containing part of the range the full line style. + this._clearMultilineExecutionLineHighlights(); + if (start.line !== end.line) { + for (let line = start.line; line < end.line; ++line) { + let lineHandle = this._codeMirror.getLineHandle(line); + this._codeMirror.addLineClass(lineHandle, "wrap", WebInspector.TextEditor.ExecutionLineStyleClassName); + this._executionMultilineHandles.push(lineHandle); + } + } + + this._executionRangeHighlightMarker = this._codeMirror.markText(start, end, {className: "execution-range-highlight"}); + }); + } + + _setBreakpointStylesOnLine(lineNumber) + { + var columnBreakpoints = this._breakpoints[lineNumber]; + console.assert(columnBreakpoints); + if (!columnBreakpoints) + return; + + var allDisabled = true; + var allResolved = true; + var allAutoContinue = true; + var multiple = Object.keys(columnBreakpoints).length > 1; + for (var columnNumber in columnBreakpoints) { + var breakpointInfo = columnBreakpoints[columnNumber]; + if (!breakpointInfo.disabled) + allDisabled = false; + if (!breakpointInfo.resolved) + allResolved = false; + if (!breakpointInfo.autoContinue) + allAutoContinue = false; + } + + allResolved = allResolved && WebInspector.debuggerManager.breakpointsEnabled; + + function updateStyles() + { + // We might not have a line if the content isn't fully populated yet. + // This will be called again when the content is available. + var lineHandle = this._codeMirror.getLineHandle(lineNumber); + if (!lineHandle) + return; + + this._codeMirror.addLineClass(lineHandle, "wrap", WebInspector.TextEditor.HasBreakpointStyleClassName); + + if (allResolved) + this._codeMirror.addLineClass(lineHandle, "wrap", WebInspector.TextEditor.BreakpointResolvedStyleClassName); + else + this._codeMirror.removeLineClass(lineHandle, "wrap", WebInspector.TextEditor.BreakpointResolvedStyleClassName); + + if (allDisabled) + this._codeMirror.addLineClass(lineHandle, "wrap", WebInspector.TextEditor.BreakpointDisabledStyleClassName); + else + this._codeMirror.removeLineClass(lineHandle, "wrap", WebInspector.TextEditor.BreakpointDisabledStyleClassName); + + if (allAutoContinue) + this._codeMirror.addLineClass(lineHandle, "wrap", WebInspector.TextEditor.BreakpointAutoContinueStyleClassName); + else + this._codeMirror.removeLineClass(lineHandle, "wrap", WebInspector.TextEditor.BreakpointAutoContinueStyleClassName); + + if (multiple) + this._codeMirror.addLineClass(lineHandle, "wrap", WebInspector.TextEditor.MultipleBreakpointsStyleClassName); + else + this._codeMirror.removeLineClass(lineHandle, "wrap", WebInspector.TextEditor.MultipleBreakpointsStyleClassName); + } + + this._codeMirror.operation(updateStyles.bind(this)); + } + + _addBreakpointToLineAndColumnWithInfo(lineNumber, columnNumber, breakpointInfo) + { + if (!this._breakpoints[lineNumber]) + this._breakpoints[lineNumber] = {}; + this._breakpoints[lineNumber][columnNumber] = breakpointInfo; + + this._setBreakpointStylesOnLine(lineNumber); + } + + _removeBreakpointFromLineAndColumn(lineNumber, columnNumber) + { + console.assert(columnNumber in this._breakpoints[lineNumber]); + delete this._breakpoints[lineNumber][columnNumber]; + + // There are still breakpoints on the line. Update the breakpoint style. + if (!isEmptyObject(this._breakpoints[lineNumber])) { + this._setBreakpointStylesOnLine(lineNumber); + return; + } + + delete this._breakpoints[lineNumber]; + + function updateStyles() + { + var lineHandle = this._codeMirror.getLineHandle(lineNumber); + if (!lineHandle) + return; + + this._codeMirror.removeLineClass(lineHandle, "wrap", WebInspector.TextEditor.HasBreakpointStyleClassName); + this._codeMirror.removeLineClass(lineHandle, "wrap", WebInspector.TextEditor.BreakpointResolvedStyleClassName); + this._codeMirror.removeLineClass(lineHandle, "wrap", WebInspector.TextEditor.BreakpointDisabledStyleClassName); + this._codeMirror.removeLineClass(lineHandle, "wrap", WebInspector.TextEditor.BreakpointAutoContinueStyleClassName); + this._codeMirror.removeLineClass(lineHandle, "wrap", WebInspector.TextEditor.MultipleBreakpointsStyleClassName); + } + + this._codeMirror.operation(updateStyles.bind(this)); + } + + _allColumnBreakpointInfoForLine(lineNumber) + { + return this._breakpoints[lineNumber]; + } + + _setColumnBreakpointInfoForLine(lineNumber, columnBreakpointInfo) + { + console.assert(columnBreakpointInfo); + this._breakpoints[lineNumber] = columnBreakpointInfo; + this._setBreakpointStylesOnLine(lineNumber); + } + + _gutterMouseDown(codeMirror, lineNumber, gutterElement, event) + { + if (event.button !== 0 || event.ctrlKey) + return; + + if (!this._codeMirror.hasLineClass(lineNumber, "wrap", WebInspector.TextEditor.HasBreakpointStyleClassName)) { + console.assert(!(lineNumber in this._breakpoints)); + + // No breakpoint, add a new one. + if (this._delegate && typeof this._delegate.textEditorBreakpointAdded === "function") { + var data = this._delegate.textEditorBreakpointAdded(this, lineNumber, 0); + if (data) { + var breakpointInfo = data.breakpointInfo; + if (breakpointInfo) + this._addBreakpointToLineAndColumnWithInfo(data.lineNumber, data.columnNumber, breakpointInfo); + } + } + + return; + } + + console.assert(lineNumber in this._breakpoints); + + if (this._codeMirror.hasLineClass(lineNumber, "wrap", WebInspector.TextEditor.MultipleBreakpointsStyleClassName)) { + console.assert(!isEmptyObject(this._breakpoints[lineNumber])); + return; + } + + // Single existing breakpoint, start tracking it for dragging. + console.assert(Object.keys(this._breakpoints[lineNumber]).length === 1); + var columnNumber = Object.keys(this._breakpoints[lineNumber])[0]; + this._draggingBreakpointInfo = this._breakpoints[lineNumber][columnNumber]; + this._lineNumberWithMousedDownBreakpoint = lineNumber; + this._lineNumberWithDraggedBreakpoint = lineNumber; + this._columnNumberWithMousedDownBreakpoint = columnNumber; + this._columnNumberWithDraggedBreakpoint = columnNumber; + + this._documentMouseMovedEventListener = this._documentMouseMoved.bind(this); + this._documentMouseUpEventListener = this._documentMouseUp.bind(this); + + // Register these listeners on the document so we can track the mouse if it leaves the gutter. + document.addEventListener("mousemove", this._documentMouseMovedEventListener, true); + document.addEventListener("mouseup", this._documentMouseUpEventListener, true); + } + + _gutterContextMenu(codeMirror, lineNumber, gutterElement, event) + { + if (this._delegate && typeof this._delegate.textEditorGutterContextMenu === "function") { + var breakpoints = []; + for (var columnNumber in this._breakpoints[lineNumber]) + breakpoints.push({lineNumber, columnNumber}); + + this._delegate.textEditorGutterContextMenu(this, lineNumber, 0, breakpoints, event); + } + } + + _documentMouseMoved(event) + { + console.assert("_lineNumberWithMousedDownBreakpoint" in this); + if (!("_lineNumberWithMousedDownBreakpoint" in this)) + return; + + event.preventDefault(); + + var lineNumber; + var position = this._codeMirror.coordsChar({left: event.pageX, top: event.pageY}); + + // CodeMirror's coordsChar returns a position even if it is outside the bounds. Nullify the position + // if the event is outside the bounds of the gutter so we will remove the breakpoint. + var gutterBounds = this._codeMirror.getGutterElement().getBoundingClientRect(); + if (event.pageX < gutterBounds.left || event.pageX > gutterBounds.right || event.pageY < gutterBounds.top || event.pageY > gutterBounds.bottom) + position = null; + + // If we have a position and it has a line then use it. + if (position && "line" in position) + lineNumber = position.line; + + // The _lineNumberWithDraggedBreakpoint property can be undefined if the user drags + // outside of the gutter. The lineNumber variable can be undefined for the same reason. + + if (lineNumber === this._lineNumberWithDraggedBreakpoint) + return; + + // Record that the mouse dragged some so when mouse up fires we know to do the + // work of removing and moving the breakpoint. + this._mouseDragged = true; + + if ("_lineNumberWithDraggedBreakpoint" in this) { + // We have a line that is currently showing the dragged breakpoint. Remove that breakpoint + // and restore the previous one (if any.) + if (this._previousColumnBreakpointInfo) + this._setColumnBreakpointInfoForLine(this._lineNumberWithDraggedBreakpoint, this._previousColumnBreakpointInfo); + else + this._removeBreakpointFromLineAndColumn(this._lineNumberWithDraggedBreakpoint, this._columnNumberWithDraggedBreakpoint); + + delete this._previousColumnBreakpointInfo; + delete this._lineNumberWithDraggedBreakpoint; + delete this._columnNumberWithDraggedBreakpoint; + } + + if (lineNumber !== undefined) { + // We have a new line that will now show the dragged breakpoint. + var newColumnBreakpoints = {}; + var columnNumber = (lineNumber === this._lineNumberWithMousedDownBreakpoint ? this._columnNumberWithDraggedBreakpoint : 0); + newColumnBreakpoints[columnNumber] = this._draggingBreakpointInfo; + this._previousColumnBreakpointInfo = this._allColumnBreakpointInfoForLine(lineNumber); + this._setColumnBreakpointInfoForLine(lineNumber, newColumnBreakpoints); + this._lineNumberWithDraggedBreakpoint = lineNumber; + this._columnNumberWithDraggedBreakpoint = columnNumber; + } + } + + _documentMouseUp(event) + { + console.assert("_lineNumberWithMousedDownBreakpoint" in this); + if (!("_lineNumberWithMousedDownBreakpoint" in this)) + return; + + event.preventDefault(); + + document.removeEventListener("mousemove", this._documentMouseMovedEventListener, true); + document.removeEventListener("mouseup", this._documentMouseUpEventListener, true); + + var delegateImplementsBreakpointClicked = this._delegate && typeof this._delegate.textEditorBreakpointClicked === "function"; + var delegateImplementsBreakpointRemoved = this._delegate && typeof this._delegate.textEditorBreakpointRemoved === "function"; + var delegateImplementsBreakpointMoved = this._delegate && typeof this._delegate.textEditorBreakpointMoved === "function"; + + if (this._mouseDragged) { + if (!("_lineNumberWithDraggedBreakpoint" in this)) { + // The breakpoint was dragged off the gutter, remove it. + if (delegateImplementsBreakpointRemoved) { + this._ignoreSetBreakpointInfoCalls = true; + this._delegate.textEditorBreakpointRemoved(this, this._lineNumberWithMousedDownBreakpoint, this._columnNumberWithMousedDownBreakpoint); + delete this._ignoreSetBreakpointInfoCalls; + } + } else if (this._lineNumberWithMousedDownBreakpoint !== this._lineNumberWithDraggedBreakpoint) { + // The dragged breakpoint was moved to a new line. + + // If there is are breakpoints already at the drop line, tell the delegate to remove them. + // We have already updated the breakpoint info internally, so when the delegate removes the breakpoints + // and tells us to clear the breakpoint info, we can ignore those calls. + if (this._previousColumnBreakpointInfo && delegateImplementsBreakpointRemoved) { + this._ignoreSetBreakpointInfoCalls = true; + for (var columnNumber in this._previousColumnBreakpointInfo) + this._delegate.textEditorBreakpointRemoved(this, this._lineNumberWithDraggedBreakpoint, columnNumber); + delete this._ignoreSetBreakpointInfoCalls; + } + + // Tell the delegate to move the breakpoint from one line to another. + if (delegateImplementsBreakpointMoved) { + this._ignoreSetBreakpointInfoCalls = true; + this._delegate.textEditorBreakpointMoved(this, this._lineNumberWithMousedDownBreakpoint, this._columnNumberWithMousedDownBreakpoint, this._lineNumberWithDraggedBreakpoint, this._columnNumberWithDraggedBreakpoint); + delete this._ignoreSetBreakpointInfoCalls; + } + } + } else { + // Toggle the disabled state of the breakpoint. + console.assert(this._lineNumberWithMousedDownBreakpoint in this._breakpoints); + console.assert(this._columnNumberWithMousedDownBreakpoint in this._breakpoints[this._lineNumberWithMousedDownBreakpoint]); + if (this._lineNumberWithMousedDownBreakpoint in this._breakpoints && this._columnNumberWithMousedDownBreakpoint in this._breakpoints[this._lineNumberWithMousedDownBreakpoint] && delegateImplementsBreakpointClicked) + this._delegate.textEditorBreakpointClicked(this, this._lineNumberWithMousedDownBreakpoint, this._columnNumberWithMousedDownBreakpoint); + } + + delete this._documentMouseMovedEventListener; + delete this._documentMouseUpEventListener; + delete this._lineNumberWithMousedDownBreakpoint; + delete this._lineNumberWithDraggedBreakpoint; + delete this._columnNumberWithMousedDownBreakpoint; + delete this._columnNumberWithDraggedBreakpoint; + delete this._previousColumnBreakpointInfo; + delete this._mouseDragged; + } + + _openClickedLinks(event) + { + // Get the position in the text and the token at that position. + var position = this._codeMirror.coordsChar({left: event.pageX, top: event.pageY}); + var tokenInfo = this._codeMirror.getTokenAt(position); + if (!tokenInfo || !tokenInfo.type || !tokenInfo.string) + return; + + // If the token is not a link, then ignore it. + if (!/\blink\b/.test(tokenInfo.type)) + return; + + // The token string is the URL we should open. It might be a relative URL. + var url = tokenInfo.string; + + // Get the base URL. + var baseURL = ""; + if (this._delegate && typeof this._delegate.textEditorBaseURL === "function") + baseURL = this._delegate.textEditorBaseURL(this); + + // Open the link after resolving the absolute URL from the base URL. + WebInspector.openURL(absoluteURL(url, baseURL)); + + // Stop processing the event. + event.preventDefault(); + event.stopPropagation(); + } + + _isPositionVisible(position) + { + var scrollInfo = this._codeMirror.getScrollInfo(); + var visibleRangeStart = scrollInfo.top; + var visibleRangeEnd = visibleRangeStart + scrollInfo.clientHeight; + var coords = this._codeMirror.charCoords(position, "local"); + + return coords.top >= visibleRangeStart && coords.bottom <= visibleRangeEnd; + } + + _scrollIntoViewCentered(position) + { + var scrollInfo = this._codeMirror.getScrollInfo(); + var lineHeight = Math.ceil(this._codeMirror.defaultTextHeight()); + var margin = Math.floor((scrollInfo.clientHeight - lineHeight) / 2); + this._codeMirror.scrollIntoView(position, margin); + } +}; + +WebInspector.TextEditor.HighlightedStyleClassName = "highlighted"; +WebInspector.TextEditor.SearchResultStyleClassName = "search-result"; +WebInspector.TextEditor.HasBreakpointStyleClassName = "has-breakpoint"; +WebInspector.TextEditor.BreakpointResolvedStyleClassName = "breakpoint-resolved"; +WebInspector.TextEditor.BreakpointAutoContinueStyleClassName = "breakpoint-auto-continue"; +WebInspector.TextEditor.BreakpointDisabledStyleClassName = "breakpoint-disabled"; +WebInspector.TextEditor.MultipleBreakpointsStyleClassName = "multiple-breakpoints"; +WebInspector.TextEditor.ExecutionLineStyleClassName = "execution-line"; +WebInspector.TextEditor.BouncyHighlightStyleClassName = "bouncy-highlight"; +WebInspector.TextEditor.NumberOfFindsPerSearchBatch = 10; +WebInspector.TextEditor.HighlightAnimationDuration = 2000; + +WebInspector.TextEditor.Event = { + ExecutionLineNumberDidChange: "text-editor-execution-line-number-did-change", + NumberOfSearchResultsDidChange: "text-editor-number-of-search-results-did-change", + ContentDidChange: "text-editor-content-did-change", + FormattingDidChange: "text-editor-formatting-did-change" +}; |