2 * Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
26 WI.TextEditor = class TextEditor extends WI.View
28 constructor(element, mimeType, delegate)
32 this.element.classList.add("text-editor", WI.SyntaxHighlightedStyleClassName);
34 this._codeMirror = WI.CodeMirrorEditor.create(this.element, {
36 indentWithTabs: WI.settings.indentWithTabs.value,
37 indentUnit: WI.settings.indentUnit.value,
38 tabSize: WI.settings.tabSize.value,
40 lineWrapping: WI.settings.enableLineWrapping.value,
42 autoCloseBrackets: true,
43 showWhitespaceCharacters: WI.settings.showWhitespaceCharacters.value,
44 styleSelectedText: true,
47 WI.settings.indentWithTabs.addEventListener(WI.Setting.Event.Changed, (event) => {
48 this._codeMirror.setOption("indentWithTabs", WI.settings.indentWithTabs.value);
51 WI.settings.indentUnit.addEventListener(WI.Setting.Event.Changed, (event) => {
52 this._codeMirror.setOption("indentUnit", WI.settings.indentUnit.value);
55 WI.settings.tabSize.addEventListener(WI.Setting.Event.Changed, (event) => {
56 this._codeMirror.setOption("tabSize", WI.settings.tabSize.value);
59 WI.settings.enableLineWrapping.addEventListener(WI.Setting.Event.Changed, (event) => {
60 this._codeMirror.setOption("lineWrapping", WI.settings.enableLineWrapping.value);
63 WI.settings.showWhitespaceCharacters.addEventListener(WI.Setting.Event.Changed, (event) => {
64 this._codeMirror.setOption("showWhitespaceCharacters", WI.settings.showWhitespaceCharacters.value);
67 this._codeMirror.on("focus", this._editorFocused.bind(this));
68 this._codeMirror.on("change", this._contentChanged.bind(this));
69 this._codeMirror.on("gutterClick", this._gutterMouseDown.bind(this));
70 this._codeMirror.on("gutterContextMenu", this._gutterContextMenu.bind(this));
71 this._codeMirror.getScrollerElement().addEventListener("click", this._openClickedLinks.bind(this), true);
73 this._completionController = new WI.CodeMirrorCompletionController(this._codeMirror, this);
74 this._tokenTrackingController = new WI.CodeMirrorTokenTrackingController(this._codeMirror, this);
76 this._initialStringNotSet = true;
78 this.mimeType = mimeType;
80 this._breakpoints = {};
81 this._executionLineNumber = NaN;
82 this._executionColumnNumber = NaN;
84 this._executionLineHandle = null;
85 this._executionMultilineHandles = [];
86 this._executionRangeHighlightMarker = null;
88 this._searchQuery = null;
89 this._searchResults = [];
90 this._currentSearchResultIndex = -1;
91 this._ignoreCodeMirrorContentDidChangeEvent = 0;
93 this._formatted = false;
94 this._formattingPromise = null;
95 this._formatterSourceMap = null;
96 this._deferReveal = false;
97 this._repeatReveal = false;
99 this._delegate = delegate || null;
106 return this._visible;
111 return this._codeMirror.getValue();
114 set string(newString)
116 let previousSelectedTextRange = this._repeatReveal ? this.selectedTextRange : null;
120 // Clear any styles that may have been set on the empty line before content loaded.
121 if (this._initialStringNotSet)
122 this._codeMirror.removeLineClass(0, "wrap");
124 if (this._codeMirror.getValue() !== newString)
125 this._codeMirror.setValue(newString);
127 // Ensure we at display content even if the value did not change. This often happens when auto formatting.
131 if (this._initialStringNotSet) {
132 this._codeMirror.clearHistory();
133 this._codeMirror.markClean();
135 this._initialStringNotSet = false;
137 // There may have been an attempt at a search before the initial string was set. If so, reperform it now that we have content.
138 if (this._searchQuery) {
139 let query = this._searchQuery;
140 this._searchQuery = null;
141 this.performSearch(query);
144 if (this._codeMirror.getMode().name === "null") {
145 // If the content matches a known MIME type, but isn't explicitly declared as
146 // such, attempt to detect that so we can enable syntax highlighting and
147 // formatting features.
148 this._attemptToDetermineMIMEType();
152 // Update the execution line now that we might have content for that line.
153 this._updateExecutionLine();
154 this._updateExecutionRangeHighlight();
156 // Set the breakpoint styles now that we might have content for those lines.
157 for (var lineNumber in this._breakpoints)
158 this._setBreakpointStylesOnLine(lineNumber);
160 // Try revealing the pending line, or previous position, now that we might have new content.
161 this._revealPendingPositionIfPossible();
162 if (previousSelectedTextRange) {
163 this.selectedTextRange = previousSelectedTextRange;
164 let position = this._codeMirrorPositionFromTextRange(previousSelectedTextRange);
165 this._scrollIntoViewCentered(position.start);
169 this._ignoreCodeMirrorContentDidChangeEvent++;
170 this._codeMirror.operation(update.bind(this));
171 this._ignoreCodeMirrorContentDidChangeEvent--;
172 console.assert(this._ignoreCodeMirrorContentDidChangeEvent >= 0);
177 return this._codeMirror.getOption("readOnly") || false;
180 set readOnly(readOnly)
182 this._codeMirror.setOption("readOnly", readOnly);
187 return this._formatted;
192 let historySize = this._codeMirror.historySize().undo;
194 // Formatting code creates a history item.
198 return historySize > 0;
201 updateFormattedState(formatted)
203 return this._format(formatted);
208 let mode = this._codeMirror.getMode().name;
209 return mode === "javascript" || mode === "css";
214 // Can be overridden by subclasses.
215 return this.hasFormatter();
218 canShowTypeAnnotations()
223 canShowCoverageHints()
228 get selectedTextRange()
230 var start = this._codeMirror.getCursor(true);
231 var end = this._codeMirror.getCursor(false);
232 return this._textRangeFromCodeMirrorPosition(start, end);
235 set selectedTextRange(textRange)
237 if (document.activeElement === document.body)
240 var position = this._codeMirrorPositionFromTextRange(textRange);
241 this._codeMirror.setSelection(position.start, position.end);
246 return this._mimeType;
249 set mimeType(newMIMEType)
251 newMIMEType = parseMIMEType(newMIMEType).type;
253 this._mimeType = newMIMEType;
254 this._codeMirror.setOption("mode", {name: newMIMEType, globalVars: true});
256 this.dispatchEventToListeners(WI.TextEditor.Event.MIMETypeChanged);
259 get executionLineNumber()
261 return this._executionLineNumber;
264 get executionColumnNumber()
266 return this._executionColumnNumber;
269 get formatterSourceMap()
271 return this._formatterSourceMap;
274 get tokenTrackingController()
276 return this._tokenTrackingController;
281 return this._delegate;
284 set delegate(newDelegate)
286 this._delegate = newDelegate || null;
289 get numberOfSearchResults()
291 return this._searchResults.length;
294 get currentSearchQuery()
296 return this._searchQuery;
299 set automaticallyRevealFirstSearchResult(reveal)
301 this._automaticallyRevealFirstSearchResult = reveal;
303 // If we haven't shown a search result yet, reveal one now.
304 if (this._automaticallyRevealFirstSearchResult && this._searchResults.length > 0) {
305 if (this._currentSearchResultIndex === -1)
306 this._revealFirstSearchResultAfterCursor();
310 set deferReveal(defer)
312 this._deferReveal = defer;
315 set repeatReveal(repeat)
317 this._repeatReveal = repeat;
322 if (this._searchQuery === query)
325 this.searchCleared();
327 this._searchQuery = query;
329 // Defer until the initial string is set.
330 if (this._initialStringNotSet)
333 // Allow subclasses to handle the searching if they have a better way.
334 // If we are formatted, just use CodeMirror's search.
335 if (typeof this.customPerformSearch === "function" && !this.formatted) {
336 if (this.customPerformSearch(query))
340 // Go down the slow patch for all other text content.
341 var queryRegex = new RegExp(query.escapeForRegExp(), "gi");
342 var searchCursor = this._codeMirror.getSearchCursor(queryRegex, {line: 0, ch: 0}, false);
343 var boundBatchSearch = batchSearch.bind(this);
344 var numberOfSearchResultsDidChangeTimeout = null;
346 function reportNumberOfSearchResultsDidChange()
348 if (numberOfSearchResultsDidChangeTimeout) {
349 clearTimeout(numberOfSearchResultsDidChangeTimeout);
350 numberOfSearchResultsDidChangeTimeout = null;
353 this.dispatchEventToListeners(WI.TextEditor.Event.NumberOfSearchResultsDidChange);
356 function batchSearch()
358 // Bail if the query changed since we started.
359 if (this._searchQuery !== query)
362 var newSearchResults = [];
363 var foundResult = false;
364 for (var i = 0; i < WI.TextEditor.NumberOfFindsPerSearchBatch && (foundResult = searchCursor.findNext()); ++i) {
365 var textRange = this._textRangeFromCodeMirrorPosition(searchCursor.from(), searchCursor.to());
366 newSearchResults.push(textRange);
369 this.addSearchResults(newSearchResults);
371 // Don't report immediately, coalesce updates so they come in no faster than half a second.
372 if (!numberOfSearchResultsDidChangeTimeout)
373 numberOfSearchResultsDidChangeTimeout = setTimeout(reportNumberOfSearchResultsDidChange.bind(this), 500);
376 // More lines to search, set a timeout so we don't block the UI long.
377 setTimeout(boundBatchSearch, 50);
379 // Report immediately now that we are finished, canceling any pending update.
380 reportNumberOfSearchResultsDidChange.call(this);
388 setExecutionLineAndColumn(lineNumber, columnNumber)
390 // Only return early if there isn't a line handle and that isn't changing.
391 if (!this._executionLineHandle && isNaN(lineNumber))
394 this._executionLineNumber = lineNumber;
395 this._executionColumnNumber = columnNumber;
397 if (!this._initialStringNotSet) {
398 this._updateExecutionLine();
399 this._updateExecutionRangeHighlight();
402 // Still dispatch the event even if the number didn't change. The execution state still
403 // could have changed (e.g. continuing in a loop with a breakpoint inside).
404 this.dispatchEventToListeners(WI.TextEditor.Event.ExecutionLineNumberDidChange);
407 addSearchResults(textRanges)
409 console.assert(textRanges);
410 if (!textRanges || !textRanges.length)
413 function markRanges()
415 for (var i = 0; i < textRanges.length; ++i) {
416 var position = this._codeMirrorPositionFromTextRange(textRanges[i]);
417 var mark = this._codeMirror.markText(position.start, position.end, {className: WI.TextEditor.SearchResultStyleClassName});
418 this._searchResults.push(mark);
421 // If we haven't shown a search result yet, reveal one now.
422 if (this._automaticallyRevealFirstSearchResult) {
423 if (this._currentSearchResultIndex === -1)
424 this._revealFirstSearchResultAfterCursor();
428 this._codeMirror.operation(markRanges.bind(this));
433 this._codeMirror.operation(() => {
434 for (let searchResult of this._searchResults)
435 searchResult.clear();
438 this._searchQuery = null;
439 this._searchResults = [];
440 this._currentSearchResultIndex = -1;
443 searchQueryWithSelection()
445 if (!this._codeMirror.somethingSelected())
448 return this._codeMirror.getSelection();
451 revealPreviousSearchResult(changeFocus)
453 if (!this._searchResults.length)
456 if (this._currentSearchResultIndex === -1 || this._cursorDoesNotMatchLastRevealedSearchResult()) {
457 this._revealFirstSearchResultBeforeCursor(changeFocus);
461 if (this._currentSearchResultIndex > 0)
462 --this._currentSearchResultIndex;
464 this._currentSearchResultIndex = this._searchResults.length - 1;
466 this._revealSearchResult(this._searchResults[this._currentSearchResultIndex], changeFocus, -1);
469 revealNextSearchResult(changeFocus)
471 if (!this._searchResults.length)
474 if (this._currentSearchResultIndex === -1 || this._cursorDoesNotMatchLastRevealedSearchResult()) {
475 this._revealFirstSearchResultAfterCursor(changeFocus);
479 if (this._currentSearchResultIndex + 1 < this._searchResults.length)
480 ++this._currentSearchResultIndex;
482 this._currentSearchResultIndex = 0;
484 this._revealSearchResult(this._searchResults[this._currentSearchResultIndex], changeFocus, 1);
489 return this._codeMirror.getLine(lineNumber);
492 getTextInRange(startPosition, endPosition)
494 return this._codeMirror.getRange(startPosition.toCodeMirror(), endPosition.toCodeMirror());
497 addStyleToTextRange(startPosition, endPosition, styleClassName)
499 endPosition = endPosition.offsetColumn(1);
500 return this._codeMirror.getDoc().markText(startPosition.toCodeMirror(), endPosition.toCodeMirror(), {className: styleClassName, inclusiveLeft: true, inclusiveRight: true});
503 revealPosition(position, textRangeToSelect, forceUnformatted, noHighlight)
505 console.assert(position === undefined || position instanceof WI.SourceCodePosition, "revealPosition called without a SourceCodePosition");
506 if (!(position instanceof WI.SourceCodePosition))
509 if (!this._visible || this._initialStringNotSet || this._deferReveal) {
510 // If we can't get a line handle or are not visible then we wait to do the reveal.
511 this._positionToReveal = position;
512 this._textRangeToSelect = textRangeToSelect;
513 this._forceUnformatted = forceUnformatted;
517 // Delete now that the reveal is happening.
518 delete this._positionToReveal;
519 delete this._textRangeToSelect;
520 delete this._forceUnformatted;
522 // If we need to unformat, reveal the line after a wait.
523 // Otherwise the line highlight doesn't work properly.
524 if (this._formatted && forceUnformatted) {
525 this.updateFormattedState(false).then(() => {
526 setTimeout(this.revealPosition.bind(this), 0, position, textRangeToSelect);
531 let line = Number.constrain(position.lineNumber, 0, this._codeMirror.lineCount() - 1);
532 let lineHandle = this._codeMirror.getLineHandle(line);
534 if (!textRangeToSelect) {
535 let column = Number.constrain(position.columnNumber, 0, this._codeMirror.getLine(line).length);
536 textRangeToSelect = new WI.TextRange(line, column, line, column);
539 function removeStyleClass()
541 this._codeMirror.removeLineClass(lineHandle, "wrap", WI.TextEditor.HighlightedStyleClassName);
544 function revealAndHighlightLine()
546 // If the line is not visible, reveal it as the center line in the editor.
547 var position = this._codeMirrorPositionFromTextRange(textRangeToSelect);
548 if (!this._isPositionVisible(position.start))
549 this._scrollIntoViewCentered(position.start);
551 this.selectedTextRange = textRangeToSelect;
556 // Avoid highlighting the execution line while debugging.
557 if (WI.debuggerManager.paused && line === this._executionLineNumber)
560 this._codeMirror.addLineClass(lineHandle, "wrap", WI.TextEditor.HighlightedStyleClassName);
562 // Use a timeout instead of a animationEnd event listener because the line element might
563 // be removed if the user scrolls during the animation. In that case animationEnd isn't
564 // fired, and the line would highlight again the next time it scrolls into view.
565 setTimeout(removeStyleClass.bind(this), WI.TextEditor.HighlightAnimationDuration);
568 this._codeMirror.operation(revealAndHighlightLine.bind(this));
573 this._visible = true;
575 // Refresh since our size might have changed.
576 this._codeMirror.refresh();
578 // Try revealing the pending line now that we are visible.
579 // This needs to be done as a separate operation from the refresh
580 // so that the scrollInfo coordinates are correct.
581 this._revealPendingPositionIfPossible();
586 this._visible = false;
591 WI.settings.indentWithTabs.removeEventListener(null, null, this);
592 WI.settings.indentUnit.removeEventListener(null, null, this);
593 WI.settings.tabSize.removeEventListener(null, null, this);
594 WI.settings.enableLineWrapping.removeEventListener(null, null, this);
595 WI.settings.showWhitespaceCharacters.removeEventListener(null, null, this);
598 setBreakpointInfoForLineAndColumn(lineNumber, columnNumber, breakpointInfo)
600 if (this._ignoreSetBreakpointInfoCalls)
604 this._addBreakpointToLineAndColumnWithInfo(lineNumber, columnNumber, breakpointInfo);
606 this._removeBreakpointFromLineAndColumn(lineNumber, columnNumber);
609 updateBreakpointLineAndColumn(oldLineNumber, oldColumnNumber, newLineNumber, newColumnNumber)
611 console.assert(this._breakpoints[oldLineNumber]);
612 if (!this._breakpoints[oldLineNumber])
615 console.assert(this._breakpoints[oldLineNumber][oldColumnNumber]);
616 if (!this._breakpoints[oldLineNumber][oldColumnNumber])
619 var breakpointInfo = this._breakpoints[oldLineNumber][oldColumnNumber];
620 this._removeBreakpointFromLineAndColumn(oldLineNumber, oldColumnNumber);
621 this._addBreakpointToLineAndColumnWithInfo(newLineNumber, newColumnNumber, breakpointInfo);
624 addStyleClassToLine(lineNumber, styleClassName)
626 var lineHandle = this._codeMirror.getLineHandle(lineNumber);
630 return this._codeMirror.addLineClass(lineHandle, "wrap", styleClassName);
633 removeStyleClassFromLine(lineNumber, styleClassName)
635 var lineHandle = this._codeMirror.getLineHandle(lineNumber);
636 console.assert(lineHandle);
640 return this._codeMirror.removeLineClass(lineHandle, "wrap", styleClassName);
643 toggleStyleClassForLine(lineNumber, styleClassName)
645 var lineHandle = this._codeMirror.getLineHandle(lineNumber);
646 console.assert(lineHandle);
650 return this._codeMirror.toggleLineClass(lineHandle, "wrap", styleClassName);
653 createWidgetForLine(lineNumber)
655 var lineHandle = this._codeMirror.getLineHandle(lineNumber);
659 var widgetElement = document.createElement("div");
660 var lineWidget = this._codeMirror.addLineWidget(lineHandle, widgetElement, {coverGutter: false, noHScroll: true});
661 return new WI.LineWidget(lineWidget, widgetElement);
666 return this._codeMirror.lineCount();
671 this._codeMirror.focus();
674 contentDidChange(replacedRanges, newRanges)
676 // Implemented by subclasses.
681 return this._codeMirror.rectsForRange(range);
686 return this._codeMirror.getAllMarks().map(WI.TextMarker.textMarkerForCodeMirrorTextMarker);
689 markersAtPosition(position)
691 return this._codeMirror.findMarksAt(position).map(WI.TextMarker.textMarkerForCodeMirrorTextMarker);
694 createColorMarkers(range)
696 return createCodeMirrorColorTextMarkers(this._codeMirror, range);
699 createGradientMarkers(range)
701 return createCodeMirrorGradientTextMarkers(this._codeMirror, range);
704 createCubicBezierMarkers(range)
706 return createCodeMirrorCubicBezierTextMarkers(this._codeMirror, range);
709 createSpringMarkers(range)
711 return createCodeMirrorSpringTextMarkers(this._codeMirror, range);
714 editingControllerForMarker(editableMarker)
716 switch (editableMarker.type) {
717 case WI.TextMarker.Type.Color:
718 return new WI.CodeMirrorColorEditingController(this._codeMirror, editableMarker);
719 case WI.TextMarker.Type.Gradient:
720 return new WI.CodeMirrorGradientEditingController(this._codeMirror, editableMarker);
721 case WI.TextMarker.Type.CubicBezier:
722 return new WI.CodeMirrorBezierEditingController(this._codeMirror, editableMarker);
723 case WI.TextMarker.Type.Spring:
724 return new WI.CodeMirrorSpringEditingController(this._codeMirror, editableMarker);
726 return new WI.CodeMirrorEditingController(this._codeMirror, editableMarker);
730 visibleRangeOffsets()
732 var startOffset = null;
733 var endOffset = null;
734 var visibleRange = this._codeMirror.getViewport();
736 if (this._formatterSourceMap) {
737 startOffset = this._formatterSourceMap.formattedToOriginalOffset(Math.max(visibleRange.from - 1, 0), 0);
738 endOffset = this._formatterSourceMap.formattedToOriginalOffset(visibleRange.to - 1, 0);
740 startOffset = this._codeMirror.getDoc().indexFromPos({line: visibleRange.from, ch: 0});
741 endOffset = this._codeMirror.getDoc().indexFromPos({line: visibleRange.to, ch: 0});
744 return {startOffset, endOffset};
747 visibleRangePositions()
749 let visibleRange = this._codeMirror.getViewport();
753 if (this._formatterSourceMap) {
754 startLine = this._formatterSourceMap.formattedToOriginal(Math.max(visibleRange.from - 1, 0), 0).lineNumber;
755 endLine = this._formatterSourceMap.formattedToOriginal(visibleRange.to - 1, 0).lineNumber;
757 startLine = visibleRange.from;
758 endLine = visibleRange.to;
762 startPosition: new WI.SourceCodePosition(startLine, 0),
763 endPosition: new WI.SourceCodePosition(endLine, 0)
767 originalPositionToCurrentPosition(position)
769 if (!this._formatterSourceMap)
772 let {lineNumber, columnNumber} = this._formatterSourceMap.originalToFormatted(position.lineNumber, position.columnNumber);
773 return new WI.SourceCodePosition(lineNumber, columnNumber);
776 originalOffsetToCurrentPosition(offset)
779 if (this._formatterSourceMap) {
780 var location = this._formatterSourceMap.originalPositionToFormatted(offset);
781 position = {line: location.lineNumber, ch: location.columnNumber};
783 position = this._codeMirror.getDoc().posFromIndex(offset);
788 currentOffsetToCurrentPosition(offset)
790 let pos = this._codeMirror.getDoc().posFromIndex(offset);
791 return new WI.SourceCodePosition(pos.line, pos.ch);
794 currentPositionToOriginalOffset(position)
798 if (this._formatterSourceMap)
799 offset = this._formatterSourceMap.formattedToOriginalOffset(position.line, position.ch);
801 offset = this._codeMirror.getDoc().indexFromPos(position);
806 currentPositionToOriginalPosition(position)
808 if (!this._formatterSourceMap)
811 let location = this._formatterSourceMap.formattedToOriginal(position.lineNumber, position.columnNumber);
812 return new WI.SourceCodePosition(location.lineNumber, location.columnNumber);
815 currentPositionToCurrentOffset(position)
817 return this._codeMirror.getDoc().indexFromPos(position.toCodeMirror());
820 setInlineWidget(position, inlineElement)
822 return this._codeMirror.setUniqueBookmark(position.toCodeMirror(), {widget: inlineElement});
825 addScrollHandler(handler)
827 this._codeMirror.on("scroll", handler);
830 removeScrollHandler(handler)
832 this._codeMirror.off("scroll", handler);
839 // FIXME: <https://webkit.org/b/146256> Web Inspector: Nested ContentBrowsers / ContentViewContainers cause too many ContentView updates
840 // Ideally we would not get an updateLayout call if we are not visible. We should restructure ContentView
841 // show/hide restoration to reduce duplicated work and solve this in the process.
843 // FIXME: visible check can be removed once <https://webkit.org/b/150741> is fixed.
845 this._codeMirror.refresh();
850 if (this._formatted === formatted)
851 return Promise.resolve(this._formatted);
853 console.assert(!formatted || this.canBeFormatted());
854 if (formatted && !this.canBeFormatted())
855 return Promise.resolve(this._formatted);
857 if (this._formattingPromise)
858 return this._formattingPromise;
860 this._ignoreCodeMirrorContentDidChangeEvent++;
861 this._formattingPromise = this.prettyPrint(formatted).then(() => {
862 this._ignoreCodeMirrorContentDidChangeEvent--;
863 console.assert(this._ignoreCodeMirrorContentDidChangeEvent >= 0);
865 this._formattingPromise = null;
867 let originalFormatted = this._formatted;
868 this._formatted = !!this._formatterSourceMap;
870 if (this._formatted !== originalFormatted)
871 this.dispatchEventToListeners(WI.TextEditor.Event.FormattingDidChange);
873 return this._formatted;
876 return this._formattingPromise;
881 return new Promise((resolve, reject) => {
882 let beforePrettyPrintState = {
883 selectionAnchor: this._codeMirror.getCursor("anchor"),
884 selectionHead: this._codeMirror.getCursor("head"),
888 this._undoFormatting(beforePrettyPrintState, resolve);
889 else if (this._canUseFormatterWorker())
890 this._startWorkerPrettyPrint(beforePrettyPrintState, resolve);
892 this._startCodeMirrorPrettyPrint(beforePrettyPrintState, resolve);
896 _canUseFormatterWorker()
898 return this._codeMirror.getMode().name === "javascript";
901 _attemptToDetermineMIMEType()
903 let startTime = Date.now();
905 const isModule = false;
906 const includeSourceMapData = false;
907 let workerProxy = WI.FormatterWorkerProxy.singleton();
908 workerProxy.formatJavaScript(this.string, isModule, WI.indentString(), includeSourceMapData, ({formattedText}) => {
912 this.mimeType = "text/javascript";
914 if (Date.now() - startTime < 100)
915 this.updateFormattedState(true);
919 _startWorkerPrettyPrint(beforePrettyPrintState, callback)
921 let sourceText = this._codeMirror.getValue();
922 let indentString = WI.indentString();
923 const includeSourceMapData = true;
925 let sourceType = this._delegate ? this._delegate.textEditorScriptSourceType(this) : WI.Script.SourceType.Program;
926 const isModule = sourceType === WI.Script.SourceType.Module;
928 let workerProxy = WI.FormatterWorkerProxy.singleton();
929 workerProxy.formatJavaScript(sourceText, isModule, indentString, includeSourceMapData, ({formattedText, sourceMapData}) => {
930 // Handle if formatting failed, which is possible for invalid programs.
931 if (formattedText === null) {
935 this._finishPrettyPrint(beforePrettyPrintState, formattedText, sourceMapData, callback);
939 _startCodeMirrorPrettyPrint(beforePrettyPrintState, callback)
941 let indentString = WI.indentString();
942 let start = {line: 0, ch: 0};
943 let end = {line: this._codeMirror.lineCount() - 1};
944 let builder = new FormatterContentBuilder(indentString);
945 let formatter = new WI.Formatter(this._codeMirror, builder);
946 formatter.format(start, end);
948 let formattedText = builder.formattedContent;
949 let sourceMapData = builder.sourceMapData;
950 this._finishPrettyPrint(beforePrettyPrintState, formattedText, sourceMapData, callback);
953 _finishPrettyPrint(beforePrettyPrintState, formattedText, sourceMapData, callback)
955 this._codeMirror.operation(() => {
956 this._formatterSourceMap = WI.FormatterSourceMap.fromSourceMapData(sourceMapData);
957 this._codeMirror.setValue(formattedText);
958 this._updateAfterFormatting(true, beforePrettyPrintState);
964 _undoFormatting(beforePrettyPrintState, callback)
966 this._codeMirror.operation(() => {
967 this._codeMirror.undo();
968 this._updateAfterFormatting(false, beforePrettyPrintState);
974 _updateAfterFormatting(pretty, beforePrettyPrintState)
976 let oldSelectionAnchor = beforePrettyPrintState.selectionAnchor;
977 let oldSelectionHead = beforePrettyPrintState.selectionHead;
978 let newSelectionAnchor, newSelectionHead;
979 let newExecutionLocation = null;
982 if (this._positionToReveal) {
983 let newRevealPosition = this._formatterSourceMap.originalToFormatted(this._positionToReveal.lineNumber, this._positionToReveal.columnNumber);
984 this._positionToReveal = new WI.SourceCodePosition(newRevealPosition.lineNumber, newRevealPosition.columnNumber);
987 if (this._textRangeToSelect) {
988 let mappedRevealSelectionStart = this._formatterSourceMap.originalToFormatted(this._textRangeToSelect.startLine, this._textRangeToSelect.startColumn);
989 let mappedRevealSelectionEnd = this._formatterSourceMap.originalToFormatted(this._textRangeToSelect.endLine, this._textRangeToSelect.endColumn);
990 this._textRangeToSelect = new WI.TextRange(mappedRevealSelectionStart.lineNumber, mappedRevealSelectionStart.columnNumber, mappedRevealSelectionEnd.lineNumber, mappedRevealSelectionEnd.columnNumber);
993 if (!isNaN(this._executionLineNumber)) {
994 console.assert(!isNaN(this._executionColumnNumber));
995 newExecutionLocation = this._formatterSourceMap.originalToFormatted(this._executionLineNumber, this._executionColumnNumber);
998 let mappedAnchorLocation = this._formatterSourceMap.originalToFormatted(oldSelectionAnchor.line, oldSelectionAnchor.ch);
999 let mappedHeadLocation = this._formatterSourceMap.originalToFormatted(oldSelectionHead.line, oldSelectionHead.ch);
1000 newSelectionAnchor = {line: mappedAnchorLocation.lineNumber, ch: mappedAnchorLocation.columnNumber};
1001 newSelectionHead = {line: mappedHeadLocation.lineNumber, ch: mappedHeadLocation.columnNumber};
1003 if (this._positionToReveal) {
1004 let newRevealPosition = this._formatterSourceMap.formattedToOriginal(this._positionToReveal.lineNumber, this._positionToReveal.columnNumber);
1005 this._positionToReveal = new WI.SourceCodePosition(newRevealPosition.lineNumber, newRevealPosition.columnNumber);
1008 if (this._textRangeToSelect) {
1009 let mappedRevealSelectionStart = this._formatterSourceMap.formattedToOriginal(this._textRangeToSelect.startLine, this._textRangeToSelect.startColumn);
1010 let mappedRevealSelectionEnd = this._formatterSourceMap.formattedToOriginal(this._textRangeToSelect.endLine, this._textRangeToSelect.endColumn);
1011 this._textRangeToSelect = new WI.TextRange(mappedRevealSelectionStart.lineNumber, mappedRevealSelectionStart.columnNumber, mappedRevealSelectionEnd.lineNumber, mappedRevealSelectionEnd.columnNumber);
1014 if (!isNaN(this._executionLineNumber)) {
1015 console.assert(!isNaN(this._executionColumnNumber));
1016 newExecutionLocation = this._formatterSourceMap.formattedToOriginal(this._executionLineNumber, this._executionColumnNumber);
1019 let mappedAnchorLocation = this._formatterSourceMap.formattedToOriginal(oldSelectionAnchor.line, oldSelectionAnchor.ch);
1020 let mappedHeadLocation = this._formatterSourceMap.formattedToOriginal(oldSelectionHead.line, oldSelectionHead.ch);
1021 newSelectionAnchor = {line: mappedAnchorLocation.lineNumber, ch: mappedAnchorLocation.columnNumber};
1022 newSelectionHead = {line: mappedHeadLocation.lineNumber, ch: mappedHeadLocation.columnNumber};
1024 this._formatterSourceMap = null;
1027 this._scrollIntoViewCentered(newSelectionAnchor);
1028 this._codeMirror.setSelection(newSelectionAnchor, newSelectionHead);
1030 if (newExecutionLocation) {
1031 this._executionLineHandle = null;
1032 this._executionMultilineHandles = [];
1033 this.setExecutionLineAndColumn(newExecutionLocation.lineNumber, newExecutionLocation.columnNumber);
1036 // FIXME: <rdar://problem/13129955> FindBanner: New searches should not lose search position (start from current selection/caret)
1037 if (this.currentSearchQuery) {
1038 let searchQuery = this.currentSearchQuery;
1039 this.searchCleared();
1040 // Set timeout so that this happens after the current CodeMirror operation.
1041 // The editor has to update for the value and selection changes.
1042 setTimeout(() => { this.performSearch(searchQuery); }, 0);
1045 if (this._delegate && typeof this._delegate.textEditorUpdatedFormatting === "function")
1046 this._delegate.textEditorUpdatedFormatting(this);
1053 return !this._codeMirror.isClean();
1056 _editorFocused(codeMirror)
1058 this.dispatchEventToListeners(WI.TextEditor.Event.Focused);
1061 _contentChanged(codeMirror, change)
1063 if (this._ignoreCodeMirrorContentDidChangeEvent > 0)
1066 var replacedRanges = [];
1069 replacedRanges.push(new WI.TextRange(
1075 newRanges.push(new WI.TextRange(
1078 change.from.line + change.text.length - 1,
1079 change.text.length === 1 ? change.from.ch + change.text[0].length : change.text.lastValue.length
1081 change = change.next;
1083 this.contentDidChange(replacedRanges, newRanges);
1085 if (this._formatted) {
1086 this._formatterSourceMap = null;
1087 this._formatted = false;
1089 if (this._delegate && typeof this._delegate.textEditorUpdatedFormatting === "function")
1090 this._delegate.textEditorUpdatedFormatting(this);
1092 this.dispatchEventToListeners(WI.TextEditor.Event.FormattingDidChange);
1095 this.dispatchEventToListeners(WI.TextEditor.Event.ContentDidChange);
1098 _textRangeFromCodeMirrorPosition(start, end)
1100 console.assert(start);
1101 console.assert(end);
1103 return new WI.TextRange(start.line, start.ch, end.line, end.ch);
1106 _codeMirrorPositionFromTextRange(textRange)
1108 console.assert(textRange);
1110 var start = {line: textRange.startLine, ch: textRange.startColumn};
1111 var end = {line: textRange.endLine, ch: textRange.endColumn};
1112 return {start, end};
1115 _revealPendingPositionIfPossible()
1117 // Nothing to do if we don't have a pending position.
1118 if (!this._positionToReveal)
1121 // Don't try to reveal unless we are visible.
1125 this.revealPosition(this._positionToReveal, this._textRangeToSelect, this._forceUnformatted);
1128 _revealSearchResult(result, changeFocus, directionInCaseOfRevalidation)
1130 let position = result.find();
1132 // Check for a valid position, it might have been removed from editing by the user.
1133 // If the position is invalide, revalidate all positions reveal as needed.
1135 this._revalidateSearchResults(directionInCaseOfRevalidation);
1139 // If the line is not visible, reveal it as the center line in the editor.
1140 if (!this._isPositionVisible(position.from))
1141 this._scrollIntoViewCentered(position.from);
1143 // Update the text selection to select the search result.
1144 this.selectedTextRange = this._textRangeFromCodeMirrorPosition(position.from, position.to);
1146 // Remove the automatically reveal state now that we have revealed a search result.
1147 this._automaticallyRevealFirstSearchResult = false;
1149 // Focus the editor if requested.
1151 this._codeMirror.focus();
1153 // Collect info for the bouncy highlight.
1154 let highlightEditorPosition = this._codeMirror.getCursor("start");
1155 let textContent = this._codeMirror.getSelection();
1157 // Remove the bouncy highlight if it is still around. The animation will not
1158 // start unless we remove it and add it back to the document.
1159 this._removeBouncyHighlightElementIfNeeded();
1161 // Create the bouncy highlight.
1162 this._bouncyHighlightElement = document.createElement("div");
1163 this._bouncyHighlightElement.className = WI.TextEditor.BouncyHighlightStyleClassName;
1164 this._bouncyHighlightElement.textContent = textContent;
1166 function positionBouncyHighlight() {
1167 // Adjust the coordinates to be based in the text editor's space.
1168 let coordinates = this._codeMirror.cursorCoords(highlightEditorPosition, "page");
1169 let textEditorRect = this.element.getBoundingClientRect();
1170 coordinates.top -= textEditorRect.top;
1171 coordinates.left -= textEditorRect.left;
1173 // Position the bouncy highlight.
1174 this._bouncyHighlightElement.style.top = coordinates.top + "px";
1175 this._bouncyHighlightElement.style.left = coordinates.left + "px";
1178 // Position and show the highlight.
1179 positionBouncyHighlight.call(this);
1180 this.element.appendChild(this._bouncyHighlightElement);
1182 // Reposition the highlight if the editor scrolls.
1183 this._bouncyHighlightScrollHandler = () => { positionBouncyHighlight.call(this); };
1184 this.addScrollHandler(this._bouncyHighlightScrollHandler);
1186 // Listen for the end of the animation so we can remove the element.
1187 this._bouncyHighlightElement.addEventListener("animationend", () => {
1188 this._removeBouncyHighlightElementIfNeeded();
1192 _removeBouncyHighlightElementIfNeeded()
1194 if (!this._bouncyHighlightElement)
1197 this.removeScrollHandler(this._bouncyHighlightScrollHandler);
1198 this._bouncyHighlightScrollHandler = null;
1200 this._bouncyHighlightElement.remove();
1201 this._bouncyHighlightElement = null;
1204 _binarySearchInsertionIndexInSearchResults(object, comparator)
1206 // It is possible that markers in the search results array may have been deleted.
1207 // In those cases the comparator will return "null" and we immediately stop
1208 // the binary search and return null. The search results list needs to be updated.
1209 var array = this._searchResults;
1212 var last = array.length - 1;
1214 while (first <= last) {
1215 var mid = (first + last) >> 1;
1216 var c = comparator(object, array[mid]);
1230 _revealFirstSearchResultBeforeCursor(changeFocus)
1232 console.assert(this._searchResults.length);
1234 var currentCursorPosition = this._codeMirror.getCursor("start");
1235 if (currentCursorPosition.line === 0 && currentCursorPosition.ch === 0) {
1236 this._currentSearchResultIndex = this._searchResults.length - 1;
1237 this._revealSearchResult(this._searchResults[this._currentSearchResultIndex], changeFocus, -1);
1241 var index = this._binarySearchInsertionIndexInSearchResults(currentCursorPosition, function(current, searchResult) {
1242 var searchResultMarker = searchResult.find();
1243 if (!searchResultMarker)
1245 return WI.compareCodeMirrorPositions(current, searchResultMarker.from);
1248 if (index === null) {
1249 this._revalidateSearchResults(-1);
1253 this._currentSearchResultIndex = index;
1254 this._revealSearchResult(this._searchResults[this._currentSearchResultIndex], changeFocus);
1257 _revealFirstSearchResultAfterCursor(changeFocus)
1259 console.assert(this._searchResults.length);
1261 var currentCursorPosition = this._codeMirror.getCursor("start");
1262 if (currentCursorPosition.line === 0 && currentCursorPosition.ch === 0) {
1263 this._currentSearchResultIndex = 0;
1264 this._revealSearchResult(this._searchResults[this._currentSearchResultIndex], changeFocus, 1);
1268 var index = this._binarySearchInsertionIndexInSearchResults(currentCursorPosition, function(current, searchResult) {
1269 var searchResultMarker = searchResult.find();
1270 if (!searchResultMarker)
1272 return WI.compareCodeMirrorPositions(current, searchResultMarker.from);
1275 if (index === null) {
1276 this._revalidateSearchResults(1);
1280 if (index + 1 < this._searchResults.length)
1285 this._currentSearchResultIndex = index;
1286 this._revealSearchResult(this._searchResults[this._currentSearchResultIndex], changeFocus);
1289 _cursorDoesNotMatchLastRevealedSearchResult()
1291 console.assert(this._currentSearchResultIndex !== -1);
1292 console.assert(this._searchResults.length);
1294 var lastRevealedSearchResultMarker = this._searchResults[this._currentSearchResultIndex].find();
1295 if (!lastRevealedSearchResultMarker)
1298 var currentCursorPosition = this._codeMirror.getCursor("start");
1299 var lastRevealedSearchResultPosition = lastRevealedSearchResultMarker.from;
1301 return WI.compareCodeMirrorPositions(currentCursorPosition, lastRevealedSearchResultPosition) !== 0;
1304 _revalidateSearchResults(direction)
1306 console.assert(direction !== undefined);
1308 this._currentSearchResultIndex = -1;
1310 var updatedSearchResults = [];
1311 for (var i = 0; i < this._searchResults.length; ++i) {
1312 if (this._searchResults[i].find())
1313 updatedSearchResults.push(this._searchResults[i]);
1316 console.assert(updatedSearchResults.length !== this._searchResults.length);
1318 this._searchResults = updatedSearchResults;
1319 this.dispatchEventToListeners(WI.TextEditor.Event.NumberOfSearchResultsDidChange);
1321 if (this._searchResults.length) {
1323 this._revealFirstSearchResultAfterCursor();
1325 this._revealFirstSearchResultBeforeCursor();
1329 _clearMultilineExecutionLineHighlights()
1331 if (this._executionMultilineHandles.length) {
1332 for (let lineHandle of this._executionMultilineHandles)
1333 this._codeMirror.removeLineClass(lineHandle, "wrap", WI.TextEditor.ExecutionLineStyleClassName);
1334 this._executionMultilineHandles = [];
1338 _updateExecutionLine()
1340 this._codeMirror.operation(() => {
1341 if (this._executionLineHandle) {
1342 this._codeMirror.removeLineClass(this._executionLineHandle, "wrap", WI.TextEditor.ExecutionLineStyleClassName);
1343 this._codeMirror.removeLineClass(this._executionLineHandle, "wrap", "primary");
1346 this._clearMultilineExecutionLineHighlights();
1348 this._executionLineHandle = !isNaN(this._executionLineNumber) ? this._codeMirror.getLineHandle(this._executionLineNumber) : null;
1350 if (this._executionLineHandle) {
1351 this._codeMirror.addLineClass(this._executionLineHandle, "wrap", WI.TextEditor.ExecutionLineStyleClassName);
1352 this._codeMirror.addLineClass(this._executionLineHandle, "wrap", "primary");
1353 this._codeMirror.removeLineClass(this._executionLineHandle, "wrap", WI.TextEditor.HighlightedStyleClassName);
1358 _updateExecutionRangeHighlight()
1360 if (this._executionRangeHighlightMarker) {
1361 this._executionRangeHighlightMarker.clear();
1362 this._executionRangeHighlightMarker = null;
1365 if (isNaN(this._executionLineNumber))
1368 let currentPosition = new WI.SourceCodePosition(this._executionLineNumber, this._executionColumnNumber);
1370 this._delegate.textEditorExecutionHighlightRange(currentPosition, (range) => {
1373 // Highlight the rest of the line.
1374 start = {line: this._executionLineNumber, ch: this._executionColumnNumber};
1375 end = {line: this._executionLineNumber};
1377 // Highlight the range.
1378 start = range.startPosition.toCodeMirror();
1379 end = range.endPosition.toCodeMirror();
1382 // Ensure the marker is cleared in case there were multiple updates very quickly.
1383 if (this._executionRangeHighlightMarker) {
1384 this._executionRangeHighlightMarker.clear();
1385 this._executionRangeHighlightMarker = null;
1388 // Avoid highlighting trailing whitespace.
1389 let text = this._codeMirror.getRange(start, end);
1390 let trailingWhitespace = text.match(/\s+$/);
1391 if (trailingWhitespace)
1392 end.ch = Math.max(0, end.ch - trailingWhitespace[0].length);
1394 // Give each line containing part of the range the full line style.
1395 this._clearMultilineExecutionLineHighlights();
1396 if (start.line !== end.line) {
1397 for (let line = start.line; line < end.line; ++line) {
1398 let lineHandle = this._codeMirror.getLineHandle(line);
1399 this._codeMirror.addLineClass(lineHandle, "wrap", WI.TextEditor.ExecutionLineStyleClassName);
1400 this._executionMultilineHandles.push(lineHandle);
1404 this._executionRangeHighlightMarker = this._codeMirror.markText(start, end, {className: "execution-range-highlight"});
1408 _setBreakpointStylesOnLine(lineNumber)
1410 var columnBreakpoints = this._breakpoints[lineNumber];
1411 console.assert(columnBreakpoints);
1412 if (!columnBreakpoints)
1415 var allDisabled = true;
1416 var allResolved = true;
1417 var allAutoContinue = true;
1418 var multiple = Object.keys(columnBreakpoints).length > 1;
1419 for (var columnNumber in columnBreakpoints) {
1420 var breakpointInfo = columnBreakpoints[columnNumber];
1421 if (!breakpointInfo.disabled)
1422 allDisabled = false;
1423 if (!breakpointInfo.resolved)
1424 allResolved = false;
1425 if (!breakpointInfo.autoContinue)
1426 allAutoContinue = false;
1429 allResolved = allResolved && WI.debuggerManager.breakpointsEnabled;
1431 function updateStyles()
1433 // We might not have a line if the content isn't fully populated yet.
1434 // This will be called again when the content is available.
1435 var lineHandle = this._codeMirror.getLineHandle(lineNumber);
1439 this._codeMirror.addLineClass(lineHandle, "wrap", WI.TextEditor.HasBreakpointStyleClassName);
1442 this._codeMirror.addLineClass(lineHandle, "wrap", WI.TextEditor.BreakpointResolvedStyleClassName);
1444 this._codeMirror.removeLineClass(lineHandle, "wrap", WI.TextEditor.BreakpointResolvedStyleClassName);
1447 this._codeMirror.addLineClass(lineHandle, "wrap", WI.TextEditor.BreakpointDisabledStyleClassName);
1449 this._codeMirror.removeLineClass(lineHandle, "wrap", WI.TextEditor.BreakpointDisabledStyleClassName);
1451 if (allAutoContinue)
1452 this._codeMirror.addLineClass(lineHandle, "wrap", WI.TextEditor.BreakpointAutoContinueStyleClassName);
1454 this._codeMirror.removeLineClass(lineHandle, "wrap", WI.TextEditor.BreakpointAutoContinueStyleClassName);
1457 this._codeMirror.addLineClass(lineHandle, "wrap", WI.TextEditor.MultipleBreakpointsStyleClassName);
1459 this._codeMirror.removeLineClass(lineHandle, "wrap", WI.TextEditor.MultipleBreakpointsStyleClassName);
1462 this._codeMirror.operation(updateStyles.bind(this));
1465 _addBreakpointToLineAndColumnWithInfo(lineNumber, columnNumber, breakpointInfo)
1467 if (!this._breakpoints[lineNumber])
1468 this._breakpoints[lineNumber] = {};
1469 this._breakpoints[lineNumber][columnNumber] = breakpointInfo;
1471 this._setBreakpointStylesOnLine(lineNumber);
1474 _removeBreakpointFromLineAndColumn(lineNumber, columnNumber)
1476 console.assert(columnNumber in this._breakpoints[lineNumber]);
1477 delete this._breakpoints[lineNumber][columnNumber];
1479 // There are still breakpoints on the line. Update the breakpoint style.
1480 if (!isEmptyObject(this._breakpoints[lineNumber])) {
1481 this._setBreakpointStylesOnLine(lineNumber);
1485 delete this._breakpoints[lineNumber];
1487 function updateStyles()
1489 var lineHandle = this._codeMirror.getLineHandle(lineNumber);
1493 this._codeMirror.removeLineClass(lineHandle, "wrap", WI.TextEditor.HasBreakpointStyleClassName);
1494 this._codeMirror.removeLineClass(lineHandle, "wrap", WI.TextEditor.BreakpointResolvedStyleClassName);
1495 this._codeMirror.removeLineClass(lineHandle, "wrap", WI.TextEditor.BreakpointDisabledStyleClassName);
1496 this._codeMirror.removeLineClass(lineHandle, "wrap", WI.TextEditor.BreakpointAutoContinueStyleClassName);
1497 this._codeMirror.removeLineClass(lineHandle, "wrap", WI.TextEditor.MultipleBreakpointsStyleClassName);
1500 this._codeMirror.operation(updateStyles.bind(this));
1503 _allColumnBreakpointInfoForLine(lineNumber)
1505 return this._breakpoints[lineNumber];
1508 _setColumnBreakpointInfoForLine(lineNumber, columnBreakpointInfo)
1510 console.assert(columnBreakpointInfo);
1511 this._breakpoints[lineNumber] = columnBreakpointInfo;
1512 this._setBreakpointStylesOnLine(lineNumber);
1515 _gutterMouseDown(codeMirror, lineNumber, gutterElement, event)
1517 if (event.button !== 0 || event.ctrlKey)
1520 if (!this._codeMirror.hasLineClass(lineNumber, "wrap", WI.TextEditor.HasBreakpointStyleClassName)) {
1521 console.assert(!(lineNumber in this._breakpoints));
1523 // No breakpoint, add a new one.
1524 if (this._delegate && typeof this._delegate.textEditorBreakpointAdded === "function") {
1525 var data = this._delegate.textEditorBreakpointAdded(this, lineNumber, 0);
1527 var breakpointInfo = data.breakpointInfo;
1529 this._addBreakpointToLineAndColumnWithInfo(data.lineNumber, data.columnNumber, breakpointInfo);
1536 console.assert(lineNumber in this._breakpoints);
1538 if (this._codeMirror.hasLineClass(lineNumber, "wrap", WI.TextEditor.MultipleBreakpointsStyleClassName)) {
1539 console.assert(!isEmptyObject(this._breakpoints[lineNumber]));
1543 // Single existing breakpoint, start tracking it for dragging.
1544 console.assert(Object.keys(this._breakpoints[lineNumber]).length === 1);
1545 var columnNumber = Object.keys(this._breakpoints[lineNumber])[0];
1546 this._draggingBreakpointInfo = this._breakpoints[lineNumber][columnNumber];
1547 this._lineNumberWithMousedDownBreakpoint = lineNumber;
1548 this._lineNumberWithDraggedBreakpoint = lineNumber;
1549 this._columnNumberWithMousedDownBreakpoint = columnNumber;
1550 this._columnNumberWithDraggedBreakpoint = columnNumber;
1552 this._documentMouseMovedEventListener = this._documentMouseMoved.bind(this);
1553 this._documentMouseUpEventListener = this._documentMouseUp.bind(this);
1555 // Register these listeners on the document so we can track the mouse if it leaves the gutter.
1556 document.addEventListener("mousemove", this._documentMouseMovedEventListener, true);
1557 document.addEventListener("mouseup", this._documentMouseUpEventListener, true);
1560 _gutterContextMenu(codeMirror, lineNumber, gutterElement, event)
1562 if (this._delegate && typeof this._delegate.textEditorGutterContextMenu === "function") {
1563 var breakpoints = [];
1564 for (var columnNumber in this._breakpoints[lineNumber])
1565 breakpoints.push({lineNumber, columnNumber});
1567 this._delegate.textEditorGutterContextMenu(this, lineNumber, 0, breakpoints, event);
1571 _documentMouseMoved(event)
1573 console.assert("_lineNumberWithMousedDownBreakpoint" in this);
1574 if (!("_lineNumberWithMousedDownBreakpoint" in this))
1577 event.preventDefault();
1580 var position = this._codeMirror.coordsChar({left: event.pageX, top: event.pageY});
1582 // CodeMirror's coordsChar returns a position even if it is outside the bounds. Nullify the position
1583 // if the event is outside the bounds of the gutter so we will remove the breakpoint.
1584 var gutterBounds = this._codeMirror.getGutterElement().getBoundingClientRect();
1585 if (event.pageX < gutterBounds.left || event.pageX > gutterBounds.right || event.pageY < gutterBounds.top || event.pageY > gutterBounds.bottom)
1588 // If we have a position and it has a line then use it.
1589 if (position && "line" in position)
1590 lineNumber = position.line;
1592 // The _lineNumberWithDraggedBreakpoint property can be undefined if the user drags
1593 // outside of the gutter. The lineNumber variable can be undefined for the same reason.
1595 if (lineNumber === this._lineNumberWithDraggedBreakpoint)
1598 // Record that the mouse dragged some so when mouse up fires we know to do the
1599 // work of removing and moving the breakpoint.
1600 this._mouseDragged = true;
1602 if ("_lineNumberWithDraggedBreakpoint" in this) {
1603 // We have a line that is currently showing the dragged breakpoint. Remove that breakpoint
1604 // and restore the previous one (if any.)
1605 if (this._previousColumnBreakpointInfo)
1606 this._setColumnBreakpointInfoForLine(this._lineNumberWithDraggedBreakpoint, this._previousColumnBreakpointInfo);
1608 this._removeBreakpointFromLineAndColumn(this._lineNumberWithDraggedBreakpoint, this._columnNumberWithDraggedBreakpoint);
1610 delete this._previousColumnBreakpointInfo;
1611 delete this._lineNumberWithDraggedBreakpoint;
1612 delete this._columnNumberWithDraggedBreakpoint;
1615 if (lineNumber !== undefined) {
1616 // We have a new line that will now show the dragged breakpoint.
1617 var newColumnBreakpoints = {};
1618 var columnNumber = lineNumber === this._lineNumberWithMousedDownBreakpoint ? this._columnNumberWithDraggedBreakpoint : 0;
1619 newColumnBreakpoints[columnNumber] = this._draggingBreakpointInfo;
1620 this._previousColumnBreakpointInfo = this._allColumnBreakpointInfoForLine(lineNumber);
1621 this._setColumnBreakpointInfoForLine(lineNumber, newColumnBreakpoints);
1622 this._lineNumberWithDraggedBreakpoint = lineNumber;
1623 this._columnNumberWithDraggedBreakpoint = columnNumber;
1627 _documentMouseUp(event)
1629 console.assert("_lineNumberWithMousedDownBreakpoint" in this);
1630 if (!("_lineNumberWithMousedDownBreakpoint" in this))
1633 event.preventDefault();
1635 document.removeEventListener("mousemove", this._documentMouseMovedEventListener, true);
1636 document.removeEventListener("mouseup", this._documentMouseUpEventListener, true);
1638 var delegateImplementsBreakpointClicked = this._delegate && typeof this._delegate.textEditorBreakpointClicked === "function";
1639 var delegateImplementsBreakpointRemoved = this._delegate && typeof this._delegate.textEditorBreakpointRemoved === "function";
1640 var delegateImplementsBreakpointMoved = this._delegate && typeof this._delegate.textEditorBreakpointMoved === "function";
1642 if (this._mouseDragged) {
1643 if (!("_lineNumberWithDraggedBreakpoint" in this)) {
1644 // The breakpoint was dragged off the gutter, remove it.
1645 if (delegateImplementsBreakpointRemoved) {
1646 this._ignoreSetBreakpointInfoCalls = true;
1647 this._delegate.textEditorBreakpointRemoved(this, this._lineNumberWithMousedDownBreakpoint, this._columnNumberWithMousedDownBreakpoint);
1648 delete this._ignoreSetBreakpointInfoCalls;
1650 } else if (this._lineNumberWithMousedDownBreakpoint !== this._lineNumberWithDraggedBreakpoint) {
1651 // The dragged breakpoint was moved to a new line.
1653 // If there is are breakpoints already at the drop line, tell the delegate to remove them.
1654 // We have already updated the breakpoint info internally, so when the delegate removes the breakpoints
1655 // and tells us to clear the breakpoint info, we can ignore those calls.
1656 if (this._previousColumnBreakpointInfo && delegateImplementsBreakpointRemoved) {
1657 this._ignoreSetBreakpointInfoCalls = true;
1658 for (var columnNumber in this._previousColumnBreakpointInfo)
1659 this._delegate.textEditorBreakpointRemoved(this, this._lineNumberWithDraggedBreakpoint, columnNumber);
1660 delete this._ignoreSetBreakpointInfoCalls;
1663 // Tell the delegate to move the breakpoint from one line to another.
1664 if (delegateImplementsBreakpointMoved) {
1665 this._ignoreSetBreakpointInfoCalls = true;
1666 this._delegate.textEditorBreakpointMoved(this, this._lineNumberWithMousedDownBreakpoint, this._columnNumberWithMousedDownBreakpoint, this._lineNumberWithDraggedBreakpoint, this._columnNumberWithDraggedBreakpoint);
1667 delete this._ignoreSetBreakpointInfoCalls;
1671 // Toggle the disabled state of the breakpoint.
1672 console.assert(this._lineNumberWithMousedDownBreakpoint in this._breakpoints);
1673 console.assert(this._columnNumberWithMousedDownBreakpoint in this._breakpoints[this._lineNumberWithMousedDownBreakpoint]);
1674 if (this._lineNumberWithMousedDownBreakpoint in this._breakpoints && this._columnNumberWithMousedDownBreakpoint in this._breakpoints[this._lineNumberWithMousedDownBreakpoint] && delegateImplementsBreakpointClicked)
1675 this._delegate.textEditorBreakpointClicked(this, this._lineNumberWithMousedDownBreakpoint, this._columnNumberWithMousedDownBreakpoint);
1678 delete this._documentMouseMovedEventListener;
1679 delete this._documentMouseUpEventListener;
1680 delete this._lineNumberWithMousedDownBreakpoint;
1681 delete this._lineNumberWithDraggedBreakpoint;
1682 delete this._columnNumberWithMousedDownBreakpoint;
1683 delete this._columnNumberWithDraggedBreakpoint;
1684 delete this._previousColumnBreakpointInfo;
1685 delete this._mouseDragged;
1688 _openClickedLinks(event)
1690 // Get the position in the text and the token at that position.
1691 var position = this._codeMirror.coordsChar({left: event.pageX, top: event.pageY});
1692 var tokenInfo = this._codeMirror.getTokenAt(position);
1693 if (!tokenInfo || !tokenInfo.type || !tokenInfo.string)
1696 // If the token is not a link, then ignore it.
1697 if (!/\blink\b/.test(tokenInfo.type))
1700 // The token string is the URL we should open. It might be a relative URL.
1701 var url = tokenInfo.string;
1703 // Get the base URL.
1705 if (this._delegate && typeof this._delegate.textEditorBaseURL === "function")
1706 baseURL = this._delegate.textEditorBaseURL(this);
1708 // Open the link after resolving the absolute URL from the base URL.
1709 WI.openURL(absoluteURL(url, baseURL));
1711 // Stop processing the event.
1712 event.preventDefault();
1713 event.stopPropagation();
1716 _isPositionVisible(position)
1718 var scrollInfo = this._codeMirror.getScrollInfo();
1719 var visibleRangeStart = scrollInfo.top;
1720 var visibleRangeEnd = visibleRangeStart + scrollInfo.clientHeight;
1721 var coords = this._codeMirror.charCoords(position, "local");
1723 return coords.top >= visibleRangeStart && coords.bottom <= visibleRangeEnd;
1726 _scrollIntoViewCentered(position)
1728 var scrollInfo = this._codeMirror.getScrollInfo();
1729 var lineHeight = Math.ceil(this._codeMirror.defaultTextHeight());
1730 var margin = Math.floor((scrollInfo.clientHeight - lineHeight) / 2);
1731 this._codeMirror.scrollIntoView(position, margin);
1735 WI.TextEditor.HighlightedStyleClassName = "highlighted";
1736 WI.TextEditor.SearchResultStyleClassName = "search-result";
1737 WI.TextEditor.HasBreakpointStyleClassName = "has-breakpoint";
1738 WI.TextEditor.BreakpointResolvedStyleClassName = "breakpoint-resolved";
1739 WI.TextEditor.BreakpointAutoContinueStyleClassName = "breakpoint-auto-continue";
1740 WI.TextEditor.BreakpointDisabledStyleClassName = "breakpoint-disabled";
1741 WI.TextEditor.MultipleBreakpointsStyleClassName = "multiple-breakpoints";
1742 WI.TextEditor.ExecutionLineStyleClassName = "execution-line";
1743 WI.TextEditor.BouncyHighlightStyleClassName = "bouncy-highlight";
1744 WI.TextEditor.NumberOfFindsPerSearchBatch = 10;
1745 WI.TextEditor.HighlightAnimationDuration = 2000;
1747 WI.TextEditor.Event = {
1748 Focused: "text-editor-focused",
1749 ExecutionLineNumberDidChange: "text-editor-execution-line-number-did-change",
1750 NumberOfSearchResultsDidChange: "text-editor-number-of-search-results-did-change",
1751 ContentDidChange: "text-editor-content-did-change",
1752 FormattingDidChange: "text-editor-formatting-did-change",
1753 MIMETypeChanged: "text-editor-mime-type-changed",