1/*
2 * Copyright (C) 2011 Google Inc. All rights reserved.
3 * Copyright (C) 2010 Apple Inc. All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
7 * met:
8 *
9 *     * Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *     * Redistributions in binary form must reproduce the above
12 * copyright notice, this list of conditions and the following disclaimer
13 * in the documentation and/or other materials provided with the
14 * distribution.
15 *     * Neither the name of Google Inc. nor the names of its
16 * contributors may be used to endorse or promote products derived from
17 * this software without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 */
31
32/**
33 * @constructor
34 * @extends {WebInspector.View}
35 * @implements {WebInspector.TextEditor}
36 * @param {?string} url
37 * @param {WebInspector.TextEditorDelegate} delegate
38 */
39WebInspector.DefaultTextEditor = function(url, delegate)
40{
41    WebInspector.View.call(this);
42    this._delegate = delegate;
43    this._url = url;
44
45    this.registerRequiredCSS("textEditor.css");
46
47    this.element.className = "text-editor monospace";
48    this.markAsLayoutBoundary();
49
50    // Prevent middle-click pasting in the editor unless it is explicitly enabled for certain component.
51    this.element.addEventListener("mouseup", preventDefaultOnMouseUp.bind(this), false);
52    function preventDefaultOnMouseUp(event)
53    {
54        if (event.button === 1)
55            event.consume(true);
56    }
57
58    this._textModel = new WebInspector.TextEditorModel();
59    this._textModel.addEventListener(WebInspector.TextEditorModel.Events.TextChanged, this._textChanged, this);
60
61    var syncScrollListener = this._syncScroll.bind(this);
62    var syncDecorationsForLineListener = this._syncDecorationsForLine.bind(this);
63    var syncLineHeightListener = this._syncLineHeight.bind(this);
64    this._mainPanel = new WebInspector.TextEditorMainPanel(this._delegate, this._textModel, url, syncScrollListener, syncDecorationsForLineListener);
65    this._gutterPanel = new WebInspector.TextEditorGutterPanel(this._textModel, syncDecorationsForLineListener, syncLineHeightListener);
66
67    this._mainPanel.element.addEventListener("scroll", this._handleScrollChanged.bind(this), false);
68
69    this._gutterPanel.element.addEventListener("mousedown", this._onMouseDown.bind(this), true);
70
71    // Explicitly enable middle-click pasting in the editor main panel.
72    this._mainPanel.element.addEventListener("mouseup", consumeMouseUp.bind(this), false);
73    function consumeMouseUp(event)
74    {
75        if (event.button === 1)
76            event.consume(false);
77    }
78
79    this.element.appendChild(this._mainPanel.element);
80    this.element.appendChild(this._gutterPanel.element);
81
82    // Forward mouse wheel events from the unscrollable gutter to the main panel.
83    function forwardWheelEvent(event)
84    {
85        var clone = document.createEvent("WheelEvent");
86        clone.initWebKitWheelEvent(event.wheelDeltaX, event.wheelDeltaY,
87                                   event.view,
88                                   event.screenX, event.screenY,
89                                   event.clientX, event.clientY,
90                                   event.ctrlKey, event.altKey, event.shiftKey, event.metaKey);
91        this._mainPanel.element.dispatchEvent(clone);
92    }
93    this._gutterPanel.element.addEventListener("mousewheel", forwardWheelEvent.bind(this), false);
94
95    this.element.addEventListener("keydown", this._handleKeyDown.bind(this), false);
96    this.element.addEventListener("contextmenu", this._contextMenu.bind(this), true);
97
98    this._wordMovementController = new WebInspector.DefaultTextEditor.WordMovementController(this, this._textModel);
99    this._registerShortcuts();
100}
101
102/**
103 * @constructor
104 * @param {WebInspector.TextRange} range
105 * @param {string} text
106 */
107WebInspector.DefaultTextEditor.EditInfo = function(range, text)
108{
109    this.range = range;
110    this.text = text;
111}
112
113WebInspector.DefaultTextEditor.prototype = {
114    /**
115     * @return {boolean}
116     */
117    isClean: function()
118    {
119        return this._textModel.isClean();
120    },
121
122    markClean: function()
123    {
124        this._textModel.markClean();
125    },
126    /**
127     * @param {number} lineNumber
128     * @param {number} column
129     * @return {?{startColumn: number, endColumn: number, type: string}}
130     */
131    tokenAtTextPosition: function(lineNumber, column)
132    {
133        return this._mainPanel.tokenAtTextPosition(lineNumber, column);
134    },
135
136    /*
137     * @param {number} lineNumber
138     * @param {number} column
139     * @return {?{x: number, y: number, height: number}}
140     */
141    cursorPositionToCoordinates: function(lineNumber, column)
142    {
143        return this._mainPanel.cursorPositionToCoordinates(lineNumber, column);
144    },
145
146    /**
147     * @param {number} x
148     * @param {number} y
149     * @return {?WebInspector.TextRange}
150     */
151    coordinatesToCursorPosition: function(x, y)
152    {
153        return this._mainPanel.coordinatesToCursorPosition(x, y);
154    },
155
156    /**
157     * @param {WebInspector.TextRange} range
158     * @return {string}
159     */
160    copyRange: function(range)
161    {
162        return this._textModel.copyRange(range);
163    },
164
165    /**
166     * @param {string} regex
167     * @param {string} cssClass
168     * @return {Object}
169     */
170    highlightRegex: function(regex, cssClass)
171    {
172        return this._mainPanel.highlightRegex(regex, cssClass);
173    },
174
175    /**
176     * @param {Object} highlightDescriptor
177     */
178    removeHighlight: function(highlightDescriptor)
179    {
180        this._mainPanel.removeHighlight(highlightDescriptor);
181    },
182
183    /**
184     * @param {WebInspector.TextRange} range
185     * @param {string} cssClass
186     * @return {Object}
187     */
188    highlightRange: function(range, cssClass)
189    {
190        return this._mainPanel.highlightRange(range, cssClass);
191    },
192
193    /**
194     * @param {string} mimeType
195     */
196    set mimeType(mimeType)
197    {
198        this._mainPanel.mimeType = mimeType;
199    },
200
201    /**
202     * @param {boolean} readOnly
203     */
204    setReadOnly: function(readOnly)
205    {
206        if (this._mainPanel.readOnly() === readOnly)
207            return;
208        this._mainPanel.setReadOnly(readOnly, this.isShowing());
209        WebInspector.markBeingEdited(this.element, !readOnly);
210    },
211
212    /**
213     * @return {boolean}
214     */
215    readOnly: function()
216    {
217        return this._mainPanel.readOnly();
218    },
219
220    /**
221     * @return {Element}
222     */
223    defaultFocusedElement: function()
224    {
225        return this._mainPanel.defaultFocusedElement();
226    },
227
228    /**
229     * @param {number} lineNumber
230     */
231    revealLine: function(lineNumber)
232    {
233        this._mainPanel.revealLine(lineNumber);
234    },
235
236    _onMouseDown: function(event)
237    {
238        var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
239        if (!target)
240            return;
241        this.dispatchEventToListeners(WebInspector.TextEditor.Events.GutterClick, { lineNumber: target.lineNumber, event: event });
242    },
243
244    /**
245     * @param {number} lineNumber
246     * @param {boolean} disabled
247     * @param {boolean} conditional
248     */
249    addBreakpoint: function(lineNumber, disabled, conditional)
250    {
251        this.beginUpdates();
252        this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint");
253        if (disabled)
254            this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint-disabled");
255        else
256            this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-disabled");
257        if (conditional)
258            this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint-conditional");
259        else
260            this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-conditional");
261        this.endUpdates();
262    },
263
264    /**
265     * @param {number} lineNumber
266     */
267    removeBreakpoint: function(lineNumber)
268    {
269        this.beginUpdates();
270        this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint");
271        this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-disabled");
272        this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-conditional");
273        this.endUpdates();
274    },
275
276    /**
277     * @param {number} lineNumber
278     */
279    setExecutionLine: function(lineNumber)
280    {
281        this._executionLineNumber = lineNumber;
282        this._mainPanel.addDecoration(lineNumber, "webkit-execution-line");
283        this._gutterPanel.addDecoration(lineNumber, "webkit-execution-line");
284    },
285
286    clearExecutionLine: function()
287    {
288        if (typeof this._executionLineNumber === "number") {
289            this._mainPanel.removeDecoration(this._executionLineNumber, "webkit-execution-line");
290            this._gutterPanel.removeDecoration(this._executionLineNumber, "webkit-execution-line");
291        }
292        delete this._executionLineNumber;
293    },
294
295    /**
296     * @param {number} lineNumber
297     * @param {Element} element
298     */
299    addDecoration: function(lineNumber, element)
300    {
301        this._mainPanel.addDecoration(lineNumber, element);
302        this._gutterPanel.addDecoration(lineNumber, element);
303        this._syncDecorationsForLine(lineNumber);
304    },
305
306    /**
307     * @param {number} lineNumber
308     * @param {Element} element
309     */
310    removeDecoration: function(lineNumber, element)
311    {
312        this._mainPanel.removeDecoration(lineNumber, element);
313        this._gutterPanel.removeDecoration(lineNumber, element);
314        this._syncDecorationsForLine(lineNumber);
315    },
316
317    /**
318     * @param {WebInspector.TextRange} range
319     */
320    markAndRevealRange: function(range)
321    {
322        if (range)
323            this.setSelection(range);
324        this._mainPanel.markAndRevealRange(range);
325    },
326
327    /**
328     * @param {number} lineNumber
329     */
330    highlightLine: function(lineNumber)
331    {
332        if (typeof lineNumber !== "number" || lineNumber < 0)
333            return;
334
335        lineNumber = Math.min(lineNumber, this._textModel.linesCount - 1);
336        this._mainPanel.highlightLine(lineNumber);
337    },
338
339    clearLineHighlight: function()
340    {
341        this._mainPanel.clearLineHighlight();
342    },
343
344    /**
345     * @return {Array.<Element>}
346     */
347    elementsToRestoreScrollPositionsFor: function()
348    {
349        return [this._mainPanel.element];
350    },
351
352    /**
353     * @param {WebInspector.TextEditor} textEditor
354     */
355    inheritScrollPositions: function(textEditor)
356    {
357        this._mainPanel.element._scrollTop = textEditor._mainPanel.element.scrollTop;
358        this._mainPanel.element._scrollLeft = textEditor._mainPanel.element.scrollLeft;
359    },
360
361    beginUpdates: function()
362    {
363        this._mainPanel.beginUpdates();
364        this._gutterPanel.beginUpdates();
365    },
366
367    endUpdates: function()
368    {
369        this._mainPanel.endUpdates();
370        this._gutterPanel.endUpdates();
371        this._updatePanelOffsets();
372    },
373
374    onResize: function()
375    {
376        this._mainPanel.resize();
377        this._gutterPanel.resize();
378        this._updatePanelOffsets();
379    },
380
381    _textChanged: function(event)
382    {
383        this._mainPanel.textChanged(event.data.oldRange, event.data.newRange);
384        this._gutterPanel.textChanged(event.data.oldRange, event.data.newRange);
385        this._updatePanelOffsets();
386        if (event.data.editRange)
387            this._delegate.onTextChanged(event.data.oldRange, event.data.newRange);
388    },
389
390    /**
391     * @param {WebInspector.TextRange} range
392     * @param {string} text
393     * @return {WebInspector.TextRange}
394     */
395    editRange: function(range, text)
396    {
397        return this._textModel.editRange(range, text, this.lastSelection());
398    },
399
400    _updatePanelOffsets: function()
401    {
402        var lineNumbersWidth = this._gutterPanel.element.offsetWidth;
403        if (lineNumbersWidth)
404            this._mainPanel.element.style.setProperty("left", (lineNumbersWidth + 2) + "px");
405        else
406            this._mainPanel.element.style.removeProperty("left"); // Use default value set in CSS.
407    },
408
409    _syncScroll: function()
410    {
411        var mainElement = this._mainPanel.element;
412        var gutterElement = this._gutterPanel.element;
413        // Handle horizontal scroll bar at the bottom of the main panel.
414        this._gutterPanel.syncClientHeight(mainElement.clientHeight);
415        gutterElement.scrollTop = mainElement.scrollTop;
416    },
417
418    /**
419     * @param {number} lineNumber
420     */
421    _syncDecorationsForLine: function(lineNumber)
422    {
423        if (lineNumber >= this._textModel.linesCount)
424            return;
425
426        var mainChunk = this._mainPanel.chunkForLine(lineNumber);
427        if (mainChunk.linesCount === 1 && mainChunk.isDecorated()) {
428            var gutterChunk = this._gutterPanel.makeLineAChunk(lineNumber);
429            var height = mainChunk.height;
430            if (height)
431                gutterChunk.element.style.setProperty("height", height + "px");
432            else
433                gutterChunk.element.style.removeProperty("height");
434        } else {
435            var gutterChunk = this._gutterPanel.chunkForLine(lineNumber);
436            if (gutterChunk.linesCount === 1)
437                gutterChunk.element.style.removeProperty("height");
438        }
439    },
440
441    /**
442     * @param {Element} gutterRow
443     */
444    _syncLineHeight: function(gutterRow)
445    {
446        if (this._lineHeightSynced)
447            return;
448        if (gutterRow && gutterRow.offsetHeight) {
449            // Force equal line heights for the child panels.
450            this.element.style.setProperty("line-height", gutterRow.offsetHeight + "px");
451            this._lineHeightSynced = true;
452        }
453    },
454
455    _registerShortcuts: function()
456    {
457        var keys = WebInspector.KeyboardShortcut.Keys;
458        var modifiers = WebInspector.KeyboardShortcut.Modifiers;
459
460        this._shortcuts = {};
461
462        this._shortcuts[WebInspector.KeyboardShortcut.SelectAll] = this._handleSelectAll.bind(this);
463        this._wordMovementController._registerShortcuts(this._shortcuts);
464    },
465
466    _handleSelectAll: function()
467    {
468        this.setSelection(this._textModel.range());
469        return true;
470    },
471
472    _handleKeyDown: function(e)
473    {
474        // If the event was not triggered from the entire editor, then
475        // ignore it. https://bugs.webkit.org/show_bug.cgi?id=102906
476        if (e.target.enclosingNodeOrSelfWithClass("webkit-line-decorations"))
477            return;
478
479        var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e);
480
481        var handler = this._shortcuts[shortcutKey];
482        if (handler && handler()) {
483            e.consume(true);
484            return;
485        }
486        this._mainPanel.handleKeyDown(shortcutKey, e);
487    },
488
489    _contextMenu: function(event)
490    {
491        var anchor = event.target.enclosingNodeOrSelfWithNodeName("a");
492        if (anchor)
493            return;
494        var contextMenu = new WebInspector.ContextMenu(event);
495        var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
496        if (target)
497            this._delegate.populateLineGutterContextMenu(contextMenu, target.lineNumber);
498        else {
499            this._mainPanel.populateContextMenu(event.target, contextMenu);
500        }
501        contextMenu.show();
502    },
503
504    _handleScrollChanged: function(event)
505    {
506        var visibleFrom = this._mainPanel.scrollTop();
507        var firstVisibleLineNumber = this._mainPanel.lineNumberAtOffset(visibleFrom);
508        this._delegate.scrollChanged(firstVisibleLineNumber);
509    },
510
511    /**
512     * @param {number} lineNumber
513     */
514    scrollToLine: function(lineNumber)
515    {
516        this._mainPanel.scrollToLine(lineNumber);
517    },
518
519    /**
520     * @return {WebInspector.TextRange}
521     */
522    selection: function()
523    {
524        return this._mainPanel.selection();
525    },
526
527    /**
528     * @return {WebInspector.TextRange?}
529     */
530    lastSelection: function()
531    {
532        return this._mainPanel.lastSelection();
533    },
534
535    /**
536     * @param {WebInspector.TextRange} textRange
537     */
538    setSelection: function(textRange)
539    {
540        this._mainPanel.setSelection(textRange);
541    },
542
543    /**
544     * @param {string} text
545     */
546    setText: function(text)
547    {
548        this._textModel.setText(text);
549    },
550
551    /**
552     * @return {string}
553     */
554    text: function()
555    {
556        return this._textModel.text();
557    },
558
559    /**
560     * @return {WebInspector.TextRange}
561     */
562    range: function()
563    {
564        return this._textModel.range();
565    },
566
567    /**
568     * @param {number} lineNumber
569     * @return {string}
570     */
571    line: function(lineNumber)
572    {
573        return this._textModel.line(lineNumber);
574    },
575
576    /**
577     * @return {number}
578     */
579    get linesCount()
580    {
581        return this._textModel.linesCount;
582    },
583
584    /**
585     * @param {number} line
586     * @param {string} name
587     * @param {?Object} value
588     */
589    setAttribute: function(line, name, value)
590    {
591        this._textModel.setAttribute(line, name, value);
592    },
593
594    /**
595     * @param {number} line
596     * @param {string} name
597     * @return {?Object} value
598     */
599    getAttribute: function(line, name)
600    {
601        return this._textModel.getAttribute(line, name);
602    },
603
604    /**
605     * @param {number} line
606     * @param {string} name
607     */
608    removeAttribute: function(line, name)
609    {
610        this._textModel.removeAttribute(line, name);
611    },
612
613    wasShown: function()
614    {
615        if (!this.readOnly())
616            WebInspector.markBeingEdited(this.element, true);
617
618        this._mainPanel.wasShown();
619    },
620
621    willHide: function()
622    {
623        this._mainPanel.willHide();
624        this._gutterPanel.willHide();
625
626        if (!this.readOnly())
627            WebInspector.markBeingEdited(this.element, false);
628    },
629
630    /**
631     * @param {Element} element
632     * @param {Array.<Object>} resultRanges
633     * @param {string} styleClass
634     * @param {Array.<Object>=} changes
635     */
636    highlightRangesWithStyleClass: function(element, resultRanges, styleClass, changes)
637    {
638        this._mainPanel.beginDomUpdates();
639        WebInspector.highlightRangesWithStyleClass(element, resultRanges, styleClass, changes);
640        this._mainPanel.endDomUpdates();
641    },
642
643    /**
644     * @param {number} scrollTop
645     * @param {number} clientHeight
646     * @param {number} chunkSize
647     */
648    overrideViewportForTest: function(scrollTop, clientHeight, chunkSize)
649    {
650        this._mainPanel.overrideViewportForTest(scrollTop, clientHeight, chunkSize);
651    },
652
653    __proto__: WebInspector.View.prototype
654}
655
656/**
657 * @constructor
658 * @param {WebInspector.TextEditorModel} textModel
659 */
660WebInspector.TextEditorChunkedPanel = function(textModel)
661{
662    this._textModel = textModel;
663
664    this.element = document.createElement("div");
665    this.element.addEventListener("scroll", this._scroll.bind(this), false);
666
667    this._defaultChunkSize = 50;
668    this._paintCoalescingLevel = 0;
669    this._domUpdateCoalescingLevel = 0;
670}
671
672WebInspector.TextEditorChunkedPanel.prototype = {
673    /**
674     * @param {number} lineNumber
675     */
676    scrollToLine: function(lineNumber)
677    {
678        if (lineNumber >= this._textModel.linesCount)
679            return;
680
681        var chunk = this.makeLineAChunk(lineNumber);
682        this.element.scrollTop = chunk.offsetTop;
683    },
684
685    /**
686     * @param {number} lineNumber
687     */
688    revealLine: function(lineNumber)
689    {
690        if (lineNumber >= this._textModel.linesCount)
691            return;
692
693        var chunk = this.makeLineAChunk(lineNumber);
694        chunk.element.scrollIntoViewIfNeeded();
695    },
696
697    /**
698     * @param {number} lineNumber
699     * @param {string|Element} decoration
700     */
701    addDecoration: function(lineNumber, decoration)
702    {
703        if (lineNumber >= this._textModel.linesCount)
704            return;
705
706        var chunk = this.makeLineAChunk(lineNumber);
707        chunk.addDecoration(decoration);
708    },
709
710    /**
711     * @param {number} lineNumber
712     * @param {string|Element} decoration
713     */
714    removeDecoration: function(lineNumber, decoration)
715    {
716        if (lineNumber >= this._textModel.linesCount)
717            return;
718
719        var chunk = this.chunkForLine(lineNumber);
720        chunk.removeDecoration(decoration);
721    },
722
723    buildChunks: function()
724    {
725        this.beginDomUpdates();
726
727        this._container.removeChildren();
728
729        this._textChunks = [];
730        for (var i = 0; i < this._textModel.linesCount; i += this._defaultChunkSize) {
731            var chunk = this.createNewChunk(i, i + this._defaultChunkSize);
732            this._textChunks.push(chunk);
733            this._container.appendChild(chunk.element);
734        }
735
736        this.repaintAll();
737
738        this.endDomUpdates();
739    },
740
741    /**
742     * @param {number} lineNumber
743     * @return {Object}
744     */
745    makeLineAChunk: function(lineNumber)
746    {
747        var chunkNumber = this.chunkNumberForLine(lineNumber);
748        var oldChunk = this._textChunks[chunkNumber];
749
750        if (!oldChunk) {
751            console.error("No chunk for line number: " + lineNumber);
752            return null;
753        }
754
755        if (oldChunk.linesCount === 1)
756            return oldChunk;
757
758        return this.splitChunkOnALine(lineNumber, chunkNumber, true);
759    },
760
761    /**
762     * @param {number} lineNumber
763     * @param {number} chunkNumber
764     * @param {boolean=} createSuffixChunk
765     * @return {Object}
766     */
767    splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk)
768    {
769        this.beginDomUpdates();
770
771        var oldChunk = this._textChunks[chunkNumber];
772        var wasExpanded = oldChunk.expanded();
773        oldChunk.collapse();
774
775        var insertIndex = chunkNumber + 1;
776
777        // Prefix chunk.
778        if (lineNumber > oldChunk.startLine) {
779            var prefixChunk = this.createNewChunk(oldChunk.startLine, lineNumber);
780            this._textChunks.splice(insertIndex++, 0, prefixChunk);
781            this._container.insertBefore(prefixChunk.element, oldChunk.element);
782        }
783
784        // Line chunk.
785        var endLine = createSuffixChunk ? lineNumber + 1 : oldChunk.startLine + oldChunk.linesCount;
786        var lineChunk = this.createNewChunk(lineNumber, endLine);
787        this._textChunks.splice(insertIndex++, 0, lineChunk);
788        this._container.insertBefore(lineChunk.element, oldChunk.element);
789
790        // Suffix chunk.
791        if (oldChunk.startLine + oldChunk.linesCount > endLine) {
792            var suffixChunk = this.createNewChunk(endLine, oldChunk.startLine + oldChunk.linesCount);
793            this._textChunks.splice(insertIndex, 0, suffixChunk);
794            this._container.insertBefore(suffixChunk.element, oldChunk.element);
795        }
796
797        // Remove enclosing chunk.
798        this._textChunks.splice(chunkNumber, 1);
799        this._container.removeChild(oldChunk.element);
800
801        if (wasExpanded) {
802            if (prefixChunk)
803                prefixChunk.expand();
804            lineChunk.expand();
805            if (suffixChunk)
806                suffixChunk.expand();
807        }
808
809        this.endDomUpdates();
810
811        return lineChunk;
812    },
813
814    createNewChunk: function(startLine, endLine)
815    {
816        throw new Error("createNewChunk() should be implemented by descendants");
817    },
818
819    _scroll: function()
820    {
821        this._scheduleRepaintAll();
822        if (this._syncScrollListener)
823            this._syncScrollListener();
824    },
825
826    _scheduleRepaintAll: function()
827    {
828        if (this._repaintAllTimer)
829            clearTimeout(this._repaintAllTimer);
830        this._repaintAllTimer = setTimeout(this.repaintAll.bind(this), 50);
831    },
832
833    beginUpdates: function()
834    {
835        this._paintCoalescingLevel++;
836    },
837
838    endUpdates: function()
839    {
840        this._paintCoalescingLevel--;
841        if (!this._paintCoalescingLevel)
842            this.repaintAll();
843    },
844
845    beginDomUpdates: function()
846    {
847        this._domUpdateCoalescingLevel++;
848    },
849
850    endDomUpdates: function()
851    {
852        this._domUpdateCoalescingLevel--;
853    },
854
855    /**
856     * @param {number} lineNumber
857     * @return {number}
858     */
859    chunkNumberForLine: function(lineNumber)
860    {
861        function compareLineNumbers(value, chunk)
862        {
863            return value < chunk.startLine ? -1 : 1;
864        }
865        var insertBefore = insertionIndexForObjectInListSortedByFunction(lineNumber, this._textChunks, compareLineNumbers);
866        return insertBefore - 1;
867    },
868
869    /**
870     * @param {number} lineNumber
871     * @return {Object}
872     */
873    chunkForLine: function(lineNumber)
874    {
875        return this._textChunks[this.chunkNumberForLine(lineNumber)];
876    },
877
878    /**
879     * @param {number} visibleFrom
880     * @return {number}
881     */
882    _findFirstVisibleChunkNumber: function(visibleFrom)
883    {
884        function compareOffsetTops(value, chunk)
885        {
886            return value < chunk.offsetTop ? -1 : 1;
887        }
888        var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, this._textChunks, compareOffsetTops);
889        return insertBefore - 1;
890    },
891
892    /**
893     * @param {number} visibleFrom
894     * @param {number} visibleTo
895     * @return {{start: number, end: number}}
896     */
897    findVisibleChunks: function(visibleFrom, visibleTo)
898    {
899        var span = (visibleTo - visibleFrom) * 0.5;
900        visibleFrom = Math.max(visibleFrom - span, 0);
901        visibleTo = visibleTo + span;
902
903        var from = this._findFirstVisibleChunkNumber(visibleFrom);
904        for (var to = from + 1; to < this._textChunks.length; ++to) {
905            if (this._textChunks[to].offsetTop >= visibleTo)
906                break;
907        }
908        return { start: from, end: to };
909    },
910
911    /**
912     * @param {number} visibleFrom
913     * @return {number}
914     */
915    lineNumberAtOffset: function(visibleFrom)
916    {
917        var chunk = this._textChunks[this._findFirstVisibleChunkNumber(visibleFrom)];
918        if (!chunk.expanded())
919            return chunk.startLine;
920
921        var lineNumbers = [];
922        for (var i = 0; i < chunk.linesCount; ++i) {
923            lineNumbers.push(chunk.startLine + i);
924        }
925
926        function compareLineRowOffsetTops(value, lineNumber)
927        {
928            var lineRow = chunk.expandedLineRow(lineNumber);
929            return value < lineRow.offsetTop ? -1 : 1;
930        }
931        var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, lineNumbers, compareLineRowOffsetTops);
932        return lineNumbers[insertBefore - 1];
933    },
934
935    repaintAll: function()
936    {
937        delete this._repaintAllTimer;
938
939        if (this._paintCoalescingLevel)
940            return;
941
942        var visibleFrom = this.scrollTop();
943        var visibleTo = visibleFrom + this.clientHeight();
944
945        if (visibleTo) {
946            var result = this.findVisibleChunks(visibleFrom, visibleTo);
947            this.expandChunks(result.start, result.end);
948        }
949    },
950
951    scrollTop: function()
952    {
953        return typeof this._scrollTopOverrideForTest === "number" ? this._scrollTopOverrideForTest : this.element.scrollTop;
954    },
955
956    clientHeight: function()
957    {
958        return typeof this._clientHeightOverrideForTest === "number" ? this._clientHeightOverrideForTest : this.element.clientHeight;
959    },
960
961    /**
962     * @param {number} fromIndex
963     * @param {number} toIndex
964     */
965    expandChunks: function(fromIndex, toIndex)
966    {
967        // First collapse chunks to collect the DOM elements into a cache to reuse them later.
968        for (var i = 0; i < fromIndex; ++i)
969            this._textChunks[i].collapse();
970        for (var i = toIndex; i < this._textChunks.length; ++i)
971            this._textChunks[i].collapse();
972        for (var i = fromIndex; i < toIndex; ++i)
973            this._textChunks[i].expand();
974    },
975
976    /**
977     * @param {Element} firstElement
978     * @param {Element=} lastElement
979     * @return {number}
980     */
981    totalHeight: function(firstElement, lastElement)
982    {
983        lastElement = (lastElement || firstElement).nextElementSibling;
984        if (lastElement)
985            return lastElement.offsetTop - firstElement.offsetTop;
986
987        var offsetParent = firstElement.offsetParent;
988        if (offsetParent && offsetParent.scrollHeight > offsetParent.clientHeight)
989            return offsetParent.scrollHeight - firstElement.offsetTop;
990
991        var total = 0;
992        while (firstElement && firstElement !== lastElement) {
993            total += firstElement.offsetHeight;
994            firstElement = firstElement.nextElementSibling;
995        }
996        return total;
997    },
998
999    resize: function()
1000    {
1001        this.repaintAll();
1002    }
1003}
1004
1005/**
1006 * @constructor
1007 * @extends {WebInspector.TextEditorChunkedPanel}
1008 * @param {WebInspector.TextEditorModel} textModel
1009 * @param {function(number)} syncDecorationsForLineListener
1010 * @param {function(Element)} syncLineHeightListener
1011 */
1012WebInspector.TextEditorGutterPanel = function(textModel, syncDecorationsForLineListener, syncLineHeightListener)
1013{
1014    WebInspector.TextEditorChunkedPanel.call(this, textModel);
1015
1016    this._syncDecorationsForLineListener = syncDecorationsForLineListener;
1017    this._syncLineHeightListener = syncLineHeightListener;
1018
1019    this.element.className = "text-editor-lines";
1020
1021    this._container = document.createElement("div");
1022    this._container.className = "inner-container";
1023    this.element.appendChild(this._container);
1024
1025    this._freeCachedElements();
1026    this.buildChunks();
1027    this._decorations = {};
1028}
1029
1030WebInspector.TextEditorGutterPanel.prototype = {
1031    _freeCachedElements: function()
1032    {
1033        this._cachedRows = [];
1034    },
1035
1036    willHide: function()
1037    {
1038        this._freeCachedElements();
1039    },
1040
1041    /**
1042     * @param {number} startLine
1043     * @param {number} endLine
1044     * @return {WebInspector.TextEditorGutterChunk}
1045     */
1046    createNewChunk: function(startLine, endLine)
1047    {
1048        return new WebInspector.TextEditorGutterChunk(this, startLine, endLine);
1049    },
1050
1051    /**
1052     * @param {WebInspector.TextRange} oldRange
1053     * @param {WebInspector.TextRange} newRange
1054     */
1055    textChanged: function(oldRange, newRange)
1056    {
1057        this.beginDomUpdates();
1058
1059        var linesDiff = newRange.linesCount - oldRange.linesCount;
1060        if (linesDiff) {
1061            // Remove old chunks (if needed).
1062            for (var chunkNumber = this._textChunks.length - 1; chunkNumber >= 0; --chunkNumber) {
1063                var chunk = this._textChunks[chunkNumber];
1064                if (chunk.startLine + chunk.linesCount <= this._textModel.linesCount)
1065                    break;
1066                chunk.collapse();
1067                this._container.removeChild(chunk.element);
1068            }
1069            this._textChunks.length = chunkNumber + 1;
1070
1071            // Add new chunks (if needed).
1072            var totalLines = 0;
1073            if (this._textChunks.length) {
1074                var lastChunk = this._textChunks[this._textChunks.length - 1];
1075                totalLines = lastChunk.startLine + lastChunk.linesCount;
1076            }
1077
1078            for (var i = totalLines; i < this._textModel.linesCount; i += this._defaultChunkSize) {
1079                var chunk = this.createNewChunk(i, i + this._defaultChunkSize);
1080                this._textChunks.push(chunk);
1081                this._container.appendChild(chunk.element);
1082            }
1083
1084            // Shift decorations if necessary
1085            var decorationsToRestore = {};
1086            for (var lineNumber in this._decorations) {
1087                lineNumber = parseInt(lineNumber, 10);
1088
1089                // Do not move decorations before the start position.
1090                if (lineNumber < oldRange.startLine)
1091                    continue;
1092                // Decorations follow the first character of line.
1093                if (lineNumber === oldRange.startLine && oldRange.startColumn)
1094                    continue;
1095
1096                var lineDecorationsCopy = this._decorations[lineNumber].slice();
1097                for (var i = 0; i < lineDecorationsCopy.length; ++i)
1098                    this.removeDecoration(lineNumber, lineDecorationsCopy[i]);
1099                // Do not restore the decorations before the end position.
1100                if (lineNumber >= oldRange.endLine)
1101                    decorationsToRestore[lineNumber] = lineDecorationsCopy;
1102            }
1103            for (var lineNumber in decorationsToRestore) {
1104                lineNumber = parseInt(lineNumber, 10);
1105                var lineDecorationsCopy = decorationsToRestore[lineNumber];
1106                for (var i = 0; i < lineDecorationsCopy.length; ++i)
1107                    this.addDecoration(lineNumber + linesDiff, lineDecorationsCopy[i]);
1108            }
1109
1110
1111            this.repaintAll();
1112        } else {
1113            // Decorations may have been removed, so we may have to sync those lines.
1114            var chunkNumber = this.chunkNumberForLine(newRange.startLine);
1115            var chunk = this._textChunks[chunkNumber];
1116            while (chunk && chunk.startLine <= newRange.endLine) {
1117                if (chunk.linesCount === 1)
1118                    this._syncDecorationsForLineListener(chunk.startLine);
1119                chunk = this._textChunks[++chunkNumber];
1120            }
1121        }
1122
1123        this.endDomUpdates();
1124    },
1125
1126    /**
1127     * @param {number} clientHeight
1128     */
1129    syncClientHeight: function(clientHeight)
1130    {
1131        if (this.element.offsetHeight > clientHeight)
1132            this._container.style.setProperty("padding-bottom", (this.element.offsetHeight - clientHeight) + "px");
1133        else
1134            this._container.style.removeProperty("padding-bottom");
1135    },
1136
1137    /**
1138     * @param {number} lineNumber
1139     * @param {string|Element} decoration
1140     */
1141    addDecoration: function(lineNumber, decoration)
1142    {
1143        WebInspector.TextEditorChunkedPanel.prototype.addDecoration.call(this, lineNumber, decoration);
1144        var decorations = this._decorations[lineNumber];
1145        if (!decorations) {
1146            decorations = [];
1147            this._decorations[lineNumber] = decorations;
1148        }
1149        decorations.push(decoration);
1150    },
1151
1152    /**
1153     * @param {number} lineNumber
1154     * @param {string|Element} decoration
1155     */
1156    removeDecoration: function(lineNumber, decoration)
1157    {
1158        WebInspector.TextEditorChunkedPanel.prototype.removeDecoration.call(this, lineNumber, decoration);
1159        var decorations = this._decorations[lineNumber];
1160        if (decorations) {
1161            decorations.remove(decoration);
1162            if (!decorations.length)
1163                delete this._decorations[lineNumber];
1164        }
1165    },
1166
1167    __proto__: WebInspector.TextEditorChunkedPanel.prototype
1168}
1169
1170/**
1171 * @constructor
1172 * @param {WebInspector.TextEditorGutterPanel} chunkedPanel
1173 * @param {number} startLine
1174 * @param {number} endLine
1175 */
1176WebInspector.TextEditorGutterChunk = function(chunkedPanel, startLine, endLine)
1177{
1178    this._chunkedPanel = chunkedPanel;
1179    this._textModel = chunkedPanel._textModel;
1180
1181    this.startLine = startLine;
1182    endLine = Math.min(this._textModel.linesCount, endLine);
1183    this.linesCount = endLine - startLine;
1184
1185    this._expanded = false;
1186
1187    this.element = document.createElement("div");
1188    this.element.lineNumber = startLine;
1189    this.element.className = "webkit-line-number";
1190
1191    if (this.linesCount === 1) {
1192        // Single line chunks are typically created for decorations. Host line number in
1193        // the sub-element in order to allow flexible border / margin management.
1194        var innerSpan = document.createElement("span");
1195        innerSpan.className = "webkit-line-number-inner";
1196        innerSpan.textContent = startLine + 1;
1197        var outerSpan = document.createElement("div");
1198        outerSpan.className = "webkit-line-number-outer";
1199        outerSpan.appendChild(innerSpan);
1200        this.element.appendChild(outerSpan);
1201    } else {
1202        var lineNumbers = [];
1203        for (var i = startLine; i < endLine; ++i)
1204            lineNumbers.push(i + 1);
1205        this.element.textContent = lineNumbers.join("\n");
1206    }
1207}
1208
1209WebInspector.TextEditorGutterChunk.prototype = {
1210    /**
1211     * @param {string} decoration
1212     */
1213    addDecoration: function(decoration)
1214    {
1215        this._chunkedPanel.beginDomUpdates();
1216        if (typeof decoration === "string")
1217            this.element.addStyleClass(decoration);
1218        this._chunkedPanel.endDomUpdates();
1219    },
1220
1221    /**
1222     * @param {string} decoration
1223     */
1224    removeDecoration: function(decoration)
1225    {
1226        this._chunkedPanel.beginDomUpdates();
1227        if (typeof decoration === "string")
1228            this.element.removeStyleClass(decoration);
1229        this._chunkedPanel.endDomUpdates();
1230    },
1231
1232    /**
1233     * @return {boolean}
1234     */
1235    expanded: function()
1236    {
1237        return this._expanded;
1238    },
1239
1240    expand: function()
1241    {
1242        if (this.linesCount === 1)
1243            this._chunkedPanel._syncDecorationsForLineListener(this.startLine);
1244
1245        if (this._expanded)
1246            return;
1247
1248        this._expanded = true;
1249
1250        if (this.linesCount === 1)
1251            return;
1252
1253        this._chunkedPanel.beginDomUpdates();
1254
1255        this._expandedLineRows = [];
1256        var parentElement = this.element.parentElement;
1257        for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
1258            var lineRow = this._createRow(i);
1259            parentElement.insertBefore(lineRow, this.element);
1260            this._expandedLineRows.push(lineRow);
1261        }
1262        parentElement.removeChild(this.element);
1263        this._chunkedPanel._syncLineHeightListener(this._expandedLineRows[0]);
1264
1265        this._chunkedPanel.endDomUpdates();
1266    },
1267
1268    collapse: function()
1269    {
1270        if (this.linesCount === 1)
1271            this._chunkedPanel._syncDecorationsForLineListener(this.startLine);
1272
1273        if (!this._expanded)
1274            return;
1275
1276        this._expanded = false;
1277
1278        if (this.linesCount === 1)
1279            return;
1280
1281        this._chunkedPanel.beginDomUpdates();
1282
1283        var elementInserted = false;
1284        for (var i = 0; i < this._expandedLineRows.length; ++i) {
1285            var lineRow = this._expandedLineRows[i];
1286            var parentElement = lineRow.parentElement;
1287            if (parentElement) {
1288                if (!elementInserted) {
1289                    elementInserted = true;
1290                    parentElement.insertBefore(this.element, lineRow);
1291                }
1292                parentElement.removeChild(lineRow);
1293            }
1294            this._chunkedPanel._cachedRows.push(lineRow);
1295        }
1296        delete this._expandedLineRows;
1297
1298        this._chunkedPanel.endDomUpdates();
1299    },
1300
1301    /**
1302     * @return {number}
1303     */
1304    get height()
1305    {
1306        if (!this._expandedLineRows)
1307            return this._chunkedPanel.totalHeight(this.element);
1308        return this._chunkedPanel.totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]);
1309    },
1310
1311    /**
1312     * @return {number}
1313     */
1314    get offsetTop()
1315    {
1316        return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
1317    },
1318
1319    /**
1320     * @param {number} lineNumber
1321     * @return {Element}
1322     */
1323    _createRow: function(lineNumber)
1324    {
1325        var lineRow = this._chunkedPanel._cachedRows.pop() || document.createElement("div");
1326        lineRow.lineNumber = lineNumber;
1327        lineRow.className = "webkit-line-number";
1328        lineRow.textContent = lineNumber + 1;
1329        return lineRow;
1330    }
1331}
1332
1333/**
1334 * @constructor
1335 * @extends {WebInspector.TextEditorChunkedPanel}
1336 * @param {WebInspector.TextEditorDelegate} delegate
1337 * @param {WebInspector.TextEditorModel} textModel
1338 * @param {?string} url
1339 * @param {function()} syncScrollListener
1340 * @param {function(number)} syncDecorationsForLineListener
1341 */
1342WebInspector.TextEditorMainPanel = function(delegate, textModel, url, syncScrollListener, syncDecorationsForLineListener)
1343{
1344    WebInspector.TextEditorChunkedPanel.call(this, textModel);
1345
1346    this._delegate = delegate;
1347    this._syncScrollListener = syncScrollListener;
1348    this._syncDecorationsForLineListener = syncDecorationsForLineListener;
1349
1350    this._url = url;
1351    this._highlighter = new WebInspector.TextEditorHighlighter(textModel, this._highlightDataReady.bind(this));
1352    this._readOnly = true;
1353
1354    this.element.className = "text-editor-contents";
1355    this.element.tabIndex = 0;
1356
1357    this._container = document.createElement("div");
1358    this._container.className = "inner-container";
1359    this._container.tabIndex = 0;
1360    this.element.appendChild(this._container);
1361
1362    this.element.addEventListener("focus", this._handleElementFocus.bind(this), false);
1363    this.element.addEventListener("textInput", this._handleTextInput.bind(this), false);
1364    this.element.addEventListener("cut", this._handleCut.bind(this), false);
1365    this.element.addEventListener("keypress", this._handleKeyPress.bind(this), false);
1366
1367    this._showWhitespace = WebInspector.experimentsSettings.showWhitespaceInEditor.isEnabled();
1368
1369    this._container.addEventListener("focus", this._handleFocused.bind(this), false);
1370
1371    this._highlightDescriptors = [];
1372
1373    this._tokenHighlighter = new WebInspector.TextEditorMainPanel.TokenHighlighter(this, textModel);
1374    this._braceMatcher = new WebInspector.TextEditorModel.BraceMatcher(textModel);
1375    this._braceHighlighter = new WebInspector.TextEditorMainPanel.BraceHighlightController(this, textModel, this._braceMatcher);
1376    this._smartBraceController = new WebInspector.TextEditorMainPanel.SmartBraceController(this, textModel, this._braceMatcher);
1377
1378    this._freeCachedElements();
1379    this.buildChunks();
1380    this._registerShortcuts();
1381}
1382
1383WebInspector.TextEditorMainPanel._ConsecutiveWhitespaceChars = {
1384    1: " ",
1385    2: "  ",
1386    4: "    ",
1387    8: "        ",
1388    16: "                "
1389};
1390
1391WebInspector.TextEditorMainPanel.prototype = {
1392    /**
1393     * @param {number} lineNumber
1394     * @param {number} column
1395     * @return {?{startColumn: number, endColumn: number, type: string}}
1396     */
1397    tokenAtTextPosition: function(lineNumber, column)
1398    {
1399        if (lineNumber >= this._textModel.linesCount || lineNumber < 0)
1400            return null;
1401        var line = this._textModel.line(lineNumber);
1402        if (column >= line.length || column < 0)
1403            return null;
1404        var highlight = this._textModel.getAttribute(lineNumber, "highlight");
1405        if (!highlight)
1406            return this._tokenAtUnhighlightedLine(line, column);
1407        function compare(value, object)
1408        {
1409            if (value >= object.startColumn && value <= object.endColumn)
1410                return 0;
1411            return value - object.startColumn;
1412        }
1413        var index = binarySearch(column, highlight.ranges, compare);
1414        if (index >= 0) {
1415            var range = highlight.ranges[index];
1416            return {
1417                startColumn: range.startColumn,
1418                endColumn: range.endColumn,
1419                type: range.token
1420            };
1421        }
1422        return null;
1423    },
1424
1425    /**
1426     * @param {number} lineNumber
1427     * @param {number} column
1428     * @return {?{x: number, y: number, height: number}}
1429     */
1430    cursorPositionToCoordinates: function(lineNumber, column)
1431    {
1432        if (lineNumber >= this._textModel.linesCount || lineNumber < 0)
1433            return null;
1434        var line = this._textModel.line(lineNumber);
1435        if (column > line.length || column < 0)
1436            return null;
1437
1438        var chunk = this.chunkForLine(lineNumber);
1439        if (!chunk.expanded())
1440            return null;
1441        var lineRow = chunk.expandedLineRow(lineNumber);
1442        var ranges = [{
1443            startColumn: column,
1444            endColumn: column,
1445            token: "measure-cursor-position"
1446        }];
1447        var selection = this.selection();
1448
1449        this.beginDomUpdates();
1450        this._renderRanges(lineRow, line, ranges);
1451        var spans = lineRow.getElementsByClassName("webkit-measure-cursor-position");
1452        if (WebInspector.debugDefaultTextEditor)
1453            console.assert(spans.length === 0);
1454        var totalOffset = spans[0].totalOffset();
1455        var height = spans[0].offsetHeight;
1456        this._paintLineRows([lineRow]);
1457        this.endDomUpdates();
1458
1459        this._restoreSelection(selection);
1460        return {
1461            x: totalOffset.left,
1462            y: totalOffset.top,
1463            height: height
1464        };
1465    },
1466
1467    /**
1468     * @param {number} x
1469     * @param {number} y
1470     * @return {?WebInspector.TextRange}
1471     */
1472    coordinatesToCursorPosition: function(x, y)
1473    {
1474        var element = document.elementFromPoint(x, y);
1475        if (!element)
1476            return null;
1477        var lineRow = element.enclosingNodeOrSelfWithClass("webkit-line-content");
1478        if (!lineRow)
1479            return null;
1480
1481        var line = this._textModel.line(lineRow.lineNumber) + " ";
1482        var ranges = [];
1483        const prefix = "character-position-";
1484        for(var i = 0; i < line.length; ++i) {
1485            ranges.push({
1486                startColumn: i,
1487                endColumn: i,
1488                token: prefix + i
1489            });
1490        }
1491
1492        var selection = this.selection();
1493
1494        this.beginDomUpdates();
1495        this._renderRanges(lineRow, line, ranges);
1496        var charElement = document.elementFromPoint(x, y);
1497        this._paintLineRows([lineRow]);
1498        this.endDomUpdates();
1499
1500        this._restoreSelection(selection);
1501        var className = charElement.className;
1502        if (className.indexOf(prefix) < 0)
1503            return null;
1504        var column = parseInt(className.substring(className.indexOf(prefix) + prefix.length), 10);
1505
1506        return WebInspector.TextRange.createFromLocation(lineRow.lineNumber, column);
1507    },
1508
1509    /**
1510     * @param {string} line
1511     * @param {number} column
1512     * @return {?{startColumn: number, endColumn: number, type: string}}
1513     */
1514    _tokenAtUnhighlightedLine: function(line, column)
1515    {
1516        var tokenizer = WebInspector.SourceTokenizer.Registry.getInstance().getTokenizer(this.mimeType);
1517        tokenizer.condition = tokenizer.createInitialCondition();
1518        tokenizer.line = line;
1519        var lastTokenizedColumn = 0;
1520        while (lastTokenizedColumn < line.length) {
1521            var newColumn = tokenizer.nextToken(lastTokenizedColumn);
1522            if (column < newColumn) {
1523                if (!tokenizer.tokenType)
1524                    return null;
1525                return {
1526                    startColumn: lastTokenizedColumn,
1527                    endColumn: newColumn - 1,
1528                    type: tokenizer.tokenType
1529                };
1530            } else
1531                lastTokenizedColumn = newColumn;
1532        }
1533        return null;
1534    },
1535
1536    _registerShortcuts: function()
1537    {
1538        var keys = WebInspector.KeyboardShortcut.Keys;
1539        var modifiers = WebInspector.KeyboardShortcut.Modifiers;
1540
1541        this._shortcuts = {};
1542
1543        this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Enter.code, WebInspector.KeyboardShortcut.Modifiers.None)] = this._handleEnterKey.bind(this);
1544        this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.CtrlOrMeta)] = this._handleUndoRedo.bind(this, false);
1545
1546        var handleRedo = this._handleUndoRedo.bind(this, true);
1547        this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.Shift | modifiers.CtrlOrMeta)] = handleRedo;
1548        if (!WebInspector.isMac())
1549            this._shortcuts[WebInspector.KeyboardShortcut.makeKey("y", modifiers.CtrlOrMeta)] = handleRedo;
1550
1551        var handleTabKey = this._handleTabKeyPress.bind(this, false);
1552        var handleShiftTabKey = this._handleTabKeyPress.bind(this, true);
1553        this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code)] = handleTabKey;
1554        this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code, modifiers.Shift)] = handleShiftTabKey;
1555
1556        var homeKey = WebInspector.isMac() ? keys.Right : keys.Home;
1557        var homeModifier = WebInspector.isMac() ? modifiers.Meta : modifiers.None;
1558        this._shortcuts[WebInspector.KeyboardShortcut.makeKey(homeKey.code, homeModifier)] = this._handleHomeKey.bind(this, false);
1559        this._shortcuts[WebInspector.KeyboardShortcut.makeKey(homeKey.code, homeModifier | modifiers.Shift)] = this._handleHomeKey.bind(this, true);
1560
1561        this._charOverrides = {};
1562
1563        this._smartBraceController.registerShortcuts(this._shortcuts);
1564        this._smartBraceController.registerCharOverrides(this._charOverrides);
1565    },
1566
1567    _handleKeyPress: function(event)
1568    {
1569        var char = String.fromCharCode(event.which);
1570        var handler = this._charOverrides[char];
1571        if (handler && handler()) {
1572            event.consume(true);
1573            return;
1574        }
1575        this._keyDownCode = event.keyCode;
1576    },
1577
1578    /**
1579     * @param {boolean} shift
1580     */
1581    _handleHomeKey: function(shift)
1582    {
1583        var selection = this.selection();
1584
1585        var line = this._textModel.line(selection.endLine);
1586        var firstNonBlankCharacter = 0;
1587        while (firstNonBlankCharacter < line.length) {
1588            var char = line.charAt(firstNonBlankCharacter);
1589            if (char === " " || char === "\t")
1590                ++firstNonBlankCharacter;
1591            else
1592                break;
1593        }
1594        if (firstNonBlankCharacter >= line.length || selection.endColumn === firstNonBlankCharacter)
1595            return false;
1596
1597        selection.endColumn = firstNonBlankCharacter;
1598        if (!shift)
1599            selection = selection.collapseToEnd();
1600        this._restoreSelection(selection);
1601        return true;
1602    },
1603
1604    /**
1605     * @param {string} regex
1606     * @param {string} cssClass
1607     * @return {Object}
1608     */
1609    highlightRegex: function(regex, cssClass)
1610    {
1611        var highlightDescriptor = new WebInspector.TextEditorMainPanel.RegexHighlightDescriptor(new RegExp(regex, "g"), cssClass);
1612        this._highlightDescriptors.push(highlightDescriptor);
1613        this._repaintLineRowsAffectedByHighlightDescriptors([highlightDescriptor]);
1614        return highlightDescriptor;
1615    },
1616
1617    /**
1618     * @param {Object} highlightDescriptor
1619     */
1620    removeHighlight: function(highlightDescriptor)
1621    {
1622        this._highlightDescriptors.remove(highlightDescriptor);
1623        this._repaintLineRowsAffectedByHighlightDescriptors([highlightDescriptor]);
1624    },
1625
1626    /**
1627     * @param {WebInspector.TextRange} range
1628     * @param {string} cssClass
1629     * @return {Object}
1630     */
1631    highlightRange: function(range, cssClass)
1632    {
1633        var highlightDescriptor = new WebInspector.TextEditorMainPanel.RangeHighlightDescriptor(range, cssClass);
1634        this._highlightDescriptors.push(highlightDescriptor);
1635        this._repaintLineRowsAffectedByHighlightDescriptors([highlightDescriptor]);
1636        return highlightDescriptor;
1637    },
1638
1639    /**
1640     * @param {Array.<WebInspector.TextEditorMainPanel.HighlightDescriptor>} highlightDescriptors
1641     */
1642    _repaintLineRowsAffectedByHighlightDescriptors: function(highlightDescriptors)
1643    {
1644        var visibleFrom = this.scrollTop();
1645        var visibleTo = visibleFrom + this.clientHeight();
1646
1647        var visibleChunks = this.findVisibleChunks(visibleFrom, visibleTo);
1648
1649        var affectedLineRows = [];
1650        for (var i = visibleChunks.start; i < visibleChunks.end; ++i) {
1651            var chunk = this._textChunks[i];
1652            if (!chunk.expanded())
1653                continue;
1654            for (var lineNumber = chunk.startLine; lineNumber < chunk.startLine + chunk.linesCount; ++lineNumber) {
1655                var lineRow = chunk.expandedLineRow(lineNumber);
1656                var line = this._textModel.line(lineNumber);
1657                for(var j = 0; j < highlightDescriptors.length; ++j) {
1658                    if (highlightDescriptors[j].affectsLine(lineNumber, line)) {
1659                        affectedLineRows.push(lineRow);
1660                        break;
1661                    }
1662                }
1663            }
1664        }
1665        if (affectedLineRows.length === 0)
1666            return;
1667        var selection = this.selection();
1668        this._paintLineRows(affectedLineRows);
1669        this._restoreSelection(selection);
1670    },
1671
1672    resize: function()
1673    {
1674        WebInspector.TextEditorChunkedPanel.prototype.resize.call(this);
1675        this._repaintLineRowsAffectedByHighlightDescriptors(this._highlightDescriptors);
1676    },
1677
1678    wasShown: function()
1679    {
1680        this._boundSelectionChangeListener = this._handleSelectionChange.bind(this);
1681        document.addEventListener("selectionchange", this._boundSelectionChangeListener, false);
1682
1683        this._isShowing = true;
1684        this._attachMutationObserver();
1685    },
1686
1687    willHide: function()
1688    {
1689        document.removeEventListener("selectionchange", this._boundSelectionChangeListener, false);
1690        delete this._boundSelectionChangeListener;
1691
1692        this._detachMutationObserver();
1693        this._isShowing = false;
1694        this._freeCachedElements();
1695    },
1696
1697    /**
1698     * @param {Element} eventTarget
1699     * @param {WebInspector.ContextMenu} contextMenu
1700     */
1701    populateContextMenu: function(eventTarget, contextMenu)
1702    {
1703        var target = this._enclosingLineRowOrSelf(eventTarget);
1704        this._delegate.populateTextAreaContextMenu(contextMenu, target && target.lineNumber);
1705    },
1706
1707    /**
1708     * @param {WebInspector.TextRange} textRange
1709     */
1710    setSelection: function(textRange)
1711    {
1712        this._lastSelection = textRange;
1713        if (this.element.isAncestor(document.activeElement))
1714            this._restoreSelection(textRange);
1715    },
1716
1717    _handleFocused: function()
1718    {
1719        if (this._lastSelection)
1720            this.setSelection(this._lastSelection);
1721    },
1722
1723    _attachMutationObserver: function()
1724    {
1725        if (!this._isShowing)
1726            return;
1727
1728        if (this._mutationObserver)
1729            this._mutationObserver.disconnect();
1730        this._mutationObserver = new NonLeakingMutationObserver(this._handleMutations.bind(this));
1731        this._mutationObserver.observe(this._container, { subtree: true, childList: true, characterData: true });
1732    },
1733
1734    _detachMutationObserver: function()
1735    {
1736        if (!this._isShowing)
1737            return;
1738
1739        if (this._mutationObserver) {
1740            this._mutationObserver.disconnect();
1741            delete this._mutationObserver;
1742        }
1743    },
1744
1745    /**
1746     * @param {string} mimeType
1747     */
1748    set mimeType(mimeType)
1749    {
1750        this._highlighter.mimeType = mimeType;
1751    },
1752
1753    get mimeType()
1754    {
1755        return this._highlighter.mimeType;
1756    },
1757
1758    /**
1759     * @param {boolean} readOnly
1760     * @param {boolean} requestFocus
1761     */
1762    setReadOnly: function(readOnly, requestFocus)
1763    {
1764        if (this._readOnly === readOnly)
1765            return;
1766
1767        this.beginDomUpdates();
1768        this._readOnly = readOnly;
1769        if (this._readOnly)
1770            this._container.removeStyleClass("text-editor-editable");
1771        else {
1772            this._container.addStyleClass("text-editor-editable");
1773            if (requestFocus)
1774                this._updateSelectionOnStartEditing();
1775        }
1776        this.endDomUpdates();
1777    },
1778
1779    /**
1780     * @return {boolean}
1781     */
1782    readOnly: function()
1783    {
1784        return this._readOnly;
1785    },
1786
1787    _handleElementFocus: function()
1788    {
1789        if (!this._readOnly)
1790            this._container.focus();
1791    },
1792
1793    /**
1794     * @return {Element}
1795     */
1796    defaultFocusedElement: function()
1797    {
1798        if (this._readOnly)
1799            return this.element;
1800        return this._container;
1801    },
1802
1803    _updateSelectionOnStartEditing: function()
1804    {
1805        // focus() needs to go first for the case when the last selection was inside the editor and
1806        // the "Edit" button was clicked. In this case we bail at the check below, but the
1807        // editor does not receive the focus, thus "Esc" does not cancel editing until at least
1808        // one change has been made to the editor contents.
1809        this._container.focus();
1810        var selection = window.getSelection();
1811        if (selection.rangeCount) {
1812            var commonAncestorContainer = selection.getRangeAt(0).commonAncestorContainer;
1813            if (this._container.isSelfOrAncestor(commonAncestorContainer))
1814                return;
1815        }
1816
1817        selection.removeAllRanges();
1818        var range = document.createRange();
1819        range.setStart(this._container, 0);
1820        range.setEnd(this._container, 0);
1821        selection.addRange(range);
1822    },
1823
1824    /**
1825     * @param {WebInspector.TextRange} range
1826     */
1827    markAndRevealRange: function(range)
1828    {
1829        if (this._rangeToMark) {
1830            var markedLine = this._rangeToMark.startLine;
1831            delete this._rangeToMark;
1832            // Remove the marked region immediately.
1833            this.beginDomUpdates();
1834            var chunk = this.chunkForLine(markedLine);
1835            var wasExpanded = chunk.expanded();
1836            chunk.collapse();
1837            chunk.updateCollapsedLineRow();
1838            if (wasExpanded)
1839                chunk.expand();
1840            this.endDomUpdates();
1841        }
1842
1843        if (range) {
1844            this._rangeToMark = range;
1845            this.revealLine(range.startLine);
1846            var chunk = this.makeLineAChunk(range.startLine);
1847            this._paintLines(chunk.startLine, chunk.startLine + 1);
1848            if (this._markedRangeElement)
1849                this._markedRangeElement.scrollIntoViewIfNeeded();
1850        }
1851        delete this._markedRangeElement;
1852    },
1853
1854    /**
1855     * @param {number} lineNumber
1856     */
1857    highlightLine: function(lineNumber)
1858    {
1859        this.clearLineHighlight();
1860        this._highlightedLine = lineNumber;
1861        this.revealLine(lineNumber);
1862
1863        if (!this._readOnly)
1864            this._restoreSelection(WebInspector.TextRange.createFromLocation(lineNumber, 0), false);
1865
1866        this.addDecoration(lineNumber, "webkit-highlighted-line");
1867    },
1868
1869    clearLineHighlight: function()
1870    {
1871        if (typeof this._highlightedLine === "number") {
1872            this.removeDecoration(this._highlightedLine, "webkit-highlighted-line");
1873            delete this._highlightedLine;
1874        }
1875    },
1876
1877    _freeCachedElements: function()
1878    {
1879        this._cachedSpans = [];
1880        this._cachedTextNodes = [];
1881        this._cachedRows = [];
1882    },
1883
1884    /**
1885     * @param {boolean} redo
1886     * @return {boolean}
1887     */
1888    _handleUndoRedo: function(redo)
1889    {
1890        if (this.readOnly())
1891            return false;
1892
1893        this.beginUpdates();
1894
1895        var range = redo ? this._textModel.redo() : this._textModel.undo();
1896
1897        this.endUpdates();
1898
1899        // Restore location post-repaint.
1900        if (range)
1901            this._restoreSelection(range, true);
1902
1903        return true;
1904    },
1905
1906    /**
1907     * @param {boolean} shiftKey
1908     * @return {boolean}
1909     */
1910    _handleTabKeyPress: function(shiftKey)
1911    {
1912        if (this.readOnly())
1913            return false;
1914
1915        var selection = this.selection();
1916        if (!selection)
1917            return false;
1918
1919        var range = selection.normalize();
1920
1921        this.beginUpdates();
1922
1923        var newRange;
1924        var rangeWasEmpty = range.isEmpty();
1925        if (shiftKey)
1926            newRange = this._textModel.unindentLines(range);
1927        else {
1928            if (rangeWasEmpty)
1929                newRange = this._textModel.editRange(range, WebInspector.settings.textEditorIndent.get());
1930            else
1931                newRange = this._textModel.indentLines(range);
1932        }
1933
1934        this.endUpdates();
1935        if (rangeWasEmpty)
1936            newRange.startColumn = newRange.endColumn;
1937        this._restoreSelection(newRange, true);
1938        return true;
1939    },
1940
1941    _handleEnterKey: function()
1942    {
1943        if (this.readOnly())
1944            return false;
1945
1946        var range = this.selection();
1947        if (!range)
1948            return false;
1949
1950        range = range.normalize();
1951
1952        if (range.endColumn === 0)
1953            return false;
1954
1955        var line = this._textModel.line(range.startLine);
1956        var linePrefix = line.substring(0, range.startColumn);
1957        var indentMatch = linePrefix.match(/^\s+/);
1958        var currentIndent = indentMatch ? indentMatch[0] : "";
1959
1960        var textEditorIndent = WebInspector.settings.textEditorIndent.get();
1961        var indent = WebInspector.TextEditorModel.endsWithBracketRegex.test(linePrefix) ? currentIndent + textEditorIndent : currentIndent;
1962
1963        if (!indent)
1964            return false;
1965
1966        this.beginDomUpdates();
1967
1968        var lineBreak = this._textModel.lineBreak;
1969        var newRange;
1970        if (range.isEmpty() && line.substr(range.endColumn - 1, 2) === '{}') {
1971            // {|}
1972            // becomes
1973            // {
1974            //     |
1975            // }
1976            newRange = this._textModel.editRange(range, lineBreak + indent + lineBreak + currentIndent);
1977            newRange.endLine--;
1978            newRange.endColumn += textEditorIndent.length;
1979        } else
1980            newRange = this._textModel.editRange(range, lineBreak + indent);
1981
1982        this.endDomUpdates();
1983        this._restoreSelection(newRange.collapseToEnd(), true);
1984
1985        return true;
1986    },
1987
1988    /**
1989     * @param {number} lineNumber
1990     * @param {number} chunkNumber
1991     * @param {boolean=} createSuffixChunk
1992     * @return {Object}
1993     */
1994    splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk)
1995    {
1996        var selection = this.selection();
1997        var chunk = WebInspector.TextEditorChunkedPanel.prototype.splitChunkOnALine.call(this, lineNumber, chunkNumber, createSuffixChunk);
1998        this._restoreSelection(selection);
1999        return chunk;
2000    },
2001
2002    beginDomUpdates: function()
2003    {
2004        if (!this._domUpdateCoalescingLevel)
2005            this._detachMutationObserver();
2006        WebInspector.TextEditorChunkedPanel.prototype.beginDomUpdates.call(this);
2007    },
2008
2009    endDomUpdates: function()
2010    {
2011        WebInspector.TextEditorChunkedPanel.prototype.endDomUpdates.call(this);
2012        if (!this._domUpdateCoalescingLevel)
2013            this._attachMutationObserver();
2014    },
2015
2016    buildChunks: function()
2017    {
2018        for (var i = 0; i < this._textModel.linesCount; ++i)
2019            this._textModel.removeAttribute(i, "highlight");
2020
2021        WebInspector.TextEditorChunkedPanel.prototype.buildChunks.call(this);
2022    },
2023
2024    /**
2025     * @param {number} startLine
2026     * @param {number} endLine
2027     * @return {WebInspector.TextEditorMainChunk}
2028     */
2029    createNewChunk: function(startLine, endLine)
2030    {
2031        return new WebInspector.TextEditorMainChunk(this, startLine, endLine);
2032    },
2033
2034    /**
2035     * @param {number} fromIndex
2036     * @param {number} toIndex
2037     */
2038    expandChunks: function(fromIndex, toIndex)
2039    {
2040        var lastChunk = this._textChunks[toIndex - 1];
2041        var lastVisibleLine = lastChunk.startLine + lastChunk.linesCount;
2042
2043        var selection = this.selection();
2044
2045        this._muteHighlightListener = true;
2046        this._highlighter.highlight(lastVisibleLine);
2047        delete this._muteHighlightListener;
2048
2049        WebInspector.TextEditorChunkedPanel.prototype.expandChunks.call(this, fromIndex, toIndex);
2050
2051        this._restoreSelection(selection);
2052    },
2053
2054    /**
2055     * @param {number} fromLine
2056     * @param {number} toLine
2057     */
2058    _highlightDataReady: function(fromLine, toLine)
2059    {
2060        if (this._muteHighlightListener)
2061            return;
2062        this._paintLines(fromLine, toLine, true /*restoreSelection*/);
2063    },
2064
2065    /**
2066     * @param {number} fromLine
2067     * @param {number} toLine
2068     * @param {boolean=} restoreSelection
2069     */
2070    _paintLines: function(fromLine, toLine, restoreSelection)
2071    {
2072        var lineRows = [];
2073        var chunk;
2074        for (var lineNumber = fromLine; lineNumber < toLine; ++lineNumber) {
2075            if (!chunk || lineNumber < chunk.startLine || lineNumber >= chunk.startLine + chunk.linesCount)
2076                chunk = this.chunkForLine(lineNumber);
2077            var lineRow = chunk.expandedLineRow(lineNumber);
2078            if (!lineRow)
2079                continue;
2080            lineRows.push(lineRow);
2081        }
2082        if (lineRows.length === 0)
2083            return;
2084
2085        var selection;
2086        if (restoreSelection)
2087            selection = this.selection();
2088
2089        this._paintLineRows(lineRows);
2090
2091        if (restoreSelection)
2092            this._restoreSelection(selection);
2093    },
2094
2095    /**
2096     * @param {Array.<Element>} lineRows
2097     */
2098    _paintLineRows: function(lineRows)
2099    {
2100        var highlight = {};
2101        this.beginDomUpdates();
2102        for(var i = 0; i < this._highlightDescriptors.length; ++i) {
2103            var highlightDescriptor = this._highlightDescriptors[i];
2104            this._measureHighlightDescriptor(highlight, lineRows, highlightDescriptor);
2105        }
2106
2107        for(var i = 0; i < lineRows.length; ++i)
2108            this._paintLine(lineRows[i], highlight[lineRows[i].lineNumber]);
2109
2110        this.endDomUpdates();
2111    },
2112
2113    /**
2114     * @param {Object.<number, Array.<WebInspector.TextEditorMainPanel.LineOverlayHighlight>>} highlight
2115     * @param {Array.<Element>} lineRows
2116     * @param {WebInspector.TextEditorMainPanel.HighlightDescriptor} highlightDescriptor
2117     */
2118    _measureHighlightDescriptor: function(highlight, lineRows, highlightDescriptor)
2119    {
2120        var rowsToMeasure = [];
2121        for(var i = 0; i < lineRows.length; ++i) {
2122            var lineRow = lineRows[i];
2123            var line = this._textModel.line(lineRow.lineNumber);
2124            var ranges = highlightDescriptor.rangesForLine(lineRow.lineNumber, line);
2125            if (ranges.length === 0)
2126                continue;
2127            for(var j = 0; j < ranges.length; ++j)
2128                ranges[j].token = "measure-span";
2129
2130            this._renderRanges(lineRow, line, ranges);
2131            rowsToMeasure.push(lineRow);
2132        }
2133
2134        for(var i = 0; i < rowsToMeasure.length; ++i) {
2135            var lineRow = rowsToMeasure[i];
2136            var lineNumber = lineRow.lineNumber;
2137            var metrics = this._measureSpans(lineRow);
2138
2139            if (!highlight[lineNumber])
2140                highlight[lineNumber] = [];
2141
2142            highlight[lineNumber].push(new WebInspector.TextEditorMainPanel.LineOverlayHighlight(metrics, highlightDescriptor.cssClass()));
2143        }
2144    },
2145
2146    /**
2147     * @param {Element} lineRow
2148     * @return {Array.<WebInspector.TextEditorMainPanel.ElementMetrics>}
2149     */
2150    _measureSpans: function(lineRow)
2151    {
2152        var spans = lineRow.getElementsByClassName("webkit-measure-span");
2153        var metrics = [];
2154        for(var i = 0; i < spans.length; ++i)
2155            metrics.push(new WebInspector.TextEditorMainPanel.ElementMetrics(spans[i]));
2156        return metrics;
2157    },
2158
2159    /**
2160     * @param {Element} lineRow
2161     * @param {WebInspector.TextEditorMainPanel.LineOverlayHighlight} highlight
2162     */
2163    _appendOverlayHighlight: function(lineRow, highlight)
2164    {
2165        var metrics = highlight.metrics;
2166        var cssClass = highlight.cssClass;
2167        for(var i = 0; i < metrics.length; ++i) {
2168            var highlightSpan = document.createElement("span");
2169            highlightSpan._isOverlayHighlightElement = true;
2170            highlightSpan.addStyleClass(cssClass);
2171            highlightSpan.style.left = metrics[i].left + "px";
2172            highlightSpan.style.width = metrics[i].width + "px";
2173            highlightSpan.style.height = metrics[i].height + "px";
2174            highlightSpan.addStyleClass("text-editor-overlay-highlight");
2175            lineRow.insertBefore(highlightSpan, lineRow.decorationsElement);
2176        }
2177    },
2178
2179    /**
2180     * @param {Element} lineRow
2181     * @param {string} line
2182     * @param {Array.<{startColumn: number, endColumn: number, token: ?string}>} ranges
2183     * @param {boolean=} splitWhitespaceSequences
2184     */
2185    _renderRanges: function(lineRow, line, ranges, splitWhitespaceSequences)
2186    {
2187        var decorationsElement = lineRow.decorationsElement;
2188
2189        if (!decorationsElement)
2190            lineRow.removeChildren();
2191        else {
2192            while (true) {
2193                var child = lineRow.firstChild;
2194                if (!child || child === decorationsElement)
2195                    break;
2196                lineRow.removeChild(child);
2197            }
2198        }
2199
2200        if (!line)
2201            lineRow.insertBefore(document.createElement("br"), decorationsElement);
2202
2203        var plainTextStart = 0;
2204        for(var i = 0; i < ranges.length; i++) {
2205            var rangeStart = ranges[i].startColumn;
2206            var rangeEnd = ranges[i].endColumn;
2207
2208            if (plainTextStart < rangeStart) {
2209                this._insertSpanBefore(lineRow, decorationsElement, line.substring(plainTextStart, rangeStart));
2210            }
2211
2212            if (splitWhitespaceSequences && ranges[i].token === "whitespace")
2213                this._renderWhitespaceCharsWithFixedSizeSpans(lineRow, decorationsElement, rangeEnd - rangeStart + 1);
2214            else
2215                this._insertSpanBefore(lineRow, decorationsElement, line.substring(rangeStart, rangeEnd + 1), ranges[i].token ? "webkit-" + ranges[i].token : "");
2216            plainTextStart = rangeEnd + 1;
2217        }
2218        if (plainTextStart < line.length) {
2219            this._insertSpanBefore(lineRow, decorationsElement, line.substring(plainTextStart, line.length));
2220        }
2221    },
2222
2223    /**
2224     * @param {Element} lineRow
2225     * @param {Element} decorationsElement
2226     * @param {number} length
2227     */
2228    _renderWhitespaceCharsWithFixedSizeSpans: function(lineRow, decorationsElement, length)
2229    {
2230        for (var whitespaceLength = 16; whitespaceLength > 0; whitespaceLength >>= 1) {
2231            var cssClass = "webkit-whitespace webkit-whitespace-" + whitespaceLength;
2232            for (; length >= whitespaceLength; length -= whitespaceLength)
2233                this._insertSpanBefore(lineRow, decorationsElement, WebInspector.TextEditorMainPanel._ConsecutiveWhitespaceChars[whitespaceLength], cssClass);
2234        }
2235    },
2236
2237    /**
2238     * @param {Element} lineRow
2239     * @param {Array.<WebInspector.TextEditorMainPanel.LineOverlayHighlight>} overlayHighlight
2240     */
2241    _paintLine: function(lineRow, overlayHighlight)
2242    {
2243        var lineNumber = lineRow.lineNumber;
2244
2245        this.beginDomUpdates();
2246        try {
2247            var syntaxHighlight = this._textModel.getAttribute(lineNumber, "highlight");
2248
2249            var line = this._textModel.line(lineNumber);
2250            var ranges = syntaxHighlight ? syntaxHighlight.ranges : [];
2251            this._renderRanges(lineRow, line, ranges, this._showWhitespace);
2252
2253            if (overlayHighlight)
2254                for(var i = 0; i < overlayHighlight.length; ++i)
2255                    this._appendOverlayHighlight(lineRow, overlayHighlight[i]);
2256        } finally {
2257            if (this._rangeToMark && this._rangeToMark.startLine === lineNumber)
2258                this._markedRangeElement = WebInspector.highlightSearchResult(lineRow, this._rangeToMark.startColumn, this._rangeToMark.endColumn - this._rangeToMark.startColumn);
2259            this.endDomUpdates();
2260        }
2261    },
2262
2263    /**
2264     * @param {Element} lineRow
2265     */
2266    _releaseLinesHighlight: function(lineRow)
2267    {
2268        if (!lineRow)
2269            return;
2270        if ("spans" in lineRow) {
2271            var spans = lineRow.spans;
2272            for (var j = 0; j < spans.length; ++j)
2273                this._cachedSpans.push(spans[j]);
2274            delete lineRow.spans;
2275        }
2276        if ("textNodes" in lineRow) {
2277            var textNodes = lineRow.textNodes;
2278            for (var j = 0; j < textNodes.length; ++j)
2279                this._cachedTextNodes.push(textNodes[j]);
2280            delete lineRow.textNodes;
2281        }
2282        this._cachedRows.push(lineRow);
2283    },
2284
2285    /**
2286     * @param {?Node=} lastUndamagedLineRow
2287     * @return {WebInspector.TextRange}
2288     */
2289    selection: function(lastUndamagedLineRow)
2290    {
2291        var selection = window.getSelection();
2292        if (!selection.rangeCount)
2293            return null;
2294        // Selection may be outside of the editor.
2295        if (!this._container.isAncestor(selection.anchorNode) || !this._container.isAncestor(selection.focusNode))
2296            return null;
2297        // Selection may be inside one of decorations.
2298        if (selection.focusNode.enclosingNodeOrSelfWithClass("webkit-line-decorations", this._container))
2299            return null;
2300        var start = this._selectionToPosition(selection.anchorNode, selection.anchorOffset, lastUndamagedLineRow);
2301        var end = selection.isCollapsed ? start : this._selectionToPosition(selection.focusNode, selection.focusOffset, lastUndamagedLineRow);
2302        return new WebInspector.TextRange(start.line, start.column, end.line, end.column);
2303    },
2304
2305    lastSelection: function()
2306    {
2307        return this._lastSelection;
2308    },
2309
2310    /**
2311     * @param {boolean=} scrollIntoView
2312     */
2313    _restoreSelection: function(range, scrollIntoView)
2314    {
2315        if (!range)
2316            return;
2317
2318        var start = this._positionToSelection(range.startLine, range.startColumn);
2319        var end = range.isEmpty() ? start : this._positionToSelection(range.endLine, range.endColumn);
2320        window.getSelection().setBaseAndExtent(start.container, start.offset, end.container, end.offset);
2321
2322        if (scrollIntoView) {
2323            for (var node = end.container; node; node = node.parentElement) {
2324                if (node.scrollIntoViewIfNeeded) {
2325                    node.scrollIntoViewIfNeeded();
2326                    break;
2327                }
2328            }
2329        }
2330        this._lastSelection = range;
2331    },
2332
2333    /**
2334     * @param {Node} container
2335     * @param {number} offset
2336     * @param {?Node=} lastUndamagedLineRow
2337     * @return {{line: number, column: number}}
2338     */
2339    _selectionToPosition: function(container, offset, lastUndamagedLineRow)
2340    {
2341        if (container === this._container && offset === 0)
2342            return { line: 0, column: 0 };
2343        if (container === this._container && offset === 1)
2344            return { line: this._textModel.linesCount - 1, column: this._textModel.lineLength(this._textModel.linesCount - 1) };
2345
2346        // This method can be called on the damaged DOM (when DOM does not match model).
2347        // We need to start counting lines from the first undamaged line if it is given.
2348        var lineNumber;
2349        var column = 0;
2350        var node;
2351        var scopeNode;
2352        if (lastUndamagedLineRow === null) {
2353             // Last undamaged row is given, but is null - force traverse from the beginning
2354            node = this._container.firstChild;
2355            scopeNode = this._container;
2356            lineNumber = 0;
2357        } else {
2358            var lineRow = this._enclosingLineRowOrSelf(container);
2359            if (!lastUndamagedLineRow || (typeof lineRow.lineNumber === "number" && lineRow.lineNumber <= lastUndamagedLineRow.lineNumber)) {
2360                // DOM is consistent (or we belong to the first damaged row)- lookup the row we belong to and start with it.
2361                node = lineRow;
2362                scopeNode = node;
2363                lineNumber = node.lineNumber;
2364            } else {
2365                // Start with the node following undamaged row. It corresponds to lineNumber + 1.
2366                node = lastUndamagedLineRow.nextSibling;
2367                scopeNode = this._container;
2368                lineNumber = lastUndamagedLineRow.lineNumber + 1;
2369            }
2370        }
2371
2372        // Fast return the line start.
2373        if (container === node && offset === 0)
2374            return { line: lineNumber, column: 0 };
2375
2376        // Traverse text and increment lineNumber / column.
2377        for (; node && node !== container; node = node.traverseNextNode(scopeNode)) {
2378            if (node.nodeName.toLowerCase() === "br") {
2379                lineNumber++;
2380                column = 0;
2381            } else if (node.nodeType === Node.TEXT_NODE) {
2382                var text = node.textContent;
2383                for (var i = 0; i < text.length; ++i) {
2384                    if (text.charAt(i) === "\n") {
2385                        lineNumber++;
2386                        column = 0;
2387                    } else
2388                        column++;
2389                }
2390            }
2391        }
2392
2393        // We reached our container node, traverse within itself until we reach given offset.
2394        if (node === container && offset) {
2395            var text = node.textContent;
2396            // In case offset == 1 and lineRow is a chunk div, we need to traverse it all.
2397            var textOffset = (node._chunk && offset === 1) ? text.length : offset;
2398            for (var i = 0; i < textOffset; ++i) {
2399                if (text.charAt(i) === "\n") {
2400                    lineNumber++;
2401                    column = 0;
2402                } else
2403                    column++;
2404            }
2405        }
2406        return { line: lineNumber, column: column };
2407    },
2408
2409    /**
2410     * @param {number} line
2411     * @param {number} column
2412     * @return {{container: Element, offset: number}}
2413     */
2414    _positionToSelection: function(line, column)
2415    {
2416        var chunk = this.chunkForLine(line);
2417        // One-lined collapsed chunks may still stay highlighted.
2418        var lineRow = chunk.linesCount === 1 ? chunk.element : chunk.expandedLineRow(line);
2419        if (lineRow)
2420            var rangeBoundary = lineRow.rangeBoundaryForOffset(column);
2421        else {
2422            var offset = column;
2423            for (var i = chunk.startLine; i < line && i < this._textModel.linesCount; ++i)
2424                offset += this._textModel.lineLength(i) + 1; // \n
2425            lineRow = chunk.element;
2426            if (lineRow.firstChild)
2427                var rangeBoundary = { container: lineRow.firstChild, offset: offset };
2428            else
2429                var rangeBoundary = { container: lineRow, offset: 0 };
2430        }
2431        return rangeBoundary;
2432    },
2433
2434    /**
2435     * @param {Node} element
2436     * @return {?Node}
2437     */
2438    _enclosingLineRowOrSelf: function(element)
2439    {
2440        var lineRow = element.enclosingNodeOrSelfWithClass("webkit-line-content");
2441        if (lineRow)
2442            return lineRow;
2443
2444        for (lineRow = element; lineRow; lineRow = lineRow.parentElement) {
2445            if (lineRow.parentElement === this._container)
2446                return lineRow;
2447        }
2448        return null;
2449    },
2450
2451    /**
2452     * @param {Element} element
2453     * @param {Element} oldChild
2454     * @param {string} content
2455     * @param {string=} className
2456     */
2457    _insertSpanBefore: function(element, oldChild, content, className)
2458    {
2459        if (className === "html-resource-link" || className === "html-external-link") {
2460            element.insertBefore(this._createLink(content, className === "html-external-link"), oldChild);
2461            return;
2462        }
2463
2464        var span = this._cachedSpans.pop() || document.createElement("span");
2465        if (!className)
2466            span.removeAttribute("class");
2467        else
2468            span.className = className;
2469        if (WebInspector.FALSE) // For paint debugging.
2470            span.addStyleClass("debug-fadeout");
2471        span.textContent = content;
2472        element.insertBefore(span, oldChild);
2473        if (!("spans" in element))
2474            element.spans = [];
2475        element.spans.push(span);
2476    },
2477
2478    /**
2479     * @param {Element} element
2480     * @param {Element} oldChild
2481     * @param {string} text
2482     */
2483    _insertTextNodeBefore: function(element, oldChild, text)
2484    {
2485        var textNode = this._cachedTextNodes.pop();
2486        if (textNode)
2487            textNode.nodeValue = text;
2488        else
2489            textNode = document.createTextNode(text);
2490        element.insertBefore(textNode, oldChild);
2491        if (!("textNodes" in element))
2492            element.textNodes = [];
2493        element.textNodes.push(textNode);
2494    },
2495
2496    /**
2497     * @param {string} content
2498     * @param {boolean} isExternal
2499     * @return {Element}
2500     */
2501    _createLink: function(content, isExternal)
2502    {
2503        var quote = content.charAt(0);
2504        if (content.length > 1 && (quote === "\"" || quote === "'"))
2505            content = content.substring(1, content.length - 1);
2506        else
2507            quote = null;
2508
2509        var span = document.createElement("span");
2510        span.className = "webkit-html-attribute-value";
2511        if (quote)
2512            span.appendChild(document.createTextNode(quote));
2513        span.appendChild(this._delegate.createLink(content, isExternal));
2514        if (quote)
2515            span.appendChild(document.createTextNode(quote));
2516        return span;
2517    },
2518
2519    /**
2520     * @param {Array.<WebKitMutation>} mutations
2521     */
2522    _handleMutations: function(mutations)
2523    {
2524        if (this._readOnly) {
2525            delete this._keyDownCode;
2526            return;
2527        }
2528
2529        // Annihilate noop BR addition + removal that takes place upon line removal.
2530        var filteredMutations = mutations.slice();
2531        var addedBRs = new Map();
2532        for (var i = 0; i < mutations.length; ++i) {
2533            var mutation = mutations[i];
2534            if (mutation.type !== "childList")
2535                continue;
2536            if (mutation.addedNodes.length === 1 && mutation.addedNodes[0].nodeName === "BR")
2537                addedBRs.put(mutation.addedNodes[0], mutation);
2538            else if (mutation.removedNodes.length === 1 && mutation.removedNodes[0].nodeName === "BR") {
2539                var noopMutation = addedBRs.get(mutation.removedNodes[0]);
2540                if (noopMutation) {
2541                    filteredMutations.remove(mutation);
2542                    filteredMutations.remove(noopMutation);
2543                }
2544            }
2545        }
2546
2547        var dirtyLines;
2548        for (var i = 0; i < filteredMutations.length; ++i) {
2549            var mutation = filteredMutations[i];
2550            var changedNodes = [];
2551            if (mutation.type === "childList" && mutation.addedNodes.length)
2552                changedNodes = Array.prototype.slice.call(mutation.addedNodes);
2553            else if (mutation.type === "childList" && mutation.removedNodes.length)
2554                changedNodes = Array.prototype.slice.call(mutation.removedNodes);
2555            changedNodes.push(mutation.target);
2556
2557            for (var j = 0; j < changedNodes.length; ++j) {
2558                var lines = this._collectDirtyLines(mutation, changedNodes[j]);
2559                if (!lines)
2560                    continue;
2561                if (!dirtyLines) {
2562                    dirtyLines = lines;
2563                    continue;
2564                }
2565                dirtyLines.start = Math.min(dirtyLines.start, lines.start);
2566                dirtyLines.end = Math.max(dirtyLines.end, lines.end);
2567            }
2568        }
2569        if (dirtyLines) {
2570            delete this._rangeToMark;
2571            this._applyDomUpdates(dirtyLines);
2572        }
2573
2574        this._assertDOMMatchesTextModel();
2575
2576        delete this._keyDownCode;
2577    },
2578
2579    /**
2580     * @param {WebKitMutation} mutation
2581     * @param {Node} target
2582     * @return {?Object}
2583     */
2584    _collectDirtyLines: function(mutation, target)
2585    {
2586        var lineRow = this._enclosingLineRowOrSelf(target);
2587        if (!lineRow)
2588            return null;
2589
2590        if (lineRow.decorationsElement && lineRow.decorationsElement.isSelfOrAncestor(target)) {
2591            if (this._syncDecorationsForLineListener)
2592                this._syncDecorationsForLineListener(lineRow.lineNumber);
2593            return null;
2594        }
2595
2596        if (typeof lineRow.lineNumber !== "number")
2597            return null;
2598
2599        var startLine = lineRow.lineNumber;
2600        var endLine = lineRow._chunk ? lineRow._chunk.endLine - 1 : lineRow.lineNumber;
2601        return { start: startLine, end: endLine };
2602    },
2603
2604    /**
2605     * @param {Object} dirtyLines
2606     */
2607    _applyDomUpdates: function(dirtyLines)
2608    {
2609        var lastUndamagedLineNumber = dirtyLines.start - 1; // Can be -1
2610        var firstUndamagedLineNumber = dirtyLines.end + 1; // Can be this._textModel.linesCount
2611
2612        var lastUndamagedLineChunk = lastUndamagedLineNumber >= 0 ? this._textChunks[this.chunkNumberForLine(lastUndamagedLineNumber)] : null;
2613        var firstUndamagedLineChunk = firstUndamagedLineNumber < this._textModel.linesCount ? this._textChunks[this.chunkNumberForLine(firstUndamagedLineNumber)] : null;
2614
2615        var collectLinesFromNode = lastUndamagedLineChunk ? lastUndamagedLineChunk.lineRowContainingLine(lastUndamagedLineNumber) : null;
2616        var collectLinesToNode = firstUndamagedLineChunk ? firstUndamagedLineChunk.lineRowContainingLine(firstUndamagedLineNumber) : null;
2617        var lines = this._collectLinesFromDOM(collectLinesFromNode, collectLinesToNode);
2618
2619        var startLine = dirtyLines.start;
2620        var endLine = dirtyLines.end;
2621
2622        var originalSelection = this._lastSelection;
2623        var editInfo = this._guessEditRangeBasedOnSelection(startLine, endLine, lines);
2624        if (!editInfo) {
2625            if (WebInspector.debugDefaultTextEditor)
2626                console.warn("Falling back to expensive edit");
2627            var range = new WebInspector.TextRange(startLine, 0, endLine, this._textModel.lineLength(endLine));
2628            if (!lines.length) {
2629                // Entire damaged area has collapsed. Replace everything between start and end lines with nothing.
2630                editInfo = new WebInspector.DefaultTextEditor.EditInfo(this._textModel.growRangeRight(range), "");
2631            } else
2632                editInfo = new WebInspector.DefaultTextEditor.EditInfo(range, lines.join("\n"));
2633        }
2634
2635        var selection = this.selection(collectLinesFromNode);
2636
2637        // Unindent after block
2638        if (editInfo.text === "}" && editInfo.range.isEmpty() && selection.isEmpty() && !this._textModel.line(editInfo.range.endLine).trim()) {
2639            var offset = this._closingBlockOffset(editInfo.range);
2640            if (offset >= 0) {
2641                editInfo.range.startColumn = offset;
2642                selection.startColumn = offset + 1;
2643                selection.endColumn = offset + 1;
2644            }
2645        }
2646
2647        this._textModel.editRange(editInfo.range, editInfo.text, originalSelection);
2648        this._restoreSelection(selection);
2649    },
2650
2651    /**
2652     * @param {number} startLine
2653     * @param {number} endLine
2654     * @param {Array.<string>} lines
2655     * @return {?WebInspector.DefaultTextEditor.EditInfo}
2656     */
2657    _guessEditRangeBasedOnSelection: function(startLine, endLine, lines)
2658    {
2659        // Analyze input data
2660        var textInputData = this._textInputData;
2661        delete this._textInputData;
2662        var isBackspace = this._keyDownCode === WebInspector.KeyboardShortcut.Keys.Backspace.code;
2663        var isDelete = this._keyDownCode === WebInspector.KeyboardShortcut.Keys.Delete.code;
2664
2665        if (!textInputData && (isDelete || isBackspace))
2666            textInputData = "";
2667
2668        // Return if there is no input data or selection
2669        if (typeof textInputData === "undefined" || !this._lastSelection)
2670            return null;
2671
2672        // Adjust selection based on the keyboard actions (grow for backspace, etc.).
2673        textInputData = textInputData || "";
2674        var range = this._lastSelection.normalize();
2675        if (isBackspace && range.isEmpty())
2676            range = this._textModel.growRangeLeft(range);
2677        else if (isDelete && range.isEmpty())
2678            range = this._textModel.growRangeRight(range);
2679
2680        // Test that selection intersects damaged lines
2681        if (startLine > range.endLine || endLine < range.startLine)
2682            return null;
2683
2684        var replacementLineCount = textInputData.split("\n").length - 1;
2685        var lineCountDelta = replacementLineCount - range.linesCount;
2686        if (startLine + lines.length - endLine - 1 !== lineCountDelta)
2687            return null;
2688
2689        // Clone text model of the size that fits both: selection before edit and the damaged lines after edit.
2690        var cloneFromLine = Math.min(range.startLine, startLine);
2691        var postLastLine = startLine + lines.length + lineCountDelta;
2692        var cloneToLine = Math.min(Math.max(postLastLine, range.endLine) + 1, this._textModel.linesCount);
2693        var domModel = this._textModel.slice(cloneFromLine, cloneToLine);
2694        domModel.editRange(range.shift(-cloneFromLine), textInputData);
2695
2696        // Then we'll test if this new model matches the DOM lines.
2697        for (var i = 0; i < lines.length; ++i) {
2698            if (domModel.line(i + startLine - cloneFromLine) !== lines[i])
2699                return null;
2700        }
2701        return new WebInspector.DefaultTextEditor.EditInfo(range, textInputData);
2702    },
2703
2704    _assertDOMMatchesTextModel: function()
2705    {
2706        if (!WebInspector.debugDefaultTextEditor)
2707            return;
2708
2709        console.assert(this.element.innerText === this._textModel.text() + "\n", "DOM does not match model.");
2710        for (var lineRow = this._container.firstChild; lineRow; lineRow = lineRow.nextSibling) {
2711            var lineNumber = lineRow.lineNumber;
2712            if (typeof lineNumber !== "number") {
2713                console.warn("No line number on line row");
2714                continue;
2715            }
2716            if (lineRow._chunk) {
2717                var chunk = lineRow._chunk;
2718                console.assert(lineNumber === chunk.startLine);
2719                var chunkText = this._textModel.copyRange(new WebInspector.TextRange(chunk.startLine, 0, chunk.endLine - 1, this._textModel.lineLength(chunk.endLine - 1)));
2720                if (chunkText !== lineRow.textContent)
2721                    console.warn("Chunk is not matching: %d %O", lineNumber, lineRow);
2722            } else if (this._textModel.line(lineNumber) !== lineRow.textContent)
2723                console.warn("Line is not matching: %d %O", lineNumber, lineRow);
2724        }
2725    },
2726
2727    /**
2728     * @param {WebInspector.TextRange} oldRange
2729     * @return {number}
2730     */
2731    _closingBlockOffset: function(oldRange)
2732    {
2733        var leftBrace = this._braceMatcher.findLeftCandidate(oldRange.startLine, oldRange.startColumn);
2734        if (!leftBrace || leftBrace.token !== "block-start")
2735            return -1;
2736        var lineContent = this._textModel.line(leftBrace.lineNumber);
2737        return lineContent.length - lineContent.trimLeft().length;
2738    },
2739
2740    /**
2741     * @param {WebInspector.TextRange} oldRange
2742     * @param {WebInspector.TextRange} newRange
2743     */
2744    textChanged: function(oldRange, newRange)
2745    {
2746        this.beginDomUpdates();
2747        this._removeDecorationsInRange(oldRange);
2748        this._updateChunksForRanges(oldRange, newRange);
2749        this._updateHighlightsForRange(newRange);
2750        this.endDomUpdates();
2751    },
2752
2753    /**
2754     * @param {WebInspector.TextRange} range
2755     */
2756    _removeDecorationsInRange: function(range)
2757    {
2758        for (var i = this.chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i) {
2759            var chunk = this._textChunks[i];
2760            if (chunk.startLine > range.endLine)
2761                break;
2762            chunk.removeAllDecorations();
2763        }
2764    },
2765
2766    /**
2767     * @param {WebInspector.TextRange} oldRange
2768     * @param {WebInspector.TextRange} newRange
2769     */
2770    _updateChunksForRanges: function(oldRange, newRange)
2771    {
2772        var firstDamagedChunkNumber = this.chunkNumberForLine(oldRange.startLine);
2773        var lastDamagedChunkNumber = firstDamagedChunkNumber;
2774        while (lastDamagedChunkNumber + 1 < this._textChunks.length) {
2775            if (this._textChunks[lastDamagedChunkNumber + 1].startLine > oldRange.endLine)
2776                break;
2777            ++lastDamagedChunkNumber;
2778        }
2779
2780        var firstDamagedChunk = this._textChunks[firstDamagedChunkNumber];
2781        var lastDamagedChunk = this._textChunks[lastDamagedChunkNumber];
2782
2783        var linesDiff = newRange.linesCount - oldRange.linesCount;
2784
2785        // First, detect chunks that have not been modified and simply shift them.
2786        if (linesDiff) {
2787            for (var chunkNumber = lastDamagedChunkNumber + 1; chunkNumber < this._textChunks.length; ++chunkNumber)
2788                this._textChunks[chunkNumber].startLine += linesDiff;
2789        }
2790
2791        // Remove damaged chunks from DOM and from textChunks model.
2792        var lastUndamagedChunk = firstDamagedChunkNumber > 0 ? this._textChunks[firstDamagedChunkNumber - 1] : null;
2793        var firstUndamagedChunk = lastDamagedChunkNumber + 1 < this._textChunks.length ? this._textChunks[lastDamagedChunkNumber + 1] : null;
2794
2795        var removeDOMFromNode = lastUndamagedChunk ? lastUndamagedChunk.lastElement().nextSibling : this._container.firstChild;
2796        var removeDOMToNode = firstUndamagedChunk ? firstUndamagedChunk.firstElement() : null;
2797
2798        // Fast case - patch single expanded chunk that did not grow / shrink during edit.
2799        if (!linesDiff && firstDamagedChunk === lastDamagedChunk && firstDamagedChunk._expandedLineRows) {
2800            var lastUndamagedLineRow = lastDamagedChunk.expandedLineRow(oldRange.startLine - 1);
2801            var firstUndamagedLineRow = firstDamagedChunk.expandedLineRow(oldRange.endLine + 1);
2802            var localRemoveDOMFromNode = lastUndamagedLineRow ? lastUndamagedLineRow.nextSibling : removeDOMFromNode;
2803            var localRemoveDOMToNode = firstUndamagedLineRow || removeDOMToNode;
2804            removeSubsequentNodes(localRemoveDOMFromNode, localRemoveDOMToNode);
2805            for (var i = newRange.startLine; i < newRange.endLine + 1; ++i) {
2806                var row = firstDamagedChunk._createRow(i);
2807                firstDamagedChunk._expandedLineRows[i - firstDamagedChunk.startLine] = row;
2808                this._container.insertBefore(row, localRemoveDOMToNode);
2809            }
2810            firstDamagedChunk.updateCollapsedLineRow();
2811            this._assertDOMMatchesTextModel();
2812            return;
2813        }
2814
2815        removeSubsequentNodes(removeDOMFromNode, removeDOMToNode);
2816        this._textChunks.splice(firstDamagedChunkNumber, lastDamagedChunkNumber - firstDamagedChunkNumber + 1);
2817
2818        // Compute damaged chunks span
2819        var startLine = firstDamagedChunk.startLine;
2820        var endLine = lastDamagedChunk.endLine + linesDiff;
2821        var lineSpan = endLine - startLine;
2822
2823        // Re-create chunks for damaged area.
2824        var insertionIndex = firstDamagedChunkNumber;
2825        var chunkSize = Math.ceil(lineSpan / Math.ceil(lineSpan / this._defaultChunkSize));
2826
2827        for (var i = startLine; i < endLine; i += chunkSize) {
2828            var chunk = this.createNewChunk(i, Math.min(endLine, i + chunkSize));
2829            this._textChunks.splice(insertionIndex++, 0, chunk);
2830            this._container.insertBefore(chunk.element, removeDOMToNode);
2831        }
2832
2833        this._assertDOMMatchesTextModel();
2834    },
2835
2836    /**
2837     * @param {WebInspector.TextRange} range
2838     */
2839    _updateHighlightsForRange: function(range)
2840    {
2841        var visibleFrom = this.scrollTop();
2842        var visibleTo = visibleFrom + this.clientHeight();
2843
2844        var result = this.findVisibleChunks(visibleFrom, visibleTo);
2845        var chunk = this._textChunks[result.end - 1];
2846        var lastVisibleLine = chunk.startLine + chunk.linesCount;
2847
2848        lastVisibleLine = Math.max(lastVisibleLine, range.endLine + 1);
2849        lastVisibleLine = Math.min(lastVisibleLine, this._textModel.linesCount);
2850
2851        var updated = this._highlighter.updateHighlight(range.startLine, lastVisibleLine);
2852        if (!updated) {
2853            // Highlights for the chunks below are invalid, so just collapse them.
2854            for (var i = this.chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i)
2855                this._textChunks[i].collapse();
2856        }
2857
2858        this.repaintAll();
2859    },
2860
2861    /**
2862     * @param {Node} from
2863     * @param {Node} to
2864     * @return {Array.<string>}
2865     */
2866    _collectLinesFromDOM: function(from, to)
2867    {
2868        var textContents = [];
2869        var hasContent = false;
2870        for (var node = from ? from.nextSibling : this._container; node && node !== to; node = node.traverseNextNode(this._container)) {
2871            // Skip all children of the decoration container and overlay highlight spans.
2872            while (node && node !== to && (node._isDecorationsElement || node._isOverlayHighlightElement))
2873                node = node.nextSibling;
2874            if (!node || node === to)
2875                break;
2876
2877            hasContent = true;
2878            if (node.nodeName.toLowerCase() === "br")
2879                textContents.push("\n");
2880            else if (node.nodeType === Node.TEXT_NODE)
2881                textContents.push(node.textContent);
2882        }
2883        if (!hasContent)
2884            return [];
2885
2886        var textContent = textContents.join("");
2887        // The last \n (if any) does not "count" in a DIV.
2888        textContent = textContent.replace(/\n$/, "");
2889
2890        return textContent.split("\n");
2891    },
2892
2893    /**
2894     * @param {Event} event
2895     */
2896    _handleSelectionChange: function(event)
2897    {
2898        var textRange = this.selection();
2899        if (textRange)
2900            this._lastSelection = textRange;
2901
2902        this._tokenHighlighter.handleSelectionChange(textRange);
2903        this._braceHighlighter.handleSelectionChange(textRange);
2904        this._delegate.selectionChanged(textRange);
2905    },
2906
2907    /**
2908     * @param {Event} event
2909     */
2910    _handleTextInput: function(event)
2911    {
2912        this._textInputData = event.data;
2913    },
2914
2915    /**
2916     * @param {number} shortcutKey
2917     * @param {Event} event
2918     */
2919    handleKeyDown: function(shortcutKey, event)
2920    {
2921        var handler = this._shortcuts[shortcutKey];
2922        if (handler && handler()) {
2923            event.consume(true);
2924            return;
2925        }
2926
2927        this._keyDownCode = event.keyCode;
2928    },
2929
2930    /**
2931     * @param {Event} event
2932     */
2933    _handleCut: function(event)
2934    {
2935        this._keyDownCode = WebInspector.KeyboardShortcut.Keys.Delete.code;
2936    },
2937
2938    /**
2939     * @param {number} scrollTop
2940     * @param {number} clientHeight
2941     * @param {number} chunkSize
2942     */
2943    overrideViewportForTest: function(scrollTop, clientHeight, chunkSize)
2944    {
2945        this._scrollTopOverrideForTest = scrollTop;
2946        this._clientHeightOverrideForTest = clientHeight;
2947        this._defaultChunkSize = chunkSize;
2948    },
2949
2950    __proto__: WebInspector.TextEditorChunkedPanel.prototype
2951}
2952
2953/**
2954 * @interface
2955 */
2956WebInspector.TextEditorMainPanel.HighlightDescriptor = function() { }
2957
2958WebInspector.TextEditorMainPanel.HighlightDescriptor.prototype = {
2959    /**
2960     * @param {number} lineNumber
2961     * @param {string} line
2962     * @return {boolean}
2963     */
2964    affectsLine: function(lineNumber, line) { return false; },
2965
2966    /**
2967     * @param {number} lineNumber
2968     * @param {string} line
2969     * @return {Array.<{startColumn: number, endColumn: number}>}
2970     */
2971    rangesForLine: function(lineNumber, line) { return []; },
2972
2973    /**
2974     * @return {string}
2975     */
2976    cssClass: function() { return ""; },
2977}
2978
2979/**
2980 * @constructor
2981 * @implements {WebInspector.TextEditorMainPanel.HighlightDescriptor}
2982 */
2983WebInspector.TextEditorMainPanel.RegexHighlightDescriptor = function(regex, cssClass)
2984{
2985    this._cssClass = cssClass;
2986    this._regex = regex;
2987}
2988
2989WebInspector.TextEditorMainPanel.RegexHighlightDescriptor.prototype = {
2990    /**
2991     * @param {number} lineNumber
2992     * @param {string} line
2993     * @return {boolean}
2994     */
2995    affectsLine: function(lineNumber, line)
2996    {
2997        this._regex.lastIndex = 0;
2998        return this._regex.test(line);
2999    },
3000
3001    /**
3002     * @param {number} lineNumber
3003     * @param {string} line
3004     * @return {Array.<{startColumn: number, endColumn: number}>}
3005     */
3006    rangesForLine: function(lineNumber, line)
3007    {
3008        var ranges = [];
3009        var regexResult;
3010        this._regex.lastIndex = 0;
3011        while (regexResult = this._regex.exec(line)) {
3012            ranges.push({
3013                startColumn: regexResult.index,
3014                endColumn: regexResult.index + regexResult[0].length - 1
3015            });
3016        }
3017        return ranges;
3018    },
3019
3020    /**
3021     * @return {string}
3022     */
3023    cssClass: function()
3024    {
3025        return this._cssClass;
3026    }
3027}
3028
3029/**
3030 * @constructor
3031 * @implements {WebInspector.TextEditorMainPanel.HighlightDescriptor}
3032 * @param {WebInspector.TextRange} range
3033 * @param {string} cssClass
3034 */
3035WebInspector.TextEditorMainPanel.RangeHighlightDescriptor = function(range, cssClass)
3036{
3037    this._cssClass = cssClass;
3038    this._range = range;
3039}
3040
3041WebInspector.TextEditorMainPanel.RangeHighlightDescriptor.prototype = {
3042    /**
3043     * @param {number} lineNumber
3044     * @param {string} line
3045     * @return {boolean}
3046     */
3047    affectsLine: function(lineNumber, line)
3048    {
3049        return this._range.startLine <= lineNumber && lineNumber <= this._range.endLine && line.length > 0;
3050    },
3051
3052    /**
3053     * @param {number} lineNumber
3054     * @param {string} line
3055     * @return {Array.<{startColumn: number, endColumn: number}>}
3056     */
3057    rangesForLine: function(lineNumber, line)
3058    {
3059        if (!this.affectsLine(lineNumber, line))
3060            return [];
3061
3062        var startColumn = lineNumber === this._range.startLine ? this._range.startColumn : 0;
3063        var endColumn = lineNumber === this._range.endLine ? Math.min(this._range.endColumn, line.length) : line.length;
3064        return [{
3065            startColumn: startColumn,
3066            endColumn: endColumn
3067        }];
3068    },
3069
3070    /**
3071     * @return {string}
3072     */
3073    cssClass: function()
3074    {
3075        return this._cssClass;
3076    }
3077}
3078
3079/**
3080 * @constructor
3081 * @param {Element} element
3082 */
3083WebInspector.TextEditorMainPanel.ElementMetrics = function(element)
3084{
3085    this.width = element.offsetWidth;
3086    this.height = element.offsetHeight;
3087    this.left = element.offsetLeft;
3088}
3089
3090/**
3091 * @constructor
3092 * @param {Array.<WebInspector.TextEditorMainPanel.ElementMetrics>} metrics
3093 * @param {string} cssClass
3094 */
3095WebInspector.TextEditorMainPanel.LineOverlayHighlight = function(metrics, cssClass)
3096{
3097    this.metrics = metrics;
3098    this.cssClass = cssClass;
3099}
3100
3101/**
3102 * @constructor
3103 * @param {WebInspector.TextEditorChunkedPanel} chunkedPanel
3104 * @param {number} startLine
3105 * @param {number} endLine
3106 */
3107WebInspector.TextEditorMainChunk = function(chunkedPanel, startLine, endLine)
3108{
3109    this._chunkedPanel = chunkedPanel;
3110    this._textModel = chunkedPanel._textModel;
3111
3112    this.element = document.createElement("div");
3113    this.element.lineNumber = startLine;
3114    this.element.className = "webkit-line-content";
3115    this.element._chunk = this;
3116
3117    this._startLine = startLine;
3118    endLine = Math.min(this._textModel.linesCount, endLine);
3119    this.linesCount = endLine - startLine;
3120
3121    this._expanded = false;
3122
3123    this.updateCollapsedLineRow();
3124}
3125
3126WebInspector.TextEditorMainChunk.prototype = {
3127    /**
3128     * @param {Element|string} decoration
3129     */
3130    addDecoration: function(decoration)
3131    {
3132        this._chunkedPanel.beginDomUpdates();
3133        if (typeof decoration === "string")
3134            this.element.addStyleClass(decoration);
3135        else {
3136            if (!this.element.decorationsElement) {
3137                this.element.decorationsElement = document.createElement("div");
3138                this.element.decorationsElement.className = "webkit-line-decorations";
3139                this.element.decorationsElement._isDecorationsElement = true;
3140                this.element.appendChild(this.element.decorationsElement);
3141            }
3142            this.element.decorationsElement.appendChild(decoration);
3143        }
3144        this._chunkedPanel.endDomUpdates();
3145    },
3146
3147    /**
3148     * @param {string|Element} decoration
3149     */
3150    removeDecoration: function(decoration)
3151    {
3152        this._chunkedPanel.beginDomUpdates();
3153        if (typeof decoration === "string")
3154            this.element.removeStyleClass(decoration);
3155        else if (this.element.decorationsElement)
3156            this.element.decorationsElement.removeChild(decoration);
3157        this._chunkedPanel.endDomUpdates();
3158    },
3159
3160    removeAllDecorations: function()
3161    {
3162        this._chunkedPanel.beginDomUpdates();
3163        this.element.className = "webkit-line-content";
3164        if (this.element.decorationsElement) {
3165            if (this.element.decorationsElement.parentElement)
3166                this.element.removeChild(this.element.decorationsElement);
3167            delete this.element.decorationsElement;
3168        }
3169        this._chunkedPanel.endDomUpdates();
3170    },
3171
3172    /**
3173     * @return {boolean}
3174     */
3175    isDecorated: function()
3176    {
3177        return this.element.className !== "webkit-line-content" || !!(this.element.decorationsElement && this.element.decorationsElement.firstChild);
3178    },
3179
3180    /**
3181     * @return {number}
3182     */
3183    get startLine()
3184    {
3185        return this._startLine;
3186    },
3187
3188    /**
3189     * @return {number}
3190     */
3191    get endLine()
3192    {
3193        return this._startLine + this.linesCount;
3194    },
3195
3196    set startLine(startLine)
3197    {
3198        this._startLine = startLine;
3199        this.element.lineNumber = startLine;
3200        if (this._expandedLineRows) {
3201            for (var i = 0; i < this._expandedLineRows.length; ++i)
3202                this._expandedLineRows[i].lineNumber = startLine + i;
3203        }
3204    },
3205
3206    /**
3207     * @return {boolean}
3208     */
3209    expanded: function()
3210    {
3211        return this._expanded;
3212    },
3213
3214    expand: function()
3215    {
3216        if (this._expanded)
3217            return;
3218
3219        this._expanded = true;
3220
3221        if (this.linesCount === 1) {
3222            this._chunkedPanel._paintLines(this.startLine, this.startLine + 1);
3223            return;
3224        }
3225
3226        this._chunkedPanel.beginDomUpdates();
3227
3228        this._expandedLineRows = [];
3229        var parentElement = this.element.parentElement;
3230        for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
3231            var lineRow = this._createRow(i);
3232            parentElement.insertBefore(lineRow, this.element);
3233            this._expandedLineRows.push(lineRow);
3234        }
3235        parentElement.removeChild(this.element);
3236        this._chunkedPanel._paintLines(this.startLine, this.startLine + this.linesCount);
3237
3238        this._chunkedPanel.endDomUpdates();
3239    },
3240
3241    collapse: function()
3242    {
3243        if (!this._expanded)
3244            return;
3245
3246        this._expanded = false;
3247        if (this.linesCount === 1)
3248            return;
3249
3250        this._chunkedPanel.beginDomUpdates();
3251
3252        var elementInserted = false;
3253        for (var i = 0; i < this._expandedLineRows.length; ++i) {
3254            var lineRow = this._expandedLineRows[i];
3255            var parentElement = lineRow.parentElement;
3256            if (parentElement) {
3257                if (!elementInserted) {
3258                    elementInserted = true;
3259                    parentElement.insertBefore(this.element, lineRow);
3260                }
3261                parentElement.removeChild(lineRow);
3262            }
3263            this._chunkedPanel._releaseLinesHighlight(lineRow);
3264        }
3265        delete this._expandedLineRows;
3266
3267        this._chunkedPanel.endDomUpdates();
3268    },
3269
3270    /**
3271     * @return {number}
3272     */
3273    get height()
3274    {
3275        if (!this._expandedLineRows)
3276            return this._chunkedPanel.totalHeight(this.element);
3277        return this._chunkedPanel.totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]);
3278    },
3279
3280    /**
3281     * @return {number}
3282     */
3283    get offsetTop()
3284    {
3285        return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
3286    },
3287
3288    /**
3289     * @param {number} lineNumber
3290     * @return {Element}
3291     */
3292    _createRow: function(lineNumber)
3293    {
3294        var lineRow = this._chunkedPanel._cachedRows.pop() || document.createElement("div");
3295        lineRow.lineNumber = lineNumber;
3296        lineRow.className = "webkit-line-content";
3297        lineRow.textContent = this._textModel.line(lineNumber);
3298        if (!lineRow.textContent)
3299            lineRow.appendChild(document.createElement("br"));
3300        return lineRow;
3301    },
3302
3303    /**
3304     * Called on potentially damaged / inconsistent chunk
3305     * @param {number} lineNumber
3306     * @return {?Node}
3307     */
3308    lineRowContainingLine: function(lineNumber)
3309    {
3310        if (!this._expanded)
3311            return this.element;
3312        return this.expandedLineRow(lineNumber);
3313    },
3314
3315    /**
3316     * @param {number} lineNumber
3317     * @return {Element}
3318     */
3319    expandedLineRow: function(lineNumber)
3320    {
3321        if (!this._expanded || lineNumber < this.startLine || lineNumber >= this.startLine + this.linesCount)
3322            return null;
3323        if (!this._expandedLineRows)
3324            return this.element;
3325        return this._expandedLineRows[lineNumber - this.startLine];
3326    },
3327
3328    updateCollapsedLineRow: function()
3329    {
3330        if (this.linesCount === 1 && this._expanded)
3331            return;
3332
3333        var lines = [];
3334        for (var i = this.startLine; i < this.startLine + this.linesCount; ++i)
3335            lines.push(this._textModel.line(i));
3336
3337        if (WebInspector.FALSE)
3338            console.log("Rebuilding chunk with " + lines.length + " lines");
3339
3340        this.element.removeChildren();
3341        this.element.textContent = lines.join("\n");
3342        // The last empty line will get swallowed otherwise.
3343        if (!lines[lines.length - 1])
3344            this.element.appendChild(document.createElement("br"));
3345    },
3346
3347    firstElement: function()
3348    {
3349        return this._expandedLineRows ? this._expandedLineRows[0] : this.element;
3350    },
3351
3352    /**
3353     * @return {Element}
3354     */
3355    lastElement: function()
3356    {
3357        return this._expandedLineRows ? this._expandedLineRows[this._expandedLineRows.length - 1] : this.element;
3358    }
3359}
3360
3361/**
3362 * @constructor
3363 * @param {WebInspector.TextEditorMainPanel} mainPanel
3364 * @param {WebInspector.TextEditorModel} textModel
3365 */
3366WebInspector.TextEditorMainPanel.TokenHighlighter = function(mainPanel, textModel)
3367{
3368    this._mainPanel = mainPanel;
3369    this._textModel = textModel;
3370}
3371
3372WebInspector.TextEditorMainPanel.TokenHighlighter.prototype = {
3373    /**
3374     * @param {WebInspector.TextRange} range
3375     */
3376    handleSelectionChange: function(range)
3377    {
3378        if (!range) {
3379            this._removeHighlight();
3380            return;
3381        }
3382
3383        if (range.startLine !== range.endLine) {
3384            this._removeHighlight();
3385            return;
3386        }
3387
3388        range = range.normalize();
3389        var selectedText = this._textModel.copyRange(range);
3390        if (selectedText === this._selectedWord)
3391            return;
3392
3393        if (selectedText === "") {
3394            this._removeHighlight();
3395            return;
3396        }
3397
3398        if (this._isWord(range, selectedText))
3399            this._highlight(selectedText);
3400        else
3401            this._removeHighlight();
3402    },
3403
3404    /**
3405     * @param {string} word
3406     */
3407    _regexString: function(word)
3408    {
3409        return "\\b" + word + "\\b";
3410    },
3411
3412    /**
3413     * @param {string} selectedWord
3414     */
3415    _highlight: function(selectedWord)
3416    {
3417        this._removeHighlight();
3418        this._selectedWord = selectedWord;
3419        this._highlightDescriptor = this._mainPanel.highlightRegex(this._regexString(selectedWord), "text-editor-token-highlight")
3420    },
3421
3422    _removeHighlight: function()
3423    {
3424        if (this._selectedWord) {
3425            this._mainPanel.removeHighlight(this._highlightDescriptor);
3426            delete this._selectedWord;
3427            delete this._highlightDescriptor;
3428        }
3429    },
3430
3431    /**
3432     * @param {WebInspector.TextRange} range
3433     * @param {string} selectedText
3434     * @return {boolean}
3435     */
3436    _isWord: function(range, selectedText)
3437    {
3438        var line = this._textModel.line(range.startLine);
3439        var leftBound = range.startColumn === 0 || !WebInspector.TextUtils.isWordChar(line.charAt(range.startColumn - 1));
3440        var rightBound = range.endColumn === line.length || !WebInspector.TextUtils.isWordChar(line.charAt(range.endColumn));
3441        return leftBound && rightBound && WebInspector.TextUtils.isWord(selectedText);
3442    }
3443}
3444
3445/**
3446 * @constructor
3447 * @param {WebInspector.TextEditorModel} textModel
3448 * @param {WebInspector.TextEditor} textEditor
3449 */
3450WebInspector.DefaultTextEditor.WordMovementController = function(textEditor, textModel)
3451{
3452    this._textModel = textModel;
3453    this._textEditor = textEditor;
3454}
3455
3456WebInspector.DefaultTextEditor.WordMovementController.prototype = {
3457
3458    /**
3459     * @param {Object.<number, function()>} shortcuts
3460     */
3461    _registerShortcuts: function(shortcuts)
3462    {
3463        var keys = WebInspector.KeyboardShortcut.Keys;
3464        var modifiers = WebInspector.KeyboardShortcut.Modifiers;
3465
3466        const wordJumpModifier = WebInspector.isMac() ? modifiers.Alt : modifiers.Ctrl;
3467        shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Backspace.code, wordJumpModifier)] = this._handleCtrlBackspace.bind(this);
3468        shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Left.code, wordJumpModifier)] = this._handleCtrlArrow.bind(this, "left");
3469        shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Right.code, wordJumpModifier)] = this._handleCtrlArrow.bind(this, "right");
3470        shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Left.code, modifiers.Shift | wordJumpModifier)] = this._handleCtrlShiftArrow.bind(this, "left");
3471        shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Right.code, modifiers.Shift | wordJumpModifier)] = this._handleCtrlShiftArrow.bind(this, "right");
3472    },
3473
3474    /**
3475     * @param {WebInspector.TextRange} selection
3476     * @param {string} direction
3477     * @return {WebInspector.TextRange}
3478     */
3479    _rangeForCtrlArrowMove: function(selection, direction)
3480    {
3481        const isStopChar = WebInspector.TextUtils.isStopChar;
3482        const isSpaceChar = WebInspector.TextUtils.isSpaceChar;
3483
3484        var lineNumber = selection.endLine;
3485        var column = selection.endColumn;
3486        if (direction === "left")
3487            --column;
3488
3489        if (column === -1 && direction === "left") {
3490            if (lineNumber > 0)
3491                return new WebInspector.TextRange(selection.startLine, selection.startColumn, lineNumber - 1, this._textModel.line(lineNumber - 1).length);
3492            else
3493                return selection.clone();
3494        }
3495
3496        var line = this._textModel.line(lineNumber);
3497        if (column === line.length && direction === "right") {
3498            if (lineNumber + 1 < this._textModel.linesCount)
3499                return new WebInspector.TextRange(selection.startLine, selection.startColumn, selection.endLine + 1, 0);
3500            else
3501                return selection.clone();
3502        }
3503
3504        var delta = direction === "left" ? -1 : +1;
3505        var directionDependentEndColumnOffset = (delta + 1) / 2;
3506
3507        if (isSpaceChar(line.charAt(column))) {
3508            while(column + delta >= 0 && column + delta < line.length && isSpaceChar(line.charAt(column + delta)))
3509                column += delta;
3510            if (column + delta < 0 || column + delta === line.length)
3511                return new WebInspector.TextRange(selection.startLine, selection.startColumn, lineNumber, column + directionDependentEndColumnOffset);
3512            else
3513                column += delta;
3514        }
3515
3516        var group = isStopChar(line.charAt(column));
3517
3518        while(column + delta >= 0 && column + delta < line.length && isStopChar(line.charAt(column + delta)) === group && !isSpaceChar(line.charAt(column + delta)))
3519            column += delta;
3520
3521        return new WebInspector.TextRange(selection.startLine, selection.startColumn, lineNumber, column + directionDependentEndColumnOffset);
3522    },
3523
3524    /**
3525     * @param {string} direction
3526     * @return {boolean}
3527     */
3528    _handleCtrlArrow: function(direction)
3529    {
3530        var newSelection = this._rangeForCtrlArrowMove(this._textEditor.selection(), direction);
3531        this._textEditor.setSelection(newSelection.collapseToEnd());
3532        return true;
3533    },
3534
3535    /**
3536     * @param {string} direction
3537     * @return {boolean}
3538     */
3539    _handleCtrlShiftArrow: function(direction)
3540    {
3541        this._textEditor.setSelection(this._rangeForCtrlArrowMove(this._textEditor.selection(), direction));
3542        return true;
3543    },
3544
3545    /**
3546     * @return {boolean}
3547     */
3548    _handleCtrlBackspace: function()
3549    {
3550        var selection = this._textEditor.selection();
3551        if (!selection.isEmpty())
3552            return false;
3553
3554        var newSelection = this._rangeForCtrlArrowMove(selection, "left");
3555        this._textModel.editRange(newSelection.normalize(), "", selection);
3556
3557        this._textEditor.setSelection(newSelection.collapseToEnd());
3558        return true;
3559    }
3560}
3561
3562/**
3563 * @constructor
3564 * @param {WebInspector.TextEditorMainPanel} textEditor
3565 * @param {WebInspector.TextEditorModel} textModel
3566 * @param {WebInspector.TextEditorModel.BraceMatcher} braceMatcher
3567 */
3568WebInspector.TextEditorMainPanel.BraceHighlightController = function(textEditor, textModel, braceMatcher)
3569{
3570    this._textEditor = textEditor;
3571    this._textModel = textModel;
3572    this._braceMatcher = braceMatcher;
3573    this._highlightDescriptors = [];
3574}
3575
3576WebInspector.TextEditorMainPanel.BraceHighlightController.prototype = {
3577    /**
3578     * @param {string} line
3579     * @param {number} column
3580     * @return {number}
3581     */
3582    activeBraceColumnForCursorPosition: function(line, column)
3583    {
3584        var char = line.charAt(column);
3585        if (WebInspector.TextUtils.isOpeningBraceChar(char))
3586            return column;
3587
3588        var previousChar = line.charAt(column - 1);
3589        if (WebInspector.TextUtils.isBraceChar(previousChar))
3590            return column - 1;
3591
3592        if (WebInspector.TextUtils.isBraceChar(char))
3593            return column;
3594        else
3595            return -1;
3596    },
3597
3598    /**
3599     * @param {WebInspector.TextRange} selectionRange
3600     */
3601    handleSelectionChange: function(selectionRange)
3602    {
3603        if (!selectionRange || !selectionRange.isEmpty()) {
3604            this._removeHighlight();
3605            return;
3606        }
3607
3608        if (this._highlightedRange && this._highlightedRange.compareTo(selectionRange) === 0)
3609            return;
3610
3611        this._removeHighlight();
3612        var lineNumber = selectionRange.startLine;
3613        var column = selectionRange.startColumn;
3614        var line = this._textModel.line(lineNumber);
3615        column = this.activeBraceColumnForCursorPosition(line, column);
3616        if (column < 0)
3617            return;
3618
3619        var enclosingBraces = this._braceMatcher.enclosingBraces(lineNumber, column);
3620        if (!enclosingBraces)
3621            return;
3622
3623        this._highlightedRange = selectionRange;
3624        this._highlightDescriptors.push(this._textEditor.highlightRange(WebInspector.TextRange.createFromLocation(enclosingBraces.leftBrace.lineNumber, enclosingBraces.leftBrace.column), "text-editor-brace-match"));
3625        this._highlightDescriptors.push(this._textEditor.highlightRange(WebInspector.TextRange.createFromLocation(enclosingBraces.rightBrace.lineNumber, enclosingBraces.rightBrace.column), "text-editor-brace-match"));
3626    },
3627
3628    _removeHighlight: function()
3629    {
3630        if (!this._highlightDescriptors.length)
3631            return;
3632
3633        for(var i = 0; i < this._highlightDescriptors.length; ++i)
3634            this._textEditor.removeHighlight(this._highlightDescriptors[i]);
3635
3636        this._highlightDescriptors = [];
3637        delete this._highlightedRange;
3638    }
3639}
3640
3641/**
3642 * @constructor
3643 * @param {WebInspector.TextEditorMainPanel} mainPanel
3644 * @param {WebInspector.TextEditorModel} textModel
3645 * @param {WebInspector.TextEditorModel.BraceMatcher} braceMatcher
3646 */
3647WebInspector.TextEditorMainPanel.SmartBraceController = function(mainPanel, textModel, braceMatcher)
3648{
3649    this._mainPanel = mainPanel;
3650    this._textModel = textModel;
3651    this._braceMatcher = braceMatcher
3652}
3653
3654WebInspector.TextEditorMainPanel.SmartBraceController.prototype = {
3655    /**
3656     * @param {Object.<number, function()>} shortcuts
3657     */
3658    registerShortcuts: function(shortcuts)
3659    {
3660        if (!WebInspector.experimentsSettings.textEditorSmartBraces.isEnabled())
3661            return;
3662
3663        var keys = WebInspector.KeyboardShortcut.Keys;
3664        var modifiers = WebInspector.KeyboardShortcut.Modifiers;
3665
3666        shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Backspace.code, modifiers.None)] = this._handleBackspace.bind(this);
3667    },
3668
3669    /**
3670     * @param {Object.<string, function()>} charOverrides
3671     */
3672    registerCharOverrides: function(charOverrides)
3673    {
3674        if (!WebInspector.experimentsSettings.textEditorSmartBraces.isEnabled())
3675            return;
3676        charOverrides["("] = this._handleBracePairInsertion.bind(this, "()");
3677        charOverrides[")"] = this._handleClosingBraceOverride.bind(this, ")");
3678        charOverrides["{"] = this._handleBracePairInsertion.bind(this, "{}");
3679        charOverrides["}"] = this._handleClosingBraceOverride.bind(this, "}");
3680    },
3681
3682    _handleBackspace: function()
3683    {
3684        var selection = this._mainPanel.lastSelection();
3685        if (!selection || !selection.isEmpty())
3686            return false;
3687
3688        var column = selection.startColumn;
3689        if (column == 0)
3690            return false;
3691
3692        var lineNumber = selection.startLine;
3693        var line = this._textModel.line(lineNumber);
3694        if (column === line.length)
3695            return false;
3696
3697        var pair = line.substr(column - 1, 2);
3698        if (pair === "()" || pair === "{}") {
3699            this._textModel.editRange(new WebInspector.TextRange(lineNumber, column - 1, lineNumber, column + 1), "");
3700            this._mainPanel.setSelection(WebInspector.TextRange.createFromLocation(lineNumber, column - 1));
3701            return true;
3702        } else
3703            return false;
3704    },
3705
3706    /**
3707     * @param {string} bracePair
3708     * @return {boolean}
3709     */
3710    _handleBracePairInsertion: function(bracePair)
3711    {
3712        var selection = this._mainPanel.lastSelection().normalize();
3713        if (selection.isEmpty()) {
3714            var lineNumber = selection.startLine;
3715            var column = selection.startColumn;
3716            var line = this._textModel.line(lineNumber);
3717            if (column < line.length) {
3718                var char = line.charAt(column);
3719                if (WebInspector.TextUtils.isWordChar(char) || (!WebInspector.TextUtils.isBraceChar(char) && WebInspector.TextUtils.isStopChar(char)))
3720                    return false;
3721            }
3722        }
3723        this._textModel.editRange(selection, bracePair);
3724        this._mainPanel.setSelection(WebInspector.TextRange.createFromLocation(selection.startLine, selection.startColumn + 1));
3725        return true;
3726    },
3727
3728    /**
3729     * @param {string} brace
3730     * @return {boolean}
3731     */
3732    _handleClosingBraceOverride: function(brace)
3733    {
3734        var selection = this._mainPanel.lastSelection().normalize();
3735        if (!selection || !selection.isEmpty())
3736            return false;
3737
3738        var lineNumber = selection.startLine;
3739        var column = selection.startColumn;
3740        var line = this._textModel.line(lineNumber);
3741        if (line.charAt(column) !== brace)
3742            return false;
3743
3744        var braces = this._braceMatcher.enclosingBraces(lineNumber, column);
3745        if (braces && braces.rightBrace.lineNumber === lineNumber && braces.rightBrace.column === column) {
3746            this._mainPanel.setSelection(WebInspector.TextRange.createFromLocation(lineNumber, column + 1));
3747            return true;
3748        } else
3749            return false;
3750    },
3751}
3752
3753WebInspector.debugDefaultTextEditor = false;
3754