282c66ab5d0d2bb7f0d2e36034221b71e5f32660
[WebKit-https.git] / Source / WebCore / inspector / front-end / CodeMirrorTextEditor.js
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
31 importScript("cm/codemirror.js");
32 importScript("cm/css.js");
33 importScript("cm/javascript.js");
34 importScript("cm/xml.js");
35 importScript("cm/htmlmixed.js");
36
37 /**
38  * @constructor
39  * @extends {WebInspector.View}
40  * @implements {WebInspector.TextEditor}
41  * @param {?string} url
42  * @param {WebInspector.TextEditorDelegate} delegate
43  */
44 WebInspector.CodeMirrorTextEditor = function(url, delegate)
45 {
46     WebInspector.View.call(this);
47     this._delegate = delegate;
48     this._url = url;
49
50     this.registerRequiredCSS("codemirror.css");
51     this.registerRequiredCSS("cmdevtools.css");
52
53     this._codeMirror = window.CodeMirror(this.element, {
54         lineNumbers: true,
55         gutters: ["CodeMirror-linenumbers", "breakpoints"]
56     });
57
58     this._codeMirror.on("change", this._change.bind(this));
59     this._codeMirror.on("gutterClick", this._gutterClick.bind(this));
60     this.element.addEventListener("contextmenu", this._contextMenu.bind(this));
61
62     this._lastRange = this.range();
63
64     this.element.firstChild.addStyleClass("source-code");
65     this.element.firstChild.addStyleClass("fill");
66     this._elementToWidget = new Map();
67 }
68
69 WebInspector.CodeMirrorTextEditor.prototype = {
70     /**
71      * @param {string} mimeType
72      */
73     set mimeType(mimeType)
74     {
75         this._codeMirror.setOption("mode", mimeType);
76         switch(mimeType) {
77             case "text/html": this._codeMirror.setOption("theme", "web-inspector-html"); break;
78             case "text/css": this._codeMirror.setOption("theme", "web-inspector-css"); break;
79             case "text/javascript": this._codeMirror.setOption("theme", "web-inspector-js"); break;
80         }
81     },
82
83     /**
84      * @param {boolean} readOnly
85      */
86     setReadOnly: function(readOnly)
87     {
88         this._codeMirror.setOption("readOnly", readOnly);
89     },
90
91     /**
92      * @return {boolean}
93      */
94     readOnly: function()
95     {
96         return !!this._codeMirror.getOption("readOnly");
97     },
98
99     /**
100      * @return {Element}
101      */
102     defaultFocusedElement: function()
103     {
104         return this.element.firstChild;
105     },
106
107     focus: function()
108     {
109         this._codeMirror.focus();
110     },
111
112     beginUpdates: function() { },
113
114     endUpdates: function() { },
115
116     /**
117      * @param {number} lineNumber
118      */
119     revealLine: function(lineNumber)
120     {
121         this._codeMirror.setCursor({ line: lineNumber, ch: 0 });
122         this._codeMirror.scrollIntoView();
123     },
124
125     _gutterClick: function(instance, lineNumber, gutter, event)
126     {
127         this.dispatchEventToListeners(WebInspector.TextEditor.Events.GutterClick, { lineNumber: lineNumber, event: event });
128     },
129
130     _contextMenu: function(event)
131     {
132         var contextMenu = new WebInspector.ContextMenu(event);
133         var target = event.target.enclosingNodeOrSelfWithClass("CodeMirror-gutter-elt");
134         if (target)
135             this._delegate.populateLineGutterContextMenu(contextMenu, parseInt(target.textContent, 10) - 1);
136         else
137             this._delegate.populateTextAreaContextMenu(contextMenu, null);
138         contextMenu.show();
139     },
140
141     /**
142      * @param {number} lineNumber
143      * @param {boolean} disabled
144      * @param {boolean} conditional
145      */
146     addBreakpoint: function(lineNumber, disabled, conditional)
147     {
148         var element = document.createElement("span");
149         element.textContent = lineNumber + 1;
150         element.className = "cm-breakpoint" + (disabled ? " cm-breakpoint-disabled" : "") + (conditional ? " cm-breakpoint-conditional" : "");
151         this._codeMirror.setGutterMarker(lineNumber, "breakpoints", element);
152     },
153
154     /**
155      * @param {number} lineNumber
156      */
157     removeBreakpoint: function(lineNumber)
158     {
159         this._codeMirror.setGutterMarker(lineNumber, "breakpoints", null);
160     },
161
162     /**
163      * @param {number} lineNumber
164      */
165     setExecutionLine: function(lineNumber)
166     {
167         this._executionLine = this._codeMirror.getLineHandle(lineNumber);
168         this._codeMirror.addLineClass(this._executionLine, null, "cm-execution-line");
169     },
170
171     clearExecutionLine: function()
172     {
173         if (this._executionLine)
174             this._codeMirror.removeLineClass(this._executionLine, null, "cm-execution-line");
175         delete this._executionLine;
176     },
177
178     /**
179      * @param {number} lineNumber
180      * @param {Element} element
181      */
182     addDecoration: function(lineNumber, element)
183     {
184         var widget = this._codeMirror.addLineWidget(lineNumber, element);
185         this._elementToWidget.put(element, widget);
186     },
187
188     /**
189      * @param {number} lineNumber
190      * @param {Element} element
191      */
192     removeDecoration: function(lineNumber, element)
193     {
194         var widget = this._elementToWidget.remove(element);
195         if (widget)
196             this._codeMirror.removeLineWidget(widget);
197     },
198
199     /**
200      * @param {WebInspector.TextRange} range
201      */
202     markAndRevealRange: function(range)
203     {
204         if (range)
205             this.setSelection(range);
206     },
207
208     /**
209      * @param {number} lineNumber
210      */
211     highlightLine: function(lineNumber)
212     {
213         this.clearLineHighlight();
214         this._highlightedLine = this._codeMirror.getLineHandle(lineNumber);
215         if (!this._highlightedLine)
216           return;
217         this.revealLine(lineNumber);
218         this._codeMirror.addLineClass(this._highlightedLine, null, "cm-highlight");
219         this._clearHighlightTimeout = setTimeout(this.clearLineHighlight.bind(this), 2000);
220     },
221
222     clearLineHighlight: function()
223     {
224         if (this._clearHighlightTimeout)
225             clearTimeout(this._clearHighlightTimeout);
226         delete this._clearHighlightTimeout;
227
228          if (this._highlightedLine)
229             this._codeMirror.removeLineClass(this._highlightedLine, null, "cm-highlight");
230         delete this._highlightedLine;
231     },
232
233     /**
234      * @return {Array.<Element>}
235      */
236     elementsToRestoreScrollPositionsFor: function()
237     {
238         return [];
239     },
240
241     /**
242      * @param {WebInspector.TextEditor} textEditor
243      */
244     inheritScrollPositions: function(textEditor)
245     {
246     },
247
248     onResize: function()
249     {
250         this._codeMirror.refresh();
251     },
252
253     /**
254      * @param {WebInspector.TextRange} range
255      * @param {string} text
256      * @return {WebInspector.TextRange}
257      */
258     editRange: function(range, text)
259     {
260         var pos = this._toPos(range);
261         this._codeMirror.replaceRange(text, pos.start, pos.end);
262         var newRange = this._toRange(pos.start, this._codeMirror.posFromIndex(this._codeMirror.indexFromPos(pos.start) + text.length));
263         this._delegate.onTextChanged(range, newRange);
264         return newRange;
265     },
266
267     _change: function()
268     {
269         var widgets = this._elementToWidget.values();
270         for (var i = 0; i < widgets.length; ++i)
271             this._codeMirror.removeLineWidget(widgets[i]);
272         this._elementToWidget.clear();
273
274         var newRange = this.range();
275         this._delegate.onTextChanged(this._lastRange, newRange);
276         this._lastRange = newRange;
277     },
278
279     /**
280      * @param {number} lineNumber
281      */
282     scrollToLine: function(lineNumber)
283     {
284         this._codeMirror.setCursor({line:lineNumber, ch:0});
285     },
286
287     /**
288      * @return {WebInspector.TextRange}
289      */
290     selection: function(textRange)
291     {
292         var start = this._codeMirror.getCursor(true);
293         var end = this._codeMirror.getCursor(false);
294
295         if (start.line > end.line || (start.line == end.line && start.ch > end.ch))
296             return this._toRange(end, start);
297
298         return this._toRange(start, end);
299     },
300
301     /**
302      * @return {WebInspector.TextRange?}
303      */
304     lastSelection: function()
305     {
306         return this._lastSelection;
307     },
308
309     /**
310      * @param {WebInspector.TextRange} textRange
311      */
312     setSelection: function(textRange)
313     {
314         this._lastSelection = textRange;
315         var pos = this._toPos(textRange);
316         this._codeMirror.setSelection(pos.start, pos.end);
317     },
318
319     /**
320      * @param {string} text
321      */
322     setText: function(text)
323     {
324         this._codeMirror.setValue(text);
325     },
326
327     /**
328      * @return {string}
329      */
330     text: function()
331     {
332         return this._codeMirror.getValue();
333     },
334
335     /**
336      * @return {WebInspector.TextRange}
337      */
338     range: function()
339     {
340         var lineCount = this.linesCount;
341         var lastLine = this._codeMirror.getLine(lineCount - 1);
342         return this._toRange({ line: 0, ch: 0 }, { line: lineCount - 1, ch: lastLine.length });
343     },
344
345     /**
346      * @param {number} lineNumber
347      * @return {string}
348      */
349     line: function(lineNumber)
350     {
351         return this._codeMirror.getLine(lineNumber);
352     },
353
354     /**
355      * @return {number}
356      */
357     get linesCount()
358     {
359         return this._codeMirror.lineCount();
360     },
361
362     /**
363      * @param {number} line
364      * @param {string} name
365      * @param {Object?} value
366      */
367     setAttribute: function(line, name, value)
368     {
369         var handle = this._codeMirror.getLineHandle(line);
370         if (handle.attributes === undefined) handle.attributes = {};
371         handle.attributes[name] = value;
372     },
373
374     /**
375      * @param {number} line
376      * @param {string} name
377      * @return {Object|null} value
378      */
379     getAttribute: function(line, name)
380     {
381         var handle = this._codeMirror.getLineHandle(line);
382         return handle.attributes && handle.attributes[name] !== undefined ? handle.attributes[name] : null;
383     },
384
385     /**
386      * @param {number} line
387      * @param {string} name
388      */
389     removeAttribute: function(line, name)
390     {
391         var handle = this._codeMirror.getLineHandle(line);
392         if (handle && handle.attributes)
393             delete handle.attributes[name];
394     },
395
396     _toPos: function(range)
397     {
398         return {
399             start: {line: range.startLine, ch: range.startColumn},
400             end: {line: range.endLine, ch: range.endColumn}
401         }
402     },
403
404     _toRange: function(start, end)
405     {
406         return new WebInspector.TextRange(start.line, start.ch, end.line, end.ch);
407     },
408
409     __proto__: WebInspector.View.prototype
410 }