1/*
2 * Copyright (C) 2011 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31importScript("../cm/codemirror.js");
32importScript("../cm/css.js");
33importScript("../cm/javascript.js");
34importScript("../cm/xml.js");
35importScript("../cm/htmlmixed.js");
36
37importScript("../cm/matchbrackets.js");
38importScript("../cm/closebrackets.js");
39importScript("../cm/markselection.js");
40importScript("../cm/comment.js");
41importScript("../cm/overlay.js");
42
43importScript("../cm/htmlembedded.js");
44importScript("../cm/clike.js");
45importScript("../cm/coffeescript.js");
46importScript("../cm/php.js");
47importScript("../cm/python.js");
48importScript("../cm/shell.js");
49importScript("CodeMirrorUtils.js");
50importScript("CodeMirrorTextEditor.js");
51
52/**
53 * @extends {WebInspector.VBox}
54 * @constructor
55 * @implements {WebInspector.Replaceable}
56 * @param {!WebInspector.ContentProvider} contentProvider
57 */
58WebInspector.SourceFrame = function(contentProvider)
59{
60    WebInspector.VBox.call(this);
61    this.element.classList.add("script-view");
62
63    this._url = contentProvider.contentURL();
64    this._contentProvider = contentProvider;
65
66    var textEditorDelegate = new WebInspector.TextEditorDelegateForSourceFrame(this);
67
68    this._textEditor = new WebInspector.CodeMirrorTextEditor(this._url, textEditorDelegate);
69
70    this._currentSearchResultIndex = -1;
71    this._searchResults = [];
72
73    this._messages = [];
74    this._rowMessages = {};
75    this._messageBubbles = {};
76
77    this._textEditor.setReadOnly(!this.canEditSource());
78
79    this._shortcuts = {};
80    this.element.addEventListener("keydown", this._handleKeyDown.bind(this), false);
81
82    this._sourcePosition = new WebInspector.StatusBarText("", "source-frame-cursor-position");
83}
84
85/**
86 * @param {string} query
87 * @param {string=} modifiers
88 * @return {!RegExp}
89 */
90WebInspector.SourceFrame.createSearchRegex = function(query, modifiers)
91{
92    var regex;
93    modifiers = modifiers || "";
94
95    // First try creating regex if user knows the / / hint.
96    try {
97        if (/^\/.+\/$/.test(query)) {
98            regex = new RegExp(query.substring(1, query.length - 1), modifiers);
99            regex.__fromRegExpQuery = true;
100        }
101    } catch (e) {
102        // Silent catch.
103    }
104
105    // Otherwise just do case-insensitive search.
106    if (!regex)
107        regex = createPlainTextSearchRegex(query, "i" + modifiers);
108
109    return regex;
110}
111
112WebInspector.SourceFrame.Events = {
113    ScrollChanged: "ScrollChanged",
114    SelectionChanged: "SelectionChanged",
115    JumpHappened: "JumpHappened"
116}
117
118WebInspector.SourceFrame.prototype = {
119    /**
120     * @param {number} key
121     * @param {function()} handler
122     */
123    addShortcut: function(key, handler)
124    {
125        this._shortcuts[key] = handler;
126    },
127
128    wasShown: function()
129    {
130        this._ensureContentLoaded();
131        this._textEditor.show(this.element);
132        this._editorAttached = true;
133        this._wasShownOrLoaded();
134    },
135
136    /**
137     * @return {boolean}
138     */
139    _isEditorShowing: function()
140    {
141        return this.isShowing() && this._editorAttached;
142    },
143
144    willHide: function()
145    {
146        WebInspector.View.prototype.willHide.call(this);
147
148        this._clearPositionToReveal();
149    },
150
151    /**
152     * @return {?Element}
153     */
154    statusBarText: function()
155    {
156        return this._sourcePosition.element;
157    },
158
159    /**
160     * @return {!Array.<!Element>}
161     */
162    statusBarItems: function()
163    {
164        return [];
165    },
166
167    /**
168     * @return {!Element}
169     */
170    defaultFocusedElement: function()
171    {
172        return this._textEditor.defaultFocusedElement();
173    },
174
175    get loaded()
176    {
177        return this._loaded;
178    },
179
180    /**
181     * @return {boolean}
182     */
183    hasContent: function()
184    {
185        return true;
186    },
187
188    get textEditor()
189    {
190        return this._textEditor;
191    },
192
193    _ensureContentLoaded: function()
194    {
195        if (!this._contentRequested) {
196            this._contentRequested = true;
197            this._contentProvider.requestContent(this.setContent.bind(this));
198        }
199    },
200
201    addMessage: function(msg)
202    {
203        this._messages.push(msg);
204        if (this.loaded)
205            this.addMessageToSource(msg.line - 1, msg);
206    },
207
208    clearMessages: function()
209    {
210        for (var line in this._messageBubbles) {
211            var bubble = this._messageBubbles[line];
212            var lineNumber = parseInt(line, 10);
213            this._textEditor.removeDecoration(lineNumber, bubble);
214        }
215
216        this._messages = [];
217        this._rowMessages = {};
218        this._messageBubbles = {};
219    },
220
221    /**
222     * @param {number} line
223     * @param {number=} column
224     * @param {boolean=} shouldHighlight
225     */
226    revealPosition: function(line, column, shouldHighlight)
227    {
228        this._clearLineToScrollTo();
229        this._clearSelectionToSet();
230        this._positionToReveal = { line: line, column: column, shouldHighlight: shouldHighlight };
231        this._innerRevealPositionIfNeeded();
232    },
233
234    _innerRevealPositionIfNeeded: function()
235    {
236        if (!this._positionToReveal)
237            return;
238
239        if (!this.loaded || !this._isEditorShowing())
240            return;
241
242        this._textEditor.revealPosition(this._positionToReveal.line, this._positionToReveal.column, this._positionToReveal.shouldHighlight);
243        delete this._positionToReveal;
244    },
245
246    _clearPositionToReveal: function()
247    {
248        this._textEditor.clearPositionHighlight();
249        delete this._positionToReveal;
250    },
251
252    /**
253     * @param {number} line
254     */
255    scrollToLine: function(line)
256    {
257        this._clearPositionToReveal();
258        this._lineToScrollTo = line;
259        this._innerScrollToLineIfNeeded();
260    },
261
262    _innerScrollToLineIfNeeded: function()
263    {
264        if (typeof this._lineToScrollTo === "number") {
265            if (this.loaded && this._isEditorShowing()) {
266                this._textEditor.scrollToLine(this._lineToScrollTo);
267                delete this._lineToScrollTo;
268            }
269        }
270    },
271
272    _clearLineToScrollTo: function()
273    {
274        delete this._lineToScrollTo;
275    },
276
277    /**
278     * @return {!WebInspector.TextRange}
279     */
280    selection: function()
281    {
282        return this.textEditor.selection();
283    },
284
285    /**
286     * @param {!WebInspector.TextRange} textRange
287     */
288    setSelection: function(textRange)
289    {
290        this._selectionToSet = textRange;
291        this._innerSetSelectionIfNeeded();
292    },
293
294    _innerSetSelectionIfNeeded: function()
295    {
296        if (this._selectionToSet && this.loaded && this._isEditorShowing()) {
297            this._textEditor.setSelection(this._selectionToSet);
298            delete this._selectionToSet;
299        }
300    },
301
302    _clearSelectionToSet: function()
303    {
304        delete this._selectionToSet;
305    },
306
307    _wasShownOrLoaded: function()
308    {
309        this._innerRevealPositionIfNeeded();
310        this._innerSetSelectionIfNeeded();
311        this._innerScrollToLineIfNeeded();
312    },
313
314    onTextChanged: function(oldRange, newRange)
315    {
316        if (this._searchResultsChangedCallback && !this._isReplacing)
317            this._searchResultsChangedCallback();
318        this.clearMessages();
319    },
320
321    _simplifyMimeType: function(content, mimeType)
322    {
323        if (!mimeType)
324            return "";
325        if (mimeType.indexOf("javascript") >= 0 ||
326            mimeType.indexOf("jscript") >= 0 ||
327            mimeType.indexOf("ecmascript") >= 0)
328            return "text/javascript";
329        // A hack around the fact that files with "php" extension might be either standalone or html embedded php scripts.
330        if (mimeType === "text/x-php" && content.match(/\<\?.*\?\>/g))
331            return "application/x-httpd-php";
332        return mimeType;
333    },
334
335    /**
336     * @param {string} highlighterType
337     */
338    setHighlighterType: function(highlighterType)
339    {
340        this._highlighterType = highlighterType;
341        this._updateHighlighterType("");
342    },
343
344    /**
345     * @param {string} content
346     */
347    _updateHighlighterType: function(content)
348    {
349        this._textEditor.setMimeType(this._simplifyMimeType(content, this._highlighterType));
350    },
351
352    /**
353     * @param {?string} content
354     */
355    setContent: function(content)
356    {
357        if (!this._loaded) {
358            this._loaded = true;
359            this._textEditor.setText(content || "");
360            this._textEditor.markClean();
361        } else {
362            var firstLine = this._textEditor.firstVisibleLine();
363            var selection = this._textEditor.selection();
364            this._textEditor.setText(content || "");
365            this._textEditor.scrollToLine(firstLine);
366            this._textEditor.setSelection(selection);
367        }
368
369        this._updateHighlighterType(content || "");
370
371        this._textEditor.beginUpdates();
372
373        this._setTextEditorDecorations();
374
375        this._wasShownOrLoaded();
376
377        if (this._delayedFindSearchMatches) {
378            this._delayedFindSearchMatches();
379            delete this._delayedFindSearchMatches;
380        }
381
382        this.onTextEditorContentLoaded();
383
384        this._textEditor.endUpdates();
385    },
386
387    onTextEditorContentLoaded: function() {},
388
389    _setTextEditorDecorations: function()
390    {
391        this._rowMessages = {};
392        this._messageBubbles = {};
393
394        this._textEditor.beginUpdates();
395
396        this._addExistingMessagesToSource();
397
398        this._textEditor.endUpdates();
399    },
400
401    /**
402     * @param {string} query
403     * @param {boolean} shouldJump
404     * @param {boolean} jumpBackwards
405     * @param {function(!WebInspector.View, number)} callback
406     * @param {function(number)} currentMatchChangedCallback
407     * @param {function()} searchResultsChangedCallback
408     */
409    performSearch: function(query, shouldJump, jumpBackwards, callback, currentMatchChangedCallback, searchResultsChangedCallback)
410    {
411        /**
412         * @param {string} query
413         * @this {WebInspector.SourceFrame}
414         */
415        function doFindSearchMatches(query)
416        {
417            this._currentSearchResultIndex = -1;
418            this._searchResults = [];
419
420            var regex = WebInspector.SourceFrame.createSearchRegex(query);
421            this._searchRegex = regex;
422            this._searchResults = this._collectRegexMatches(regex);
423            if (!this._searchResults.length)
424                this._textEditor.cancelSearchResultsHighlight();
425            else if (shouldJump && jumpBackwards)
426                this.jumpToPreviousSearchResult();
427            else if (shouldJump)
428                this.jumpToNextSearchResult();
429            else
430                this._textEditor.highlightSearchResults(regex, null);
431            callback(this, this._searchResults.length);
432        }
433
434        this._resetSearch();
435        this._currentSearchMatchChangedCallback = currentMatchChangedCallback;
436        this._searchResultsChangedCallback = searchResultsChangedCallback;
437        if (this.loaded)
438            doFindSearchMatches.call(this, query);
439        else
440            this._delayedFindSearchMatches = doFindSearchMatches.bind(this, query);
441
442        this._ensureContentLoaded();
443    },
444
445    _editorFocused: function()
446    {
447        this._resetCurrentSearchResultIndex();
448    },
449
450    _resetCurrentSearchResultIndex: function()
451    {
452        if (!this._searchResults.length)
453            return;
454        this._currentSearchResultIndex = -1;
455        if (this._currentSearchMatchChangedCallback)
456            this._currentSearchMatchChangedCallback(this._currentSearchResultIndex);
457        this._textEditor.highlightSearchResults(this._searchRegex, null);
458    },
459
460    _resetSearch: function()
461    {
462        delete this._delayedFindSearchMatches;
463        delete this._currentSearchMatchChangedCallback;
464        delete this._searchResultsChangedCallback;
465        this._currentSearchResultIndex = -1;
466        this._searchResults = [];
467        delete this._searchRegex;
468    },
469
470    searchCanceled: function()
471    {
472        var range = this._currentSearchResultIndex !== -1 ? this._searchResults[this._currentSearchResultIndex] : null;
473        this._resetSearch();
474        if (!this.loaded)
475            return;
476        this._textEditor.cancelSearchResultsHighlight();
477        if (range)
478            this._textEditor.setSelection(range);
479    },
480
481    /**
482     * @return {boolean}
483     */
484    hasSearchResults: function()
485    {
486        return this._searchResults.length > 0;
487    },
488
489    jumpToFirstSearchResult: function()
490    {
491        this.jumpToSearchResult(0);
492    },
493
494    jumpToLastSearchResult: function()
495    {
496        this.jumpToSearchResult(this._searchResults.length - 1);
497    },
498
499    /**
500     * @return {number}
501     */
502    _searchResultIndexForCurrentSelection: function()
503    {
504        return insertionIndexForObjectInListSortedByFunction(this._textEditor.selection(), this._searchResults, WebInspector.TextRange.comparator);
505    },
506
507    jumpToNextSearchResult: function()
508    {
509        var currentIndex = this._searchResultIndexForCurrentSelection();
510        var nextIndex = this._currentSearchResultIndex === -1 ? currentIndex : currentIndex + 1;
511        this.jumpToSearchResult(nextIndex);
512    },
513
514    jumpToPreviousSearchResult: function()
515    {
516        var currentIndex = this._searchResultIndexForCurrentSelection();
517        this.jumpToSearchResult(currentIndex - 1);
518    },
519
520    /**
521     * @return {boolean}
522     */
523    showingFirstSearchResult: function()
524    {
525        return this._searchResults.length &&  this._currentSearchResultIndex === 0;
526    },
527
528    /**
529     * @return {boolean}
530     */
531    showingLastSearchResult: function()
532    {
533        return this._searchResults.length && this._currentSearchResultIndex === (this._searchResults.length - 1);
534    },
535
536    get currentSearchResultIndex()
537    {
538        return this._currentSearchResultIndex;
539    },
540
541    jumpToSearchResult: function(index)
542    {
543        if (!this.loaded || !this._searchResults.length)
544            return;
545        this._currentSearchResultIndex = (index + this._searchResults.length) % this._searchResults.length;
546        if (this._currentSearchMatchChangedCallback)
547            this._currentSearchMatchChangedCallback(this._currentSearchResultIndex);
548        this._textEditor.highlightSearchResults(this._searchRegex, this._searchResults[this._currentSearchResultIndex]);
549    },
550
551    /**
552     * @param {string} text
553     */
554    replaceSelectionWith: function(text)
555    {
556        var range = this._searchResults[this._currentSearchResultIndex];
557        if (!range)
558            return;
559        this._textEditor.highlightSearchResults(this._searchRegex, null);
560
561        this._isReplacing = true;
562        var newRange = this._textEditor.editRange(range, text);
563        delete this._isReplacing;
564
565        this._textEditor.setSelection(newRange.collapseToEnd());
566    },
567
568    /**
569     * @param {string} query
570     * @param {string} replacement
571     */
572    replaceAllWith: function(query, replacement)
573    {
574        this._resetCurrentSearchResultIndex();
575
576        var text = this._textEditor.text();
577        var range = this._textEditor.range();
578        var regex = WebInspector.SourceFrame.createSearchRegex(query, "g");
579        if (regex.__fromRegExpQuery)
580            text = text.replace(regex, replacement);
581        else
582            text = text.replace(regex, function() { return replacement; });
583
584        var ranges = this._collectRegexMatches(regex);
585        if (!ranges.length)
586            return;
587
588        // Calculate the position of the end of the last range to be edited.
589        var currentRangeIndex = insertionIndexForObjectInListSortedByFunction(this._textEditor.selection(), ranges, WebInspector.TextRange.comparator);
590        var lastRangeIndex = mod(currentRangeIndex - 1, ranges.length);
591        var lastRange = ranges[lastRangeIndex];
592        var replacementLineEndings = replacement.lineEndings();
593        var replacementLineCount = replacementLineEndings.length;
594        var lastLineNumber = lastRange.startLine + replacementLineEndings.length - 1;
595        var lastColumnNumber = lastRange.startColumn;
596        if (replacementLineEndings.length > 1)
597            lastColumnNumber = replacementLineEndings[replacementLineCount - 1] - replacementLineEndings[replacementLineCount - 2] - 1;
598
599        this._isReplacing = true;
600        this._textEditor.editRange(range, text);
601        this._textEditor.revealPosition(lastLineNumber, lastColumnNumber);
602        this._textEditor.setSelection(WebInspector.TextRange.createFromLocation(lastLineNumber, lastColumnNumber));
603        delete this._isReplacing;
604    },
605
606    _collectRegexMatches: function(regexObject)
607    {
608        var ranges = [];
609        for (var i = 0; i < this._textEditor.linesCount; ++i) {
610            var line = this._textEditor.line(i);
611            var offset = 0;
612            do {
613                var match = regexObject.exec(line);
614                if (match) {
615                    if (match[0].length)
616                        ranges.push(new WebInspector.TextRange(i, offset + match.index, i, offset + match.index + match[0].length));
617                    offset += match.index + 1;
618                    line = line.substring(match.index + 1);
619                }
620            } while (match && line);
621        }
622        return ranges;
623    },
624
625    _addExistingMessagesToSource: function()
626    {
627        var length = this._messages.length;
628        for (var i = 0; i < length; ++i)
629            this.addMessageToSource(this._messages[i].line - 1, this._messages[i]);
630    },
631
632    /**
633     * @param {number} lineNumber
634     * @param {!WebInspector.ConsoleMessage} msg
635     */
636    addMessageToSource: function(lineNumber, msg)
637    {
638        if (lineNumber >= this._textEditor.linesCount)
639            lineNumber = this._textEditor.linesCount - 1;
640        if (lineNumber < 0)
641            lineNumber = 0;
642
643        var rowMessages = this._rowMessages[lineNumber];
644        if (!rowMessages) {
645            rowMessages = [];
646            this._rowMessages[lineNumber] = rowMessages;
647        }
648
649        for (var i = 0; i < rowMessages.length; ++i) {
650            if (rowMessages[i].consoleMessage.isEqual(msg)) {
651                rowMessages[i].repeatCount++;
652                this._updateMessageRepeatCount(rowMessages[i]);
653                return;
654            }
655        }
656
657        var rowMessage = { consoleMessage: msg };
658        rowMessages.push(rowMessage);
659
660        this._textEditor.beginUpdates();
661        var messageBubbleElement = this._messageBubbles[lineNumber];
662        if (!messageBubbleElement) {
663            messageBubbleElement = document.createElement("div");
664            messageBubbleElement.className = "webkit-html-message-bubble";
665            this._messageBubbles[lineNumber] = messageBubbleElement;
666            this._textEditor.addDecoration(lineNumber, messageBubbleElement);
667        }
668
669        var imageElement = document.createElement("div");
670        switch (msg.level) {
671            case WebInspector.ConsoleMessage.MessageLevel.Error:
672                messageBubbleElement.classList.add("webkit-html-error-message");
673                imageElement.className = "error-icon-small";
674                break;
675            case WebInspector.ConsoleMessage.MessageLevel.Warning:
676                messageBubbleElement.classList.add("webkit-html-warning-message");
677                imageElement.className = "warning-icon-small";
678                break;
679        }
680
681        var messageLineElement = document.createElement("div");
682        messageLineElement.className = "webkit-html-message-line";
683        messageBubbleElement.appendChild(messageLineElement);
684
685        // Create the image element in the Inspector's document so we can use relative image URLs.
686        messageLineElement.appendChild(imageElement);
687        messageLineElement.appendChild(document.createTextNode(msg.messageText));
688
689        rowMessage.element = messageLineElement;
690        rowMessage.repeatCount = 1;
691        this._updateMessageRepeatCount(rowMessage);
692        this._textEditor.endUpdates();
693    },
694
695    _updateMessageRepeatCount: function(rowMessage)
696    {
697        if (rowMessage.repeatCount < 2)
698            return;
699
700        if (!rowMessage.repeatCountElement) {
701            var repeatCountElement = document.createElement("span");
702            rowMessage.element.appendChild(repeatCountElement);
703            rowMessage.repeatCountElement = repeatCountElement;
704        }
705
706        rowMessage.repeatCountElement.textContent = WebInspector.UIString(" (repeated %d times)", rowMessage.repeatCount);
707    },
708
709    /**
710     * @param {number} lineNumber
711     * @param {!WebInspector.ConsoleMessage} msg
712     */
713    removeMessageFromSource: function(lineNumber, msg)
714    {
715        if (lineNumber >= this._textEditor.linesCount)
716            lineNumber = this._textEditor.linesCount - 1;
717        if (lineNumber < 0)
718            lineNumber = 0;
719
720        var rowMessages = this._rowMessages[lineNumber];
721        for (var i = 0; rowMessages && i < rowMessages.length; ++i) {
722            var rowMessage = rowMessages[i];
723            if (rowMessage.consoleMessage !== msg)
724                continue;
725
726            var messageLineElement = rowMessage.element;
727            var messageBubbleElement = messageLineElement.parentElement;
728            messageBubbleElement.removeChild(messageLineElement);
729            rowMessages.remove(rowMessage);
730            if (!rowMessages.length)
731                delete this._rowMessages[lineNumber];
732            if (!messageBubbleElement.childElementCount) {
733                this._textEditor.removeDecoration(lineNumber, messageBubbleElement);
734                delete this._messageBubbles[lineNumber];
735            }
736            break;
737        }
738    },
739
740    populateLineGutterContextMenu: function(contextMenu, lineNumber)
741    {
742    },
743
744    populateTextAreaContextMenu: function(contextMenu, lineNumber)
745    {
746    },
747
748    /**
749     * @param {?WebInspector.TextRange} from
750     * @param {?WebInspector.TextRange} to
751     */
752    onJumpToPosition: function(from, to)
753    {
754        this.dispatchEventToListeners(WebInspector.SourceFrame.Events.JumpHappened, {
755            from: from,
756            to: to
757        });
758    },
759
760    inheritScrollPositions: function(sourceFrame)
761    {
762        this._textEditor.inheritScrollPositions(sourceFrame._textEditor);
763    },
764
765    /**
766     * @return {boolean}
767     */
768    canEditSource: function()
769    {
770        return false;
771    },
772
773    /**
774     * @param {!WebInspector.TextRange} textRange
775     */
776    selectionChanged: function(textRange)
777    {
778        this._updateSourcePosition();
779        this.dispatchEventToListeners(WebInspector.SourceFrame.Events.SelectionChanged, textRange);
780        WebInspector.notifications.dispatchEventToListeners(WebInspector.SourceFrame.Events.SelectionChanged, textRange);
781    },
782
783    _updateSourcePosition: function()
784    {
785        var selections = this._textEditor.selections();
786        if (!selections.length)
787            return;
788        if (selections.length > 1) {
789            this._sourcePosition.setText(WebInspector.UIString("%d selection regions", selections.length));
790            return;
791        }
792        var textRange = selections[0];
793        if (textRange.isEmpty()) {
794            this._sourcePosition.setText(WebInspector.UIString("Line %d, Column %d", textRange.endLine + 1, textRange.endColumn + 1));
795            return;
796        }
797        textRange = textRange.normalize();
798
799        var selectedText = this._textEditor.copyRange(textRange);
800        if (textRange.startLine === textRange.endLine)
801            this._sourcePosition.setText(WebInspector.UIString("%d characters selected", selectedText.length));
802        else
803            this._sourcePosition.setText(WebInspector.UIString("%d lines, %d characters selected", textRange.endLine - textRange.startLine + 1, selectedText.length));
804    },
805
806    /**
807     * @param {number} lineNumber
808     */
809    scrollChanged: function(lineNumber)
810    {
811        this.dispatchEventToListeners(WebInspector.SourceFrame.Events.ScrollChanged, lineNumber);
812    },
813
814    _handleKeyDown: function(e)
815    {
816        var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e);
817        var handler = this._shortcuts[shortcutKey];
818        if (handler && handler())
819            e.consume(true);
820    },
821
822    __proto__: WebInspector.VBox.prototype
823}
824
825
826/**
827 * @implements {WebInspector.TextEditorDelegate}
828 * @constructor
829 */
830WebInspector.TextEditorDelegateForSourceFrame = function(sourceFrame)
831{
832    this._sourceFrame = sourceFrame;
833}
834
835WebInspector.TextEditorDelegateForSourceFrame.prototype = {
836    onTextChanged: function(oldRange, newRange)
837    {
838        this._sourceFrame.onTextChanged(oldRange, newRange);
839    },
840
841    /**
842     * @param {!WebInspector.TextRange} textRange
843     */
844    selectionChanged: function(textRange)
845    {
846        this._sourceFrame.selectionChanged(textRange);
847    },
848
849    /**
850     * @param {number} lineNumber
851     */
852    scrollChanged: function(lineNumber)
853    {
854        this._sourceFrame.scrollChanged(lineNumber);
855    },
856
857    editorFocused: function()
858    {
859        this._sourceFrame._editorFocused();
860    },
861
862    populateLineGutterContextMenu: function(contextMenu, lineNumber)
863    {
864        this._sourceFrame.populateLineGutterContextMenu(contextMenu, lineNumber);
865    },
866
867    populateTextAreaContextMenu: function(contextMenu, lineNumber)
868    {
869        this._sourceFrame.populateTextAreaContextMenu(contextMenu, lineNumber);
870    },
871
872    /**
873     * @param {string} hrefValue
874     * @param {boolean} isExternal
875     * @return {!Element}
876     */
877    createLink: function(hrefValue, isExternal)
878    {
879        var targetLocation = WebInspector.ParsedURL.completeURL(this._sourceFrame._url, hrefValue);
880        return WebInspector.linkifyURLAsNode(targetLocation || hrefValue, hrefValue, undefined, isExternal);
881    },
882
883    /**
884     * @param {?WebInspector.TextRange} from
885     * @param {?WebInspector.TextRange} to
886     */
887    onJumpToPosition: function(from, to)
888    {
889        this._sourceFrame.onJumpToPosition(from, to);
890    }
891}
892
893importScript("GoToLineDialog.js");
894importScript("ResourceView.js");
895importScript("FontView.js");
896importScript("ImageView.js");
897