Web Inspector: Remove unnecessary promise rejection handlers now that we use the...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / TextEditor.js
1 /*
2  * Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
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.
12  *
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.
24  */
25
26 WI.TextEditor = class TextEditor extends WI.View
27 {
28     constructor(element, mimeType, delegate)
29     {
30         super(element);
31
32         this.element.classList.add("text-editor", WI.SyntaxHighlightedStyleClassName);
33
34         this._codeMirror = WI.CodeMirrorEditor.create(this.element, {
35             readOnly: true,
36             indentWithTabs: WI.settings.indentWithTabs.value,
37             indentUnit: WI.settings.indentUnit.value,
38             tabSize: WI.settings.tabSize.value,
39             lineNumbers: true,
40             lineWrapping: WI.settings.enableLineWrapping.value,
41             matchBrackets: true,
42             autoCloseBrackets: true,
43             showWhitespaceCharacters: WI.settings.showWhitespaceCharacters.value,
44             styleSelectedText: true,
45         });
46
47         WI.settings.indentWithTabs.addEventListener(WI.Setting.Event.Changed, (event) => {
48             this._codeMirror.setOption("indentWithTabs", WI.settings.indentWithTabs.value);
49         });
50
51         WI.settings.indentUnit.addEventListener(WI.Setting.Event.Changed, (event) => {
52             this._codeMirror.setOption("indentUnit", WI.settings.indentUnit.value);
53         });
54
55         WI.settings.tabSize.addEventListener(WI.Setting.Event.Changed, (event) => {
56             this._codeMirror.setOption("tabSize", WI.settings.tabSize.value);
57         });
58
59         WI.settings.enableLineWrapping.addEventListener(WI.Setting.Event.Changed, (event) => {
60             this._codeMirror.setOption("lineWrapping", WI.settings.enableLineWrapping.value);
61         });
62
63         WI.settings.showWhitespaceCharacters.addEventListener(WI.Setting.Event.Changed, (event) => {
64             this._codeMirror.setOption("showWhitespaceCharacters", WI.settings.showWhitespaceCharacters.value);
65         });
66
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);
72
73         this._completionController = new WI.CodeMirrorCompletionController(this._codeMirror, this);
74         this._tokenTrackingController = new WI.CodeMirrorTokenTrackingController(this._codeMirror, this);
75
76         this._initialStringNotSet = true;
77
78         this.mimeType = mimeType;
79
80         this._breakpoints = {};
81         this._executionLineNumber = NaN;
82         this._executionColumnNumber = NaN;
83
84         this._executionLineHandle = null;
85         this._executionMultilineHandles = [];
86         this._executionRangeHighlightMarker = null;
87
88         this._searchQuery = null;
89         this._searchResults = [];
90         this._currentSearchResultIndex = -1;
91         this._ignoreCodeMirrorContentDidChangeEvent = 0;
92
93         this._formatted = false;
94         this._formattingPromise = null;
95         this._formatterSourceMap = null;
96         this._deferReveal = false;
97         this._repeatReveal = false;
98
99         this._delegate = delegate || null;
100     }
101
102     // Public
103
104     get visible()
105     {
106         return this._visible;
107     }
108
109     get string()
110     {
111         return this._codeMirror.getValue();
112     }
113
114     set string(newString)
115     {
116         let previousSelectedTextRange = this._repeatReveal ? this.selectedTextRange : null;
117
118         function update()
119         {
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");
123
124             if (this._codeMirror.getValue() !== newString)
125                 this._codeMirror.setValue(newString);
126             else {
127                 // Ensure we at display content even if the value did not change. This often happens when auto formatting.
128                 this.layout();
129             }
130
131             if (this._initialStringNotSet) {
132                 this._codeMirror.clearHistory();
133                 this._codeMirror.markClean();
134
135                 this._initialStringNotSet = false;
136
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);
142                 }
143
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();
149                 }
150             }
151
152             // Update the execution line now that we might have content for that line.
153             this._updateExecutionLine();
154             this._updateExecutionRangeHighlight();
155
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);
159
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);
166             }
167         }
168
169         this._ignoreCodeMirrorContentDidChangeEvent++;
170         this._codeMirror.operation(update.bind(this));
171         this._ignoreCodeMirrorContentDidChangeEvent--;
172         console.assert(this._ignoreCodeMirrorContentDidChangeEvent >= 0);
173     }
174
175     get readOnly()
176     {
177         return this._codeMirror.getOption("readOnly") || false;
178     }
179
180     set readOnly(readOnly)
181     {
182         this._codeMirror.setOption("readOnly", readOnly);
183     }
184
185     get formatted()
186     {
187         return this._formatted;
188     }
189
190     get hasModified()
191     {
192         let historySize = this._codeMirror.historySize().undo;
193
194         // Formatting code creates a history item.
195         if (this._formatted)
196             historySize--;
197
198         return historySize > 0;
199     }
200
201     updateFormattedState(formatted)
202     {
203         return this._format(formatted);
204     }
205
206     hasFormatter()
207     {
208         let mode = this._codeMirror.getMode().name;
209         return mode === "javascript" || mode === "css";
210     }
211
212     canBeFormatted()
213     {
214         // Can be overridden by subclasses.
215         return this.hasFormatter();
216     }
217
218     canShowTypeAnnotations()
219     {
220         return false;
221     }
222
223     canShowCoverageHints()
224     {
225         return false;
226     }
227
228     get selectedTextRange()
229     {
230         var start = this._codeMirror.getCursor(true);
231         var end = this._codeMirror.getCursor(false);
232         return this._textRangeFromCodeMirrorPosition(start, end);
233     }
234
235     set selectedTextRange(textRange)
236     {
237         if (document.activeElement === document.body)
238             this.focus();
239
240         var position = this._codeMirrorPositionFromTextRange(textRange);
241         this._codeMirror.setSelection(position.start, position.end);
242     }
243
244     get mimeType()
245     {
246         return this._mimeType;
247     }
248
249     set mimeType(newMIMEType)
250     {
251         newMIMEType = parseMIMEType(newMIMEType).type;
252
253         this._mimeType = newMIMEType;
254         this._codeMirror.setOption("mode", {name: newMIMEType, globalVars: true});
255
256         this.dispatchEventToListeners(WI.TextEditor.Event.MIMETypeChanged);
257     }
258
259     get executionLineNumber()
260     {
261         return this._executionLineNumber;
262     }
263
264     get executionColumnNumber()
265     {
266         return this._executionColumnNumber;
267     }
268
269     get formatterSourceMap()
270     {
271         return this._formatterSourceMap;
272     }
273
274     get tokenTrackingController()
275     {
276         return this._tokenTrackingController;
277     }
278
279     get delegate()
280     {
281         return this._delegate;
282     }
283
284     set delegate(newDelegate)
285     {
286         this._delegate = newDelegate || null;
287     }
288
289     get numberOfSearchResults()
290     {
291         return this._searchResults.length;
292     }
293
294     get currentSearchQuery()
295     {
296         return this._searchQuery;
297     }
298
299     set automaticallyRevealFirstSearchResult(reveal)
300     {
301         this._automaticallyRevealFirstSearchResult = reveal;
302
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();
307         }
308     }
309
310     set deferReveal(defer)
311     {
312         this._deferReveal = defer;
313     }
314
315     set repeatReveal(repeat)
316     {
317         this._repeatReveal = repeat;
318     }
319
320     performSearch(query)
321     {
322         if (this._searchQuery === query)
323             return;
324
325         this.searchCleared();
326
327         this._searchQuery = query;
328
329         // Defer until the initial string is set.
330         if (this._initialStringNotSet)
331             return;
332
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))
337                 return;
338         }
339
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;
345
346         function reportNumberOfSearchResultsDidChange()
347         {
348             if (numberOfSearchResultsDidChangeTimeout) {
349                 clearTimeout(numberOfSearchResultsDidChangeTimeout);
350                 numberOfSearchResultsDidChangeTimeout = null;
351             }
352
353             this.dispatchEventToListeners(WI.TextEditor.Event.NumberOfSearchResultsDidChange);
354         }
355
356         function batchSearch()
357         {
358             // Bail if the query changed since we started.
359             if (this._searchQuery !== query)
360                 return;
361
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);
367             }
368
369             this.addSearchResults(newSearchResults);
370
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);
374
375             if (foundResult) {
376                 // More lines to search, set a timeout so we don't block the UI long.
377                 setTimeout(boundBatchSearch, 50);
378             } else {
379                 // Report immediately now that we are finished, canceling any pending update.
380                 reportNumberOfSearchResultsDidChange.call(this);
381             }
382         }
383
384         // Start the search.
385         boundBatchSearch();
386     }
387
388     setExecutionLineAndColumn(lineNumber, columnNumber)
389     {
390         // Only return early if there isn't a line handle and that isn't changing.
391         if (!this._executionLineHandle && isNaN(lineNumber))
392             return;
393
394         this._executionLineNumber = lineNumber;
395         this._executionColumnNumber = columnNumber;
396
397         if (!this._initialStringNotSet) {
398             this._updateExecutionLine();
399             this._updateExecutionRangeHighlight();
400         }
401
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);
405     }
406
407     addSearchResults(textRanges)
408     {
409         console.assert(textRanges);
410         if (!textRanges || !textRanges.length)
411             return;
412
413         function markRanges()
414         {
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);
419             }
420
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();
425             }
426         }
427
428         this._codeMirror.operation(markRanges.bind(this));
429     }
430
431     searchCleared()
432     {
433         this._codeMirror.operation(() => {
434             for (let searchResult of this._searchResults)
435                 searchResult.clear();
436         });
437
438         this._searchQuery = null;
439         this._searchResults = [];
440         this._currentSearchResultIndex = -1;
441     }
442
443     searchQueryWithSelection()
444     {
445         if (!this._codeMirror.somethingSelected())
446             return null;
447
448         return this._codeMirror.getSelection();
449     }
450
451     revealPreviousSearchResult(changeFocus)
452     {
453         if (!this._searchResults.length)
454             return;
455
456         if (this._currentSearchResultIndex === -1 || this._cursorDoesNotMatchLastRevealedSearchResult()) {
457             this._revealFirstSearchResultBeforeCursor(changeFocus);
458             return;
459         }
460
461         if (this._currentSearchResultIndex > 0)
462             --this._currentSearchResultIndex;
463         else
464             this._currentSearchResultIndex = this._searchResults.length - 1;
465
466         this._revealSearchResult(this._searchResults[this._currentSearchResultIndex], changeFocus, -1);
467     }
468
469     revealNextSearchResult(changeFocus)
470     {
471         if (!this._searchResults.length)
472             return;
473
474         if (this._currentSearchResultIndex === -1 || this._cursorDoesNotMatchLastRevealedSearchResult()) {
475             this._revealFirstSearchResultAfterCursor(changeFocus);
476             return;
477         }
478
479         if (this._currentSearchResultIndex + 1 < this._searchResults.length)
480             ++this._currentSearchResultIndex;
481         else
482             this._currentSearchResultIndex = 0;
483
484         this._revealSearchResult(this._searchResults[this._currentSearchResultIndex], changeFocus, 1);
485     }
486
487     line(lineNumber)
488     {
489         return this._codeMirror.getLine(lineNumber);
490     }
491
492     getTextInRange(startPosition, endPosition)
493     {
494         return this._codeMirror.getRange(startPosition.toCodeMirror(), endPosition.toCodeMirror());
495     }
496
497     addStyleToTextRange(startPosition, endPosition, styleClassName)
498     {
499         endPosition = endPosition.offsetColumn(1);
500         return this._codeMirror.getDoc().markText(startPosition.toCodeMirror(), endPosition.toCodeMirror(), {className: styleClassName, inclusiveLeft: true, inclusiveRight: true});
501     }
502
503     revealPosition(position, textRangeToSelect, forceUnformatted, noHighlight)
504     {
505         console.assert(position === undefined || position instanceof WI.SourceCodePosition, "revealPosition called without a SourceCodePosition");
506         if (!(position instanceof WI.SourceCodePosition))
507             return;
508
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;
514             return;
515         }
516
517         // Delete now that the reveal is happening.
518         delete this._positionToReveal;
519         delete this._textRangeToSelect;
520         delete this._forceUnformatted;
521
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);
527             });
528             return;
529         }
530
531         let line = Number.constrain(position.lineNumber, 0, this._codeMirror.lineCount() - 1);
532         let lineHandle = this._codeMirror.getLineHandle(line);
533
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);
537         }
538
539         function removeStyleClass()
540         {
541             this._codeMirror.removeLineClass(lineHandle, "wrap", WI.TextEditor.HighlightedStyleClassName);
542         }
543
544         function revealAndHighlightLine()
545         {
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);
550
551             this.selectedTextRange = textRangeToSelect;
552
553             if (noHighlight)
554                 return;
555
556             // Avoid highlighting the execution line while debugging.
557             if (WI.debuggerManager.paused && line === this._executionLineNumber)
558                 return;
559
560             this._codeMirror.addLineClass(lineHandle, "wrap", WI.TextEditor.HighlightedStyleClassName);
561
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);
566         }
567
568         this._codeMirror.operation(revealAndHighlightLine.bind(this));
569     }
570
571     shown()
572     {
573         this._visible = true;
574
575         // Refresh since our size might have changed.
576         this._codeMirror.refresh();
577
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();
582     }
583
584     hidden()
585     {
586         this._visible = false;
587     }
588
589     close()
590     {
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);
596     }
597
598     setBreakpointInfoForLineAndColumn(lineNumber, columnNumber, breakpointInfo)
599     {
600         if (this._ignoreSetBreakpointInfoCalls)
601             return;
602
603         if (breakpointInfo)
604             this._addBreakpointToLineAndColumnWithInfo(lineNumber, columnNumber, breakpointInfo);
605         else
606             this._removeBreakpointFromLineAndColumn(lineNumber, columnNumber);
607     }
608
609     updateBreakpointLineAndColumn(oldLineNumber, oldColumnNumber, newLineNumber, newColumnNumber)
610     {
611         console.assert(this._breakpoints[oldLineNumber]);
612         if (!this._breakpoints[oldLineNumber])
613             return;
614
615         console.assert(this._breakpoints[oldLineNumber][oldColumnNumber]);
616         if (!this._breakpoints[oldLineNumber][oldColumnNumber])
617             return;
618
619         var breakpointInfo = this._breakpoints[oldLineNumber][oldColumnNumber];
620         this._removeBreakpointFromLineAndColumn(oldLineNumber, oldColumnNumber);
621         this._addBreakpointToLineAndColumnWithInfo(newLineNumber, newColumnNumber, breakpointInfo);
622     }
623
624     addStyleClassToLine(lineNumber, styleClassName)
625     {
626         var lineHandle = this._codeMirror.getLineHandle(lineNumber);
627         if (!lineHandle)
628             return null;
629
630         return this._codeMirror.addLineClass(lineHandle, "wrap", styleClassName);
631     }
632
633     removeStyleClassFromLine(lineNumber, styleClassName)
634     {
635         var lineHandle = this._codeMirror.getLineHandle(lineNumber);
636         console.assert(lineHandle);
637         if (!lineHandle)
638             return null;
639
640         return this._codeMirror.removeLineClass(lineHandle, "wrap", styleClassName);
641     }
642
643     toggleStyleClassForLine(lineNumber, styleClassName)
644     {
645         var lineHandle = this._codeMirror.getLineHandle(lineNumber);
646         console.assert(lineHandle);
647         if (!lineHandle)
648             return false;
649
650         return this._codeMirror.toggleLineClass(lineHandle, "wrap", styleClassName);
651     }
652
653     createWidgetForLine(lineNumber)
654     {
655         var lineHandle = this._codeMirror.getLineHandle(lineNumber);
656         if (!lineHandle)
657             return null;
658
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);
662     }
663
664     get lineCount()
665     {
666         return this._codeMirror.lineCount();
667     }
668
669     focus()
670     {
671         this._codeMirror.focus();
672     }
673
674     contentDidChange(replacedRanges, newRanges)
675     {
676         // Implemented by subclasses.
677     }
678
679     rectsForRange(range)
680     {
681         return this._codeMirror.rectsForRange(range);
682     }
683
684     get markers()
685     {
686         return this._codeMirror.getAllMarks().map(WI.TextMarker.textMarkerForCodeMirrorTextMarker);
687     }
688
689     markersAtPosition(position)
690     {
691         return this._codeMirror.findMarksAt(position).map(WI.TextMarker.textMarkerForCodeMirrorTextMarker);
692     }
693
694     createColorMarkers(range)
695     {
696         return createCodeMirrorColorTextMarkers(this._codeMirror, range);
697     }
698
699     createGradientMarkers(range)
700     {
701         return createCodeMirrorGradientTextMarkers(this._codeMirror, range);
702     }
703
704     createCubicBezierMarkers(range)
705     {
706         return createCodeMirrorCubicBezierTextMarkers(this._codeMirror, range);
707     }
708
709     createSpringMarkers(range)
710     {
711         return createCodeMirrorSpringTextMarkers(this._codeMirror, range);
712     }
713
714     editingControllerForMarker(editableMarker)
715     {
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);
725         default:
726             return new WI.CodeMirrorEditingController(this._codeMirror, editableMarker);
727         }
728     }
729
730     visibleRangeOffsets()
731     {
732         var startOffset = null;
733         var endOffset = null;
734         var visibleRange = this._codeMirror.getViewport();
735
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);
739         } else {
740             startOffset = this._codeMirror.getDoc().indexFromPos({line: visibleRange.from, ch: 0});
741             endOffset = this._codeMirror.getDoc().indexFromPos({line: visibleRange.to, ch: 0});
742         }
743
744         return {startOffset, endOffset};
745     }
746
747     visibleRangePositions()
748     {
749         let visibleRange = this._codeMirror.getViewport();
750         let startLine;
751         let endLine;
752
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;
756         } else {
757             startLine = visibleRange.from;
758             endLine = visibleRange.to;
759         }
760
761         return {
762             startPosition: new WI.SourceCodePosition(startLine, 0),
763             endPosition: new WI.SourceCodePosition(endLine, 0)
764         };
765     }
766
767     originalPositionToCurrentPosition(position)
768     {
769         if (!this._formatterSourceMap)
770             return position;
771
772         let {lineNumber, columnNumber} = this._formatterSourceMap.originalToFormatted(position.lineNumber, position.columnNumber);
773         return new WI.SourceCodePosition(lineNumber, columnNumber);
774     }
775
776     originalOffsetToCurrentPosition(offset)
777     {
778         var position = null;
779         if (this._formatterSourceMap) {
780             var location = this._formatterSourceMap.originalPositionToFormatted(offset);
781             position = {line: location.lineNumber, ch: location.columnNumber};
782         } else
783             position = this._codeMirror.getDoc().posFromIndex(offset);
784
785         return position;
786     }
787
788     currentOffsetToCurrentPosition(offset)
789     {
790         let pos = this._codeMirror.getDoc().posFromIndex(offset);
791         return new WI.SourceCodePosition(pos.line, pos.ch);
792     }
793
794     currentPositionToOriginalOffset(position)
795     {
796         let offset = null;
797
798         if (this._formatterSourceMap)
799             offset = this._formatterSourceMap.formattedToOriginalOffset(position.line, position.ch);
800         else
801             offset = this._codeMirror.getDoc().indexFromPos(position);
802
803         return offset;
804     }
805
806     currentPositionToOriginalPosition(position)
807     {
808         if (!this._formatterSourceMap)
809             return position;
810
811         let location = this._formatterSourceMap.formattedToOriginal(position.lineNumber, position.columnNumber);
812         return new WI.SourceCodePosition(location.lineNumber, location.columnNumber);
813     }
814
815     currentPositionToCurrentOffset(position)
816     {
817         return this._codeMirror.getDoc().indexFromPos(position.toCodeMirror());
818     }
819
820     setInlineWidget(position, inlineElement)
821     {
822         return this._codeMirror.setUniqueBookmark(position.toCodeMirror(), {widget: inlineElement});
823     }
824
825     addScrollHandler(handler)
826     {
827         this._codeMirror.on("scroll", handler);
828     }
829
830     removeScrollHandler(handler)
831     {
832         this._codeMirror.off("scroll", handler);
833     }
834
835     // Protected
836
837     layout()
838     {
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.
842
843         // FIXME: visible check can be removed once <https://webkit.org/b/150741> is fixed.
844         if (this._visible)
845             this._codeMirror.refresh();
846     }
847
848     _format(formatted)
849     {
850         if (this._formatted === formatted)
851             return Promise.resolve(this._formatted);
852
853         console.assert(!formatted || this.canBeFormatted());
854         if (formatted && !this.canBeFormatted())
855             return Promise.resolve(this._formatted);
856
857         if (this._formattingPromise)
858             return this._formattingPromise;
859
860         this._ignoreCodeMirrorContentDidChangeEvent++;
861         this._formattingPromise = this.prettyPrint(formatted).then(() => {
862             this._ignoreCodeMirrorContentDidChangeEvent--;
863             console.assert(this._ignoreCodeMirrorContentDidChangeEvent >= 0);
864
865             this._formattingPromise = null;
866
867             let originalFormatted = this._formatted;
868             this._formatted = !!this._formatterSourceMap;
869
870             if (this._formatted !== originalFormatted)
871                 this.dispatchEventToListeners(WI.TextEditor.Event.FormattingDidChange);
872
873             return this._formatted;
874         });
875
876         return this._formattingPromise;
877     }
878
879     prettyPrint(pretty)
880     {
881         return new Promise((resolve, reject) => {
882             let beforePrettyPrintState = {
883                 selectionAnchor: this._codeMirror.getCursor("anchor"),
884                 selectionHead: this._codeMirror.getCursor("head"),
885             };
886
887             if (!pretty)
888                 this._undoFormatting(beforePrettyPrintState, resolve);
889             else if (this._canUseFormatterWorker())
890                 this._startWorkerPrettyPrint(beforePrettyPrintState, resolve);
891             else
892                 this._startCodeMirrorPrettyPrint(beforePrettyPrintState, resolve);
893         });
894     }
895
896     _canUseFormatterWorker()
897     {
898         return this._codeMirror.getMode().name === "javascript";
899     }
900
901     _attemptToDetermineMIMEType()
902     {
903         let startTime = Date.now();
904
905         const isModule = false;
906         const includeSourceMapData = false;
907         let workerProxy = WI.FormatterWorkerProxy.singleton();
908         workerProxy.formatJavaScript(this.string, isModule, WI.indentString(), includeSourceMapData, ({formattedText}) => {
909             if (!formattedText)
910                 return;
911
912             this.mimeType = "text/javascript";
913
914             if (Date.now() - startTime < 100)
915                 this.updateFormattedState(true);
916         });
917     }
918
919     _startWorkerPrettyPrint(beforePrettyPrintState, callback)
920     {
921         let sourceText = this._codeMirror.getValue();
922         let indentString = WI.indentString();
923         const includeSourceMapData = true;
924
925         let sourceType = this._delegate ? this._delegate.textEditorScriptSourceType(this) : WI.Script.SourceType.Program;
926         const isModule = sourceType === WI.Script.SourceType.Module;
927
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) {
932                 callback();
933                 return;
934             }
935             this._finishPrettyPrint(beforePrettyPrintState, formattedText, sourceMapData, callback);
936         });
937     }
938
939     _startCodeMirrorPrettyPrint(beforePrettyPrintState, callback)
940     {
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);
947
948         let formattedText = builder.formattedContent;
949         let sourceMapData = builder.sourceMapData;
950         this._finishPrettyPrint(beforePrettyPrintState, formattedText, sourceMapData, callback);
951     }
952
953     _finishPrettyPrint(beforePrettyPrintState, formattedText, sourceMapData, callback)
954     {
955         this._codeMirror.operation(() => {
956             this._formatterSourceMap = WI.FormatterSourceMap.fromSourceMapData(sourceMapData);
957             this._codeMirror.setValue(formattedText);
958             this._updateAfterFormatting(true, beforePrettyPrintState);
959         });
960
961         callback();
962     }
963
964     _undoFormatting(beforePrettyPrintState, callback)
965     {
966         this._codeMirror.operation(() => {
967             this._codeMirror.undo();
968             this._updateAfterFormatting(false, beforePrettyPrintState);
969         });
970
971         callback();
972     }
973
974     _updateAfterFormatting(pretty, beforePrettyPrintState)
975     {
976         let oldSelectionAnchor = beforePrettyPrintState.selectionAnchor;
977         let oldSelectionHead = beforePrettyPrintState.selectionHead;
978         let newSelectionAnchor, newSelectionHead;
979         let newExecutionLocation = null;
980
981         if (pretty) {
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);
985             }
986
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);
991             }
992
993             if (!isNaN(this._executionLineNumber)) {
994                 console.assert(!isNaN(this._executionColumnNumber));
995                 newExecutionLocation = this._formatterSourceMap.originalToFormatted(this._executionLineNumber, this._executionColumnNumber);
996             }
997
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};
1002         } else {
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);
1006             }
1007
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);
1012             }
1013
1014             if (!isNaN(this._executionLineNumber)) {
1015                 console.assert(!isNaN(this._executionColumnNumber));
1016                 newExecutionLocation = this._formatterSourceMap.formattedToOriginal(this._executionLineNumber, this._executionColumnNumber);
1017             }
1018
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};
1023
1024             this._formatterSourceMap = null;
1025         }
1026
1027         this._scrollIntoViewCentered(newSelectionAnchor);
1028         this._codeMirror.setSelection(newSelectionAnchor, newSelectionHead);
1029
1030         if (newExecutionLocation) {
1031             this._executionLineHandle = null;
1032             this._executionMultilineHandles = [];
1033             this.setExecutionLineAndColumn(newExecutionLocation.lineNumber, newExecutionLocation.columnNumber);
1034         }
1035
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);
1043         }
1044
1045         if (this._delegate && typeof this._delegate.textEditorUpdatedFormatting === "function")
1046             this._delegate.textEditorUpdatedFormatting(this);
1047     }
1048
1049     // Private
1050
1051     hasEdits()
1052     {
1053         return !this._codeMirror.isClean();
1054     }
1055
1056     _editorFocused(codeMirror)
1057     {
1058         this.dispatchEventToListeners(WI.TextEditor.Event.Focused);
1059     }
1060
1061     _contentChanged(codeMirror, change)
1062     {
1063         if (this._ignoreCodeMirrorContentDidChangeEvent > 0)
1064             return;
1065
1066         var replacedRanges = [];
1067         var newRanges = [];
1068         while (change) {
1069             replacedRanges.push(new WI.TextRange(
1070                 change.from.line,
1071                 change.from.ch,
1072                 change.to.line,
1073                 change.to.ch
1074             ));
1075             newRanges.push(new WI.TextRange(
1076                 change.from.line,
1077                 change.from.ch,
1078                 change.from.line + change.text.length - 1,
1079                 change.text.length === 1 ? change.from.ch + change.text[0].length : change.text.lastValue.length
1080             ));
1081             change = change.next;
1082         }
1083         this.contentDidChange(replacedRanges, newRanges);
1084
1085         if (this._formatted) {
1086             this._formatterSourceMap = null;
1087             this._formatted = false;
1088
1089             if (this._delegate && typeof this._delegate.textEditorUpdatedFormatting === "function")
1090                 this._delegate.textEditorUpdatedFormatting(this);
1091
1092             this.dispatchEventToListeners(WI.TextEditor.Event.FormattingDidChange);
1093         }
1094
1095         this.dispatchEventToListeners(WI.TextEditor.Event.ContentDidChange);
1096     }
1097
1098     _textRangeFromCodeMirrorPosition(start, end)
1099     {
1100         console.assert(start);
1101         console.assert(end);
1102
1103         return new WI.TextRange(start.line, start.ch, end.line, end.ch);
1104     }
1105
1106     _codeMirrorPositionFromTextRange(textRange)
1107     {
1108         console.assert(textRange);
1109
1110         var start = {line: textRange.startLine, ch: textRange.startColumn};
1111         var end = {line: textRange.endLine, ch: textRange.endColumn};
1112         return {start, end};
1113     }
1114
1115     _revealPendingPositionIfPossible()
1116     {
1117         // Nothing to do if we don't have a pending position.
1118         if (!this._positionToReveal)
1119             return;
1120
1121         // Don't try to reveal unless we are visible.
1122         if (!this._visible)
1123             return;
1124
1125         this.revealPosition(this._positionToReveal, this._textRangeToSelect, this._forceUnformatted);
1126     }
1127
1128     _revealSearchResult(result, changeFocus, directionInCaseOfRevalidation)
1129     {
1130         let position = result.find();
1131
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.
1134         if (!position) {
1135             this._revalidateSearchResults(directionInCaseOfRevalidation);
1136             return;
1137         }
1138
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);
1142
1143         // Update the text selection to select the search result.
1144         this.selectedTextRange = this._textRangeFromCodeMirrorPosition(position.from, position.to);
1145
1146         // Remove the automatically reveal state now that we have revealed a search result.
1147         this._automaticallyRevealFirstSearchResult = false;
1148
1149         // Focus the editor if requested.
1150         if (changeFocus)
1151             this._codeMirror.focus();
1152
1153         // Collect info for the bouncy highlight.
1154         let highlightEditorPosition = this._codeMirror.getCursor("start");
1155         let textContent = this._codeMirror.getSelection();
1156
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();
1160
1161         // Create the bouncy highlight.
1162         this._bouncyHighlightElement = document.createElement("div");
1163         this._bouncyHighlightElement.className = WI.TextEditor.BouncyHighlightStyleClassName;
1164         this._bouncyHighlightElement.textContent = textContent;
1165
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;
1172
1173             // Position the bouncy highlight.
1174             this._bouncyHighlightElement.style.top = coordinates.top + "px";
1175             this._bouncyHighlightElement.style.left = coordinates.left + "px";
1176         }
1177
1178         // Position and show the highlight.
1179         positionBouncyHighlight.call(this);
1180         this.element.appendChild(this._bouncyHighlightElement);
1181
1182         // Reposition the highlight if the editor scrolls.
1183         this._bouncyHighlightScrollHandler = () => { positionBouncyHighlight.call(this); };
1184         this.addScrollHandler(this._bouncyHighlightScrollHandler);
1185
1186         // Listen for the end of the animation so we can remove the element.
1187         this._bouncyHighlightElement.addEventListener("animationend", () => {
1188             this._removeBouncyHighlightElementIfNeeded();
1189         });
1190     }
1191
1192     _removeBouncyHighlightElementIfNeeded()
1193     {
1194         if (!this._bouncyHighlightElement)
1195             return;
1196
1197         this.removeScrollHandler(this._bouncyHighlightScrollHandler);
1198         this._bouncyHighlightScrollHandler = null;
1199
1200         this._bouncyHighlightElement.remove();
1201         this._bouncyHighlightElement = null;
1202     }
1203
1204     _binarySearchInsertionIndexInSearchResults(object, comparator)
1205     {
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;
1210
1211         var first = 0;
1212         var last = array.length - 1;
1213
1214         while (first <= last) {
1215             var mid = (first + last) >> 1;
1216             var c = comparator(object, array[mid]);
1217             if (c === null)
1218                 return null;
1219             if (c > 0)
1220                 first = mid + 1;
1221             else if (c < 0)
1222                 last = mid - 1;
1223             else
1224                 return mid;
1225         }
1226
1227         return first - 1;
1228     }
1229
1230     _revealFirstSearchResultBeforeCursor(changeFocus)
1231     {
1232         console.assert(this._searchResults.length);
1233
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);
1238             return;
1239         }
1240
1241         var index = this._binarySearchInsertionIndexInSearchResults(currentCursorPosition, function(current, searchResult) {
1242             var searchResultMarker = searchResult.find();
1243             if (!searchResultMarker)
1244                 return null;
1245             return WI.compareCodeMirrorPositions(current, searchResultMarker.from);
1246         });
1247
1248         if (index === null) {
1249             this._revalidateSearchResults(-1);
1250             return;
1251         }
1252
1253         this._currentSearchResultIndex = index;
1254         this._revealSearchResult(this._searchResults[this._currentSearchResultIndex], changeFocus);
1255     }
1256
1257     _revealFirstSearchResultAfterCursor(changeFocus)
1258     {
1259         console.assert(this._searchResults.length);
1260
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);
1265             return;
1266         }
1267
1268         var index = this._binarySearchInsertionIndexInSearchResults(currentCursorPosition, function(current, searchResult) {
1269             var searchResultMarker = searchResult.find();
1270             if (!searchResultMarker)
1271                 return null;
1272             return WI.compareCodeMirrorPositions(current, searchResultMarker.from);
1273         });
1274
1275         if (index === null) {
1276             this._revalidateSearchResults(1);
1277             return;
1278         }
1279
1280         if (index + 1 < this._searchResults.length)
1281             ++index;
1282         else
1283             index = 0;
1284
1285         this._currentSearchResultIndex = index;
1286         this._revealSearchResult(this._searchResults[this._currentSearchResultIndex], changeFocus);
1287     }
1288
1289     _cursorDoesNotMatchLastRevealedSearchResult()
1290     {
1291         console.assert(this._currentSearchResultIndex !== -1);
1292         console.assert(this._searchResults.length);
1293
1294         var lastRevealedSearchResultMarker = this._searchResults[this._currentSearchResultIndex].find();
1295         if (!lastRevealedSearchResultMarker)
1296             return true;
1297
1298         var currentCursorPosition = this._codeMirror.getCursor("start");
1299         var lastRevealedSearchResultPosition = lastRevealedSearchResultMarker.from;
1300
1301         return WI.compareCodeMirrorPositions(currentCursorPosition, lastRevealedSearchResultPosition) !== 0;
1302     }
1303
1304     _revalidateSearchResults(direction)
1305     {
1306         console.assert(direction !== undefined);
1307
1308         this._currentSearchResultIndex = -1;
1309
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]);
1314         }
1315
1316         console.assert(updatedSearchResults.length !== this._searchResults.length);
1317
1318         this._searchResults = updatedSearchResults;
1319         this.dispatchEventToListeners(WI.TextEditor.Event.NumberOfSearchResultsDidChange);
1320
1321         if (this._searchResults.length) {
1322             if (direction > 0)
1323                 this._revealFirstSearchResultAfterCursor();
1324             else
1325                 this._revealFirstSearchResultBeforeCursor();
1326         }
1327     }
1328
1329     _clearMultilineExecutionLineHighlights()
1330     {
1331         if (this._executionMultilineHandles.length) {
1332             for (let lineHandle of this._executionMultilineHandles)
1333                 this._codeMirror.removeLineClass(lineHandle, "wrap", WI.TextEditor.ExecutionLineStyleClassName);
1334             this._executionMultilineHandles = [];
1335         }
1336     }
1337
1338     _updateExecutionLine()
1339     {
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");
1344             }
1345
1346             this._clearMultilineExecutionLineHighlights();
1347
1348             this._executionLineHandle = !isNaN(this._executionLineNumber) ? this._codeMirror.getLineHandle(this._executionLineNumber) : null;
1349
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);
1354             }
1355         });
1356     }
1357
1358     _updateExecutionRangeHighlight()
1359     {
1360         if (this._executionRangeHighlightMarker) {
1361             this._executionRangeHighlightMarker.clear();
1362             this._executionRangeHighlightMarker = null;
1363         }
1364
1365         if (isNaN(this._executionLineNumber))
1366             return;
1367
1368         let currentPosition = new WI.SourceCodePosition(this._executionLineNumber, this._executionColumnNumber);
1369
1370         this._delegate.textEditorExecutionHighlightRange(currentPosition, (range) => {
1371             let start, end;
1372             if (!range) {
1373                 // Highlight the rest of the line.
1374                 start = {line: this._executionLineNumber, ch: this._executionColumnNumber};
1375                 end = {line: this._executionLineNumber};
1376             } else {
1377                 // Highlight the range.
1378                 start = range.startPosition.toCodeMirror();
1379                 end = range.endPosition.toCodeMirror();
1380             }
1381
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;
1386             }
1387
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);
1393
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);
1401                 }
1402             }
1403
1404             this._executionRangeHighlightMarker = this._codeMirror.markText(start, end, {className: "execution-range-highlight"});
1405         });
1406     }
1407
1408     _setBreakpointStylesOnLine(lineNumber)
1409     {
1410         var columnBreakpoints = this._breakpoints[lineNumber];
1411         console.assert(columnBreakpoints);
1412         if (!columnBreakpoints)
1413             return;
1414
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;
1427         }
1428
1429         allResolved = allResolved && WI.debuggerManager.breakpointsEnabled;
1430
1431         function updateStyles()
1432         {
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);
1436             if (!lineHandle)
1437                 return;
1438
1439             this._codeMirror.addLineClass(lineHandle, "wrap", WI.TextEditor.HasBreakpointStyleClassName);
1440
1441             if (allResolved)
1442                 this._codeMirror.addLineClass(lineHandle, "wrap", WI.TextEditor.BreakpointResolvedStyleClassName);
1443             else
1444                 this._codeMirror.removeLineClass(lineHandle, "wrap", WI.TextEditor.BreakpointResolvedStyleClassName);
1445
1446             if (allDisabled)
1447                 this._codeMirror.addLineClass(lineHandle, "wrap", WI.TextEditor.BreakpointDisabledStyleClassName);
1448             else
1449                 this._codeMirror.removeLineClass(lineHandle, "wrap", WI.TextEditor.BreakpointDisabledStyleClassName);
1450
1451             if (allAutoContinue)
1452                 this._codeMirror.addLineClass(lineHandle, "wrap", WI.TextEditor.BreakpointAutoContinueStyleClassName);
1453             else
1454                 this._codeMirror.removeLineClass(lineHandle, "wrap", WI.TextEditor.BreakpointAutoContinueStyleClassName);
1455
1456             if (multiple)
1457                 this._codeMirror.addLineClass(lineHandle, "wrap", WI.TextEditor.MultipleBreakpointsStyleClassName);
1458             else
1459                 this._codeMirror.removeLineClass(lineHandle, "wrap", WI.TextEditor.MultipleBreakpointsStyleClassName);
1460         }
1461
1462         this._codeMirror.operation(updateStyles.bind(this));
1463     }
1464
1465     _addBreakpointToLineAndColumnWithInfo(lineNumber, columnNumber, breakpointInfo)
1466     {
1467         if (!this._breakpoints[lineNumber])
1468             this._breakpoints[lineNumber] = {};
1469         this._breakpoints[lineNumber][columnNumber] = breakpointInfo;
1470
1471         this._setBreakpointStylesOnLine(lineNumber);
1472     }
1473
1474     _removeBreakpointFromLineAndColumn(lineNumber, columnNumber)
1475     {
1476         console.assert(columnNumber in this._breakpoints[lineNumber]);
1477         delete this._breakpoints[lineNumber][columnNumber];
1478
1479         // There are still breakpoints on the line. Update the breakpoint style.
1480         if (!isEmptyObject(this._breakpoints[lineNumber])) {
1481             this._setBreakpointStylesOnLine(lineNumber);
1482             return;
1483         }
1484
1485         delete this._breakpoints[lineNumber];
1486
1487         function updateStyles()
1488         {
1489             var lineHandle = this._codeMirror.getLineHandle(lineNumber);
1490             if (!lineHandle)
1491                 return;
1492
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);
1498         }
1499
1500         this._codeMirror.operation(updateStyles.bind(this));
1501     }
1502
1503     _allColumnBreakpointInfoForLine(lineNumber)
1504     {
1505         return this._breakpoints[lineNumber];
1506     }
1507
1508     _setColumnBreakpointInfoForLine(lineNumber, columnBreakpointInfo)
1509     {
1510         console.assert(columnBreakpointInfo);
1511         this._breakpoints[lineNumber] = columnBreakpointInfo;
1512         this._setBreakpointStylesOnLine(lineNumber);
1513     }
1514
1515     _gutterMouseDown(codeMirror, lineNumber, gutterElement, event)
1516     {
1517         if (event.button !== 0 || event.ctrlKey)
1518             return;
1519
1520         if (!this._codeMirror.hasLineClass(lineNumber, "wrap", WI.TextEditor.HasBreakpointStyleClassName)) {
1521             console.assert(!(lineNumber in this._breakpoints));
1522
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);
1526                 if (data) {
1527                     var breakpointInfo = data.breakpointInfo;
1528                     if (breakpointInfo)
1529                         this._addBreakpointToLineAndColumnWithInfo(data.lineNumber, data.columnNumber, breakpointInfo);
1530                 }
1531             }
1532
1533             return;
1534         }
1535
1536         console.assert(lineNumber in this._breakpoints);
1537
1538         if (this._codeMirror.hasLineClass(lineNumber, "wrap", WI.TextEditor.MultipleBreakpointsStyleClassName)) {
1539             console.assert(!isEmptyObject(this._breakpoints[lineNumber]));
1540             return;
1541         }
1542
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;
1551
1552         this._documentMouseMovedEventListener = this._documentMouseMoved.bind(this);
1553         this._documentMouseUpEventListener = this._documentMouseUp.bind(this);
1554
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);
1558     }
1559
1560     _gutterContextMenu(codeMirror, lineNumber, gutterElement, event)
1561     {
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});
1566
1567             this._delegate.textEditorGutterContextMenu(this, lineNumber, 0, breakpoints, event);
1568         }
1569     }
1570
1571     _documentMouseMoved(event)
1572     {
1573         console.assert("_lineNumberWithMousedDownBreakpoint" in this);
1574         if (!("_lineNumberWithMousedDownBreakpoint" in this))
1575             return;
1576
1577         event.preventDefault();
1578
1579         var lineNumber;
1580         var position = this._codeMirror.coordsChar({left: event.pageX, top: event.pageY});
1581
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)
1586             position = null;
1587
1588         // If we have a position and it has a line then use it.
1589         if (position && "line" in position)
1590             lineNumber = position.line;
1591
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.
1594
1595         if (lineNumber === this._lineNumberWithDraggedBreakpoint)
1596             return;
1597
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;
1601
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);
1607             else
1608                 this._removeBreakpointFromLineAndColumn(this._lineNumberWithDraggedBreakpoint, this._columnNumberWithDraggedBreakpoint);
1609
1610             delete this._previousColumnBreakpointInfo;
1611             delete this._lineNumberWithDraggedBreakpoint;
1612             delete this._columnNumberWithDraggedBreakpoint;
1613         }
1614
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;
1624         }
1625     }
1626
1627     _documentMouseUp(event)
1628     {
1629         console.assert("_lineNumberWithMousedDownBreakpoint" in this);
1630         if (!("_lineNumberWithMousedDownBreakpoint" in this))
1631             return;
1632
1633         event.preventDefault();
1634
1635         document.removeEventListener("mousemove", this._documentMouseMovedEventListener, true);
1636         document.removeEventListener("mouseup", this._documentMouseUpEventListener, true);
1637
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";
1641
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;
1649                 }
1650             } else if (this._lineNumberWithMousedDownBreakpoint !== this._lineNumberWithDraggedBreakpoint) {
1651                 // The dragged breakpoint was moved to a new line.
1652
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;
1661                 }
1662
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;
1668                 }
1669             }
1670         } else {
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);
1676         }
1677
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;
1686     }
1687
1688     _openClickedLinks(event)
1689     {
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)
1694             return;
1695
1696         // If the token is not a link, then ignore it.
1697         if (!/\blink\b/.test(tokenInfo.type))
1698             return;
1699
1700         // The token string is the URL we should open. It might be a relative URL.
1701         var url = tokenInfo.string;
1702
1703         // Get the base URL.
1704         var baseURL = "";
1705         if (this._delegate && typeof this._delegate.textEditorBaseURL === "function")
1706             baseURL = this._delegate.textEditorBaseURL(this);
1707
1708         // Open the link after resolving the absolute URL from the base URL.
1709         WI.openURL(absoluteURL(url, baseURL));
1710
1711         // Stop processing the event.
1712         event.preventDefault();
1713         event.stopPropagation();
1714     }
1715
1716     _isPositionVisible(position)
1717     {
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");
1722
1723         return coords.top >= visibleRangeStart && coords.bottom <= visibleRangeEnd;
1724     }
1725
1726     _scrollIntoViewCentered(position)
1727     {
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);
1732     }
1733 };
1734
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;
1746
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",
1754 };