1/*
2 * Copyright (C) 2012 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");
36importScript("cm/matchbrackets.js");
37importScript("cm/closebrackets.js");
38
39/**
40 * @constructor
41 * @extends {WebInspector.View}
42 * @implements {WebInspector.TextEditor}
43 * @param {?string} url
44 * @param {WebInspector.TextEditorDelegate} delegate
45 */
46WebInspector.CodeMirrorTextEditor = function(url, delegate)
47{
48    WebInspector.View.call(this);
49    this._delegate = delegate;
50    this._url = url;
51
52    this.registerRequiredCSS("cm/codemirror.css");
53    this.registerRequiredCSS("cm/cmdevtools.css");
54
55    this._codeMirror = window.CodeMirror(this.element, {
56        lineNumbers: true,
57        gutters: ["CodeMirror-linenumbers", "breakpoints"],
58        matchBrackets: true,
59        autoCloseBrackets: WebInspector.experimentsSettings.textEditorSmartBraces.isEnabled()
60    });
61
62    var indent = WebInspector.settings.textEditorIndent.get();
63    if (indent === WebInspector.TextUtils.Indent.TabCharacter) {
64        this._codeMirror.setOption("indentWithTabs", true);
65        this._codeMirror.setOption("indentUnit", 4);
66    } else {
67        this._codeMirror.setOption("indentWithTabs", false);
68        this._codeMirror.setOption("indentUnit", indent.length);
69    }
70
71    this._tokenHighlighter = new WebInspector.CodeMirrorTextEditor.TokenHighlighter(this._codeMirror);
72    this._blockIndentController = new WebInspector.CodeMirrorTextEditor.BlockIndentController(this._codeMirror);
73    this._fixWordMovement = new WebInspector.CodeMirrorTextEditor.FixWordMovement(this._codeMirror);
74
75    this._codeMirror.on("change", this._change.bind(this));
76    this._codeMirror.on("gutterClick", this._gutterClick.bind(this));
77    this._codeMirror.on("cursorActivity", this._cursorActivity.bind(this));
78    this._codeMirror.on("scroll", this._scroll.bind(this));
79    this.element.addEventListener("contextmenu", this._contextMenu.bind(this));
80
81    this._lastRange = this.range();
82
83    this.element.firstChild.addStyleClass("source-code");
84    this.element.addStyleClass("fill");
85    this.markAsLayoutBoundary();
86
87    this._elementToWidget = new Map();
88    this._nestedUpdatesCounter = 0;
89
90    this.element.addEventListener("focus", this._handleElementFocus.bind(this), false);
91    this.element.tabIndex = 0;
92}
93
94WebInspector.CodeMirrorTextEditor.prototype = {
95
96    /**
97     * @param {number} lineNumber
98     * @param {number} column
99     * @return {?{x: number, y: number, height: number}}
100     */
101    cursorPositionToCoordinates: function(lineNumber, column)
102    {
103        if (lineNumber >= this._codeMirror.lineCount || column > this._codeMirror.getLine(lineNumber).length || lineNumber < 0 || column < 0)
104            return null;
105
106        var metrics = this._codeMirror.cursorCoords(CodeMirror.Pos(lineNumber, column));
107
108        return {
109            x: metrics.left,
110            y: metrics.top,
111            height: metrics.bottom - metrics.top
112        };
113    },
114
115    /**
116     * @param {number} x
117     * @param {number} y
118     * @return {?WebInspector.TextRange}
119     */
120    coordinatesToCursorPosition: function(x, y)
121    {
122        var element = document.elementFromPoint(x, y);
123        if (!element || !element.isSelfOrDescendant(this._codeMirror.getWrapperElement()))
124            return null;
125        var gutterBox = this._codeMirror.getGutterElement().boxInWindow();
126        if (x >= gutterBox.x && x <= gutterBox.x + gutterBox.width &&
127            y >= gutterBox.y && y <= gutterBox.y + gutterBox.height)
128            return null;
129        var coords = this._codeMirror.coordsChar({left: x, top: y});
130        ++coords.ch;
131        return this._toRange(coords, coords);
132    },
133
134    /**
135     * @param {number} lineNumber
136     * @param {number} column
137     * @return {?{startColumn: number, endColumn: number, token: string}}
138     */
139    tokenAtTextPosition: function(lineNumber, column)
140    {
141        if (lineNumber < 0 || lineNumber >= this._codeMirror.lineCount())
142            return null;
143        var token = this._codeMirror.getTokenAt(CodeMirror.Pos(lineNumber, column || 1));
144        if (!token || !token.type)
145            return null;
146        var convertedType = null;
147        if (token.type.startsWith("variable") || token.type.startsWith("property")) {
148            return {
149                startColumn: token.start,
150                endColumn: token.end - 1,
151                type: "javascript-ident"
152            };
153        }
154        return null;
155    },
156
157    /**
158     * @param {WebInspector.TextRange} textRange
159     * @return {string}
160     */
161    copyRange: function(textRange)
162    {
163        var pos = this._toPos(textRange);
164        return this._codeMirror.getRange(pos.start, pos.end);
165    },
166
167    /**
168     * @return {boolean}
169     */
170    isClean: function()
171    {
172        return this._codeMirror.isClean();
173    },
174
175    markClean: function()
176    {
177        this._codeMirror.markClean();
178    },
179
180    /**
181     * @param {string} mimeType
182     */
183    set mimeType(mimeType)
184    {
185        this._codeMirror.setOption("mode", mimeType);
186        switch(mimeType) {
187            case "text/html": this._codeMirror.setOption("theme", "web-inspector-html"); break;
188            case "text/css": this._codeMirror.setOption("theme", "web-inspector-css"); break;
189            case "text/javascript": this._codeMirror.setOption("theme", "web-inspector-js"); break;
190        }
191    },
192
193    /**
194     * @param {boolean} readOnly
195     */
196    setReadOnly: function(readOnly)
197    {
198        this._codeMirror.setOption("readOnly", readOnly ? "nocursor" : false);
199    },
200
201    /**
202     * @return {boolean}
203     */
204    readOnly: function()
205    {
206        return !!this._codeMirror.getOption("readOnly");
207    },
208
209    /**
210     * @param {Object} highlightDescriptor
211     */
212    removeHighlight: function(highlightDescriptor)
213    {
214        highlightDescriptor.clear();
215    },
216
217    /**
218     * @param {WebInspector.TextRange} range
219     * @param {string} cssClass
220     * @return {Object}
221     */
222    highlightRange: function(range, cssClass)
223    {
224        var pos = this._toPos(range);
225        ++pos.end.ch;
226        return this._codeMirror.markText(pos.start, pos.end, {
227            className: cssClass,
228            startStyle: cssClass + "-start",
229            endStyle: cssClass + "-end"
230        });
231    },
232
233    /**
234     * @return {Element}
235     */
236    defaultFocusedElement: function()
237    {
238        return this.element;
239    },
240
241    focus: function()
242    {
243        this._codeMirror.focus();
244    },
245
246    _handleElementFocus: function()
247    {
248        this._codeMirror.focus();
249    },
250
251    beginUpdates: function()
252    {
253        ++this._nestedUpdatesCounter;
254    },
255
256    endUpdates: function()
257    {
258        if (!--this._nestedUpdatesCounter);
259            this._codeMirror.refresh();
260    },
261
262    /**
263     * @param {number} lineNumber
264     */
265    revealLine: function(lineNumber)
266    {
267        var pos = CodeMirror.Pos(lineNumber, 0);
268        var topLine = this._topScrolledLine();
269        var bottomLine = this._bottomScrolledLine();
270
271        var margin = null;
272        var lineMargin = 3;
273        var scrollInfo = this._codeMirror.getScrollInfo();
274        if ((lineNumber < topLine + lineMargin) || (lineNumber >= bottomLine - lineMargin)) {
275            // scrollIntoView could get into infinite loop if margin exceeds half of the clientHeight.
276            margin = (scrollInfo.clientHeight*0.9/2) >>> 0;
277        }
278        this._codeMirror.scrollIntoView(pos, margin);
279    },
280
281    _gutterClick: function(instance, lineNumber, gutter, event)
282    {
283        this.dispatchEventToListeners(WebInspector.TextEditor.Events.GutterClick, { lineNumber: lineNumber, event: event });
284    },
285
286    _contextMenu: function(event)
287    {
288        var contextMenu = new WebInspector.ContextMenu(event);
289        var target = event.target.enclosingNodeOrSelfWithClass("CodeMirror-gutter-elt");
290        if (target)
291            this._delegate.populateLineGutterContextMenu(contextMenu, parseInt(target.textContent, 10) - 1);
292        else
293            this._delegate.populateTextAreaContextMenu(contextMenu, null);
294        contextMenu.show();
295    },
296
297    /**
298     * @param {number} lineNumber
299     * @param {boolean} disabled
300     * @param {boolean} conditional
301     */
302    addBreakpoint: function(lineNumber, disabled, conditional)
303    {
304        var element = document.createElement("span");
305        element.textContent = lineNumber + 1;
306        element.className = "cm-breakpoint" + (disabled ? " cm-breakpoint-disabled" : "") + (conditional ? " cm-breakpoint-conditional" : "");
307        this._codeMirror.setGutterMarker(lineNumber, "breakpoints", element);
308    },
309
310    /**
311     * @param {number} lineNumber
312     */
313    removeBreakpoint: function(lineNumber)
314    {
315        this._codeMirror.setGutterMarker(lineNumber, "breakpoints", null);
316    },
317
318    /**
319     * @param {number} lineNumber
320     */
321    setExecutionLine: function(lineNumber)
322    {
323        this._executionLine = this._codeMirror.getLineHandle(lineNumber);
324        this._codeMirror.addLineClass(this._executionLine, null, "cm-execution-line");
325    },
326
327    clearExecutionLine: function()
328    {
329        if (this._executionLine)
330            this._codeMirror.removeLineClass(this._executionLine, null, "cm-execution-line");
331        delete this._executionLine;
332    },
333
334    /**
335     * @param {number} lineNumber
336     * @param {Element} element
337     */
338    addDecoration: function(lineNumber, element)
339    {
340        var widget = this._codeMirror.addLineWidget(lineNumber, element);
341        this._elementToWidget.put(element, widget);
342    },
343
344    /**
345     * @param {number} lineNumber
346     * @param {Element} element
347     */
348    removeDecoration: function(lineNumber, element)
349    {
350        var widget = this._elementToWidget.remove(element);
351        if (widget)
352            this._codeMirror.removeLineWidget(widget);
353    },
354
355    /**
356     * @param {WebInspector.TextRange} range
357     */
358    markAndRevealRange: function(range)
359    {
360        if (range)
361            this.setSelection(range);
362    },
363
364    /**
365     * @param {number} lineNumber
366     */
367    highlightLine: function(lineNumber)
368    {
369        this.clearLineHighlight();
370        this._highlightedLine = this._codeMirror.getLineHandle(lineNumber);
371        if (!this._highlightedLine)
372          return;
373        this.revealLine(lineNumber);
374        this._codeMirror.addLineClass(this._highlightedLine, null, "cm-highlight");
375        this._clearHighlightTimeout = setTimeout(this.clearLineHighlight.bind(this), 2000);
376    },
377
378    clearLineHighlight: function()
379    {
380        if (this._clearHighlightTimeout)
381            clearTimeout(this._clearHighlightTimeout);
382        delete this._clearHighlightTimeout;
383
384         if (this._highlightedLine)
385            this._codeMirror.removeLineClass(this._highlightedLine, null, "cm-highlight");
386        delete this._highlightedLine;
387    },
388
389    /**
390     * @return {Array.<Element>}
391     */
392    elementsToRestoreScrollPositionsFor: function()
393    {
394        return [];
395    },
396
397    /**
398     * @param {WebInspector.TextEditor} textEditor
399     */
400    inheritScrollPositions: function(textEditor)
401    {
402    },
403
404    onResize: function()
405    {
406        this._codeMirror.refresh();
407    },
408
409    /**
410     * @param {WebInspector.TextRange} range
411     * @param {string} text
412     * @return {WebInspector.TextRange}
413     */
414    editRange: function(range, text)
415    {
416        var pos = this._toPos(range);
417        this._codeMirror.replaceRange(text, pos.start, pos.end);
418        var newRange = this._toRange(pos.start, this._codeMirror.posFromIndex(this._codeMirror.indexFromPos(pos.start) + text.length));
419        this._delegate.onTextChanged(range, newRange);
420        return newRange;
421    },
422
423    _change: function()
424    {
425        var widgets = this._elementToWidget.values();
426        for (var i = 0; i < widgets.length; ++i)
427            this._codeMirror.removeLineWidget(widgets[i]);
428        this._elementToWidget.clear();
429
430        var newRange = this.range();
431        this._delegate.onTextChanged(this._lastRange, newRange);
432        this._lastRange = newRange;
433    },
434
435    _cursorActivity: function()
436    {
437        var start = this._codeMirror.getCursor("anchor");
438        var end = this._codeMirror.getCursor("head");
439        this._delegate.selectionChanged(this._toRange(start, end));
440    },
441
442    _coordsCharLocal: function(coords)
443    {
444        var top = coords.top;
445        var totalLines = this._codeMirror.lineCount();
446        var begin = 0;
447        var end = totalLines - 1;
448        while (end - begin > 1) {
449            var middle = (begin + end) >> 1;
450            var coords = this._codeMirror.charCoords(CodeMirror.Pos(middle, 0), "local");
451            if (coords.top >= top)
452                end = middle;
453            else
454                begin = middle;
455        }
456
457        return end;
458    },
459
460    _topScrolledLine: function()
461    {
462        var scrollInfo = this._codeMirror.getScrollInfo();
463        // Workaround for CodeMirror's coordsChar incorrect result for "local" mode.
464        return this._coordsCharLocal(scrollInfo);
465    },
466
467    _bottomScrolledLine: function()
468    {
469        var scrollInfo = this._codeMirror.getScrollInfo();
470        scrollInfo.top += scrollInfo.clientHeight;
471        // Workaround for CodeMirror's coordsChar incorrect result for "local" mode.
472        return this._coordsCharLocal(scrollInfo);
473    },
474
475    _scroll: function()
476    {
477        this._delegate.scrollChanged(this._topScrolledLine());
478    },
479
480    /**
481     * @param {number} lineNumber
482     */
483    scrollToLine: function(lineNumber)
484    {
485        function performScroll()
486        {
487            var pos = CodeMirror.Pos(lineNumber, 0);
488            var coords = this._codeMirror.charCoords(pos, "local");
489            this._codeMirror.scrollTo(0, coords.top);
490        }
491
492        setTimeout(performScroll.bind(this), 0);
493    },
494
495    /**
496     * @return {WebInspector.TextRange}
497     */
498    selection: function(textRange)
499    {
500        var start = this._codeMirror.getCursor(true);
501        var end = this._codeMirror.getCursor(false);
502
503        if (start.line > end.line || (start.line == end.line && start.ch > end.ch))
504            return this._toRange(end, start);
505
506        return this._toRange(start, end);
507    },
508
509    /**
510     * @return {WebInspector.TextRange?}
511     */
512    lastSelection: function()
513    {
514        return this._lastSelection;
515    },
516
517    /**
518     * @param {WebInspector.TextRange} textRange
519     */
520    setSelection: function(textRange)
521    {
522        function performSelectionSet()
523        {
524            this._lastSelection = textRange;
525            var pos = this._toPos(textRange);
526            this._codeMirror.setSelection(pos.start, pos.end);
527        }
528
529        setTimeout(performSelectionSet.bind(this), 0);
530    },
531
532    /**
533     * @param {string} text
534     */
535    setText: function(text)
536    {
537        this._codeMirror.setValue(text);
538    },
539
540    /**
541     * @return {string}
542     */
543    text: function()
544    {
545        return this._codeMirror.getValue();
546    },
547
548    /**
549     * @return {WebInspector.TextRange}
550     */
551    range: function()
552    {
553        var lineCount = this.linesCount;
554        var lastLine = this._codeMirror.getLine(lineCount - 1);
555        return this._toRange({ line: 0, ch: 0 }, { line: lineCount - 1, ch: lastLine.length });
556    },
557
558    /**
559     * @param {number} lineNumber
560     * @return {string}
561     */
562    line: function(lineNumber)
563    {
564        return this._codeMirror.getLine(lineNumber);
565    },
566
567    /**
568     * @return {number}
569     */
570    get linesCount()
571    {
572        return this._codeMirror.lineCount();
573    },
574
575    /**
576     * @param {number} line
577     * @param {string} name
578     * @param {Object?} value
579     */
580    setAttribute: function(line, name, value)
581    {
582        var handle = this._codeMirror.getLineHandle(line);
583        if (handle.attributes === undefined) handle.attributes = {};
584        handle.attributes[name] = value;
585    },
586
587    /**
588     * @param {number} line
589     * @param {string} name
590     * @return {Object|null} value
591     */
592    getAttribute: function(line, name)
593    {
594        var handle = this._codeMirror.getLineHandle(line);
595        return handle.attributes && handle.attributes[name] !== undefined ? handle.attributes[name] : null;
596    },
597
598    /**
599     * @param {number} line
600     * @param {string} name
601     */
602    removeAttribute: function(line, name)
603    {
604        var handle = this._codeMirror.getLineHandle(line);
605        if (handle && handle.attributes)
606            delete handle.attributes[name];
607    },
608
609    _toPos: function(range)
610    {
611        return {
612            start: {line: range.startLine, ch: range.startColumn},
613            end: {line: range.endLine, ch: range.endColumn}
614        }
615    },
616
617    _toRange: function(start, end)
618    {
619        return new WebInspector.TextRange(start.line, start.ch, end.line, end.ch);
620    },
621
622    __proto__: WebInspector.View.prototype
623}
624
625WebInspector.CodeMirrorTextEditor.TokenHighlighter = function(codeMirror)
626{
627    this._codeMirror = codeMirror;
628    this._codeMirror.on("cursorActivity", this._cursorChange.bind(this));
629}
630
631WebInspector.CodeMirrorTextEditor.TokenHighlighter.prototype = {
632    _cursorChange: function()
633    {
634        this._codeMirror.operation(this._removeHighlight.bind(this));
635        var selectionStart = this._codeMirror.getCursor("start");
636        var selectionEnd = this._codeMirror.getCursor("end");
637        if (selectionStart.line !== selectionEnd.line)
638            return;
639        if (selectionStart.ch === selectionEnd.ch)
640            return;
641
642        var selectedText = this._codeMirror.getSelection();
643        if (this._isWord(selectedText, selectionStart.line, selectionStart.ch, selectionEnd.ch))
644            this._codeMirror.operation(this._addHighlight.bind(this, selectedText, selectionStart));
645    },
646
647    _isWord: function(selectedText, lineNumber, startColumn, endColumn)
648    {
649        var line = this._codeMirror.getLine(lineNumber);
650        var leftBound = startColumn === 0 || !WebInspector.TextUtils.isWordChar(line.charAt(startColumn - 1));
651        var rightBound = endColumn === line.length || !WebInspector.TextUtils.isWordChar(line.charAt(endColumn));
652        return leftBound && rightBound && WebInspector.TextUtils.isWord(selectedText);
653    },
654
655    _removeHighlight: function()
656    {
657        if (this._highlightDescriptor) {
658            this._codeMirror.removeOverlay(this._highlightDescriptor.overlay);
659            this._codeMirror.removeLineClass(this._highlightDescriptor.selectionStart.line, "wrap", "cm-line-with-selection");
660            delete this._highlightDescriptor;
661        }
662    },
663
664    _addHighlight: function(token, selectionStart)
665    {
666        const tokenFirstChar = token.charAt(0);
667        function nextToken(stream)
668        {
669            if (stream.match(token) && (stream.eol() || !WebInspector.TextUtils.isWordChar(stream.peek())))
670                return stream.column() === selectionStart.ch ? "token-highlight column-with-selection" : "token-highlight";
671
672            var eatenChar;
673            do {
674                eatenChar = stream.next();
675            } while (eatenChar && (WebInspector.TextUtils.isWordChar(eatenChar) || stream.peek() !== tokenFirstChar));
676        }
677
678        var overlayMode = {
679            token: nextToken
680        };
681        this._codeMirror.addOverlay(overlayMode);
682        this._codeMirror.addLineClass(selectionStart.line, "wrap", "cm-line-with-selection")
683        this._highlightDescriptor = {
684            overlay: overlayMode,
685            selectionStart: selectionStart
686        };
687    }
688}
689
690WebInspector.CodeMirrorTextEditor.BlockIndentController = function(codeMirror)
691{
692    codeMirror.addKeyMap(this);
693}
694
695WebInspector.CodeMirrorTextEditor.BlockIndentController.prototype = {
696    name: "blockIndentKeymap",
697
698    Enter: function(codeMirror)
699    {
700        if (codeMirror.somethingSelected())
701            return CodeMirror.Pass;
702        var cursor = codeMirror.getCursor();
703        var line = codeMirror.getLine(cursor.line);
704        if (line.substr(cursor.ch - 1, 2) === "{}") {
705            codeMirror.execCommand("newlineAndIndent");
706            codeMirror.setCursor(cursor);
707            codeMirror.execCommand("newlineAndIndent");
708        } else
709            return CodeMirror.Pass;
710    }
711}
712
713WebInspector.CodeMirrorTextEditor.FixWordMovement = function(codeMirror)
714{
715    function moveLeft(shift, codeMirror)
716    {
717        var cursor = codeMirror.getCursor("head");
718        if (cursor.ch !== 0 || cursor.line === 0)
719            return CodeMirror.Pass;
720        codeMirror.setExtending(shift);
721        codeMirror.execCommand("goLineUp");
722        codeMirror.execCommand("goLineEnd")
723        codeMirror.setExtending(false);
724    }
725    function moveRight(shift, codeMirror)
726    {
727        var cursor = codeMirror.getCursor("head");
728        var line = codeMirror.getLine(cursor.line);
729        if (cursor.ch !== line.length || cursor.line + 1 === codeMirror.lineCount())
730            return CodeMirror.Pass;
731        codeMirror.setExtending(shift);
732        codeMirror.execCommand("goLineDown");
733        codeMirror.execCommand("goLineStart");
734        codeMirror.setExtending(false);
735    }
736
737    var modifierKey = WebInspector.isMac() ? "Alt" : "Ctrl";
738    var leftKey = modifierKey + "-Left";
739    var rightKey = modifierKey + "-Right";
740    var keyMap = {};
741    keyMap[leftKey] = moveLeft.bind(this, false);
742    keyMap[rightKey] = moveRight.bind(this, false);
743    keyMap["Shift-" + leftKey] = moveLeft.bind(this, true);
744    keyMap["Shift-" + rightKey] = moveRight.bind(this, true);
745    codeMirror.addKeyMap(keyMap);
746}
747