6d73c723b93cdad9d85b77415f00eff58681b90d
[WebKit-https.git] / Source / WebCore / inspector / front-end / TextEditorModel.js
1 /*
2  * Copyright (C) 2009 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 /**
32  * @constructor
33  * @param {number} startLine
34  * @param {number} startColumn
35  * @param {number} endLine
36  * @param {number} endColumn
37  */
38 WebInspector.TextRange = function(startLine, startColumn, endLine, endColumn)
39 {
40     this.startLine = startLine;
41     this.startColumn = startColumn;
42     this.endLine = endLine;
43     this.endColumn = endColumn;
44 }
45
46 WebInspector.TextRange.createFromLocation = function(line, column)
47 {
48     return new WebInspector.TextRange(line, column, line, column);
49 }
50
51 /**
52  * @param {Object} serializedTextRange
53  * @return {WebInspector.TextRange}
54  */
55 WebInspector.TextRange.fromObject = function (serializedTextRange)
56 {
57     return new WebInspector.TextRange(serializedTextRange.startLine, serializedTextRange.startColumn, serializedTextRange.endLine, serializedTextRange.endColumn);
58 }
59
60 WebInspector.TextRange.prototype = {
61     /**
62      * @return {boolean}
63      */
64     isEmpty: function()
65     {
66         return this.startLine === this.endLine && this.startColumn === this.endColumn;
67     },
68
69     /**
70      * @return {number}
71      */
72     get linesCount()
73     {
74         return this.endLine - this.startLine;
75     },
76
77     collapseToEnd: function()
78     {
79         return new WebInspector.TextRange(this.endLine, this.endColumn, this.endLine, this.endColumn);
80     },
81
82     /**
83      * @return {WebInspector.TextRange}
84      */
85     normalize: function()
86     {
87         if (this.startLine > this.endLine || (this.startLine === this.endLine && this.startColumn > this.endColumn))
88             return new WebInspector.TextRange(this.endLine, this.endColumn, this.startLine, this.startColumn);
89         else
90             return this.clone();
91     },
92
93     /**
94      * @return {WebInspector.TextRange}
95      */
96     clone: function()
97     {
98         return new WebInspector.TextRange(this.startLine, this.startColumn, this.endLine, this.endColumn);
99     },
100
101     /**
102      * @return {Object}
103      */
104     serializeToObject: function()
105     {
106         var serializedTextRange = {};
107         serializedTextRange.startLine = this.startLine;
108         serializedTextRange.startColumn = this.startColumn;
109         serializedTextRange.endLine = this.endLine;
110         serializedTextRange.endColumn = this.endColumn;
111         return serializedTextRange;
112     },
113
114     /**
115      * @param {WebInspector.TextRange} other
116      * @return {number}
117      */
118     compareTo: function(other)
119     {
120         if (this.startLine > other.startLine)
121             return 1;
122         if (this.startLine < other.startLine)
123             return -1;
124         if (this.startColumn > other.startColumn)
125             return 1;
126         if (this.startColumn < other.startColumn)
127             return -1;
128         return 0;
129     },
130
131     /**
132      * @param {number} lineOffset
133      * @return {WebInspector.TextRange}
134      */
135     shift: function(lineOffset)
136     {
137         return new WebInspector.TextRange(this.startLine + lineOffset, this.startColumn, this.endLine + lineOffset, this.endColumn);
138     },
139
140     toString: function()
141     {
142         return JSON.stringify(this);
143     }
144 }
145
146 /**
147  * @constructor
148  * @param {WebInspector.TextRange} newRange
149  * @param {string} originalText
150  */
151 WebInspector.TextEditorCommand = function(newRange, originalText)
152 {
153     this.newRange = newRange;
154     this.originalText = originalText;
155 }
156
157 /**
158  * @constructor
159  * @extends {WebInspector.Object}
160  */
161 WebInspector.TextEditorModel = function()
162 {
163     this._lines = [""];
164     this._attributes = [];
165     /** @type {Array.<WebInspector.TextEditorCommand>} */
166     this._undoStack = [];
167     this._noPunctuationRegex = /[^ !%&()*+,-.:;<=>?\[\]\^{|}~]+/;
168     this._lineBreak = "\n";
169 }
170
171 WebInspector.TextEditorModel.Indent = {
172     TwoSpaces: "  ",
173     FourSpaces: "    ",
174     EightSpaces: "        ",
175     TabCharacter: "\t"
176 }
177
178 WebInspector.TextEditorModel.Events = {
179     TextChanged: "TextChanged"
180 }
181
182 WebInspector.TextEditorModel.endsWithBracketRegex = /[{(\[]\s*$/;
183
184 WebInspector.TextEditorModel.prototype = {
185     /**
186      * @return {number}
187      */
188     get linesCount()
189     {
190         return this._lines.length;
191     },
192
193     /**
194      * @return {string}
195      */
196     text: function()
197     {
198         return this._lines.join(this._lineBreak);
199     },
200
201     /**
202      * @return {WebInspector.TextRange}
203      */
204     range: function()
205     {
206         return new WebInspector.TextRange(0, 0, this._lines.length - 1, this._lines[this._lines.length - 1].length);
207     },
208
209     /**
210      * @return {string}
211      */
212     get lineBreak()
213     {
214         return this._lineBreak;
215     },
216
217     /**
218      * @param {number} lineNumber
219      * @return {string}
220      */
221     line: function(lineNumber)
222     {
223         if (lineNumber >= this._lines.length)
224             throw "Out of bounds:" + lineNumber;
225         return this._lines[lineNumber];
226     },
227
228     /**
229      * @param {number} lineNumber
230      * @return {number}
231      */
232     lineLength: function(lineNumber)
233     {
234         return this._lines[lineNumber].length;
235     },
236
237     /**
238      * @param {string} text 
239      */
240     setText: function(text)
241     {
242         this._resetUndoStack();
243         text = text || "";
244         var range = this.range();
245         this._lineBreak = /\r\n/.test(text) ? "\r\n" : "\n";
246         var newRange = this._innerSetText(range, text);
247         this.dispatchEventToListeners(WebInspector.TextEditorModel.Events.TextChanged, { oldRange: range, newRange: newRange});
248     },
249
250     /**
251      * @param {WebInspector.TextRange} range
252      * @param {string} text
253      * @return {WebInspector.TextRange}
254      */
255     editRange: function(range, text)
256     {   
257         if (this._lastEditedRange && (!text || text.indexOf("\n") !== -1 || this._lastEditedRange.endLine !== range.startLine || this._lastEditedRange.endColumn !== range.startColumn))
258             this._markUndoableState();
259         return this._innerEditRange(range, text);
260     },
261
262     /**
263      * @param {WebInspector.TextRange} range
264      * @param {string} text
265      * @return {WebInspector.TextRange}
266      */
267     _innerEditRange: function(range, text)
268     {
269         var originalText = this.copyRange(range);
270         this._lastEditedRange = range;
271         var newRange = range;
272         if (text !== originalText) {
273             newRange = this._innerSetText(range, text);
274             this._pushUndoableCommand(newRange, originalText);
275         }
276
277         this.dispatchEventToListeners(WebInspector.TextEditorModel.Events.TextChanged, { oldRange: range, newRange: newRange, editRange: true });
278         return newRange;
279     },
280
281     /**
282      * @param {WebInspector.TextRange} range
283      * @param {string} text
284      * @return {WebInspector.TextRange}
285      */
286     _innerSetText: function(range, text)
287     {
288         this._eraseRange(range);
289         if (text === "")
290             return new WebInspector.TextRange(range.startLine, range.startColumn, range.startLine, range.startColumn);
291
292         var newLines = text.split(/\r?\n/);
293
294         var prefix = this._lines[range.startLine].substring(0, range.startColumn);
295         var suffix = this._lines[range.startLine].substring(range.startColumn);
296
297         var postCaret = prefix.length;
298         // Insert text.
299         if (newLines.length === 1) {
300             this._setLine(range.startLine, prefix + newLines[0] + suffix);
301             postCaret += newLines[0].length;
302         } else {
303             this._setLine(range.startLine, prefix + newLines[0]);
304             this._insertLines(range, newLines);
305             this._setLine(range.startLine + newLines.length - 1, newLines[newLines.length - 1] + suffix);
306             postCaret = newLines[newLines.length - 1].length;
307         }
308
309         return new WebInspector.TextRange(range.startLine, range.startColumn,
310                                           range.startLine + newLines.length - 1, postCaret);
311     },
312
313     /**
314      * @param {WebInspector.TextRange} range
315      * @param {Array.<string>} newLines
316      */
317     _insertLines: function(range, newLines)
318     {
319         var lines = new Array(this._lines.length + newLines.length - 1);
320         for (var i = 0; i <= range.startLine; ++i)
321             lines[i] = this._lines[i];
322         // Line at [0] is already set via setLine.
323         for (var i = 1; i < newLines.length; ++i)
324             lines[range.startLine + i] = newLines[i];
325         for (var i = range.startLine + newLines.length; i < lines.length; ++i)
326             lines[i] = this._lines[i - newLines.length + 1];
327         this._lines = lines;
328
329         // Adjust attributes, attributes move with the first character of line.
330         var attributes = new Array(lines.length);
331         var insertionIndex = range.startColumn ? range.startLine + 1 : range.startLine;
332         for (var i = 0; i < insertionIndex; ++i)
333             attributes[i] = this._attributes[i];
334         for (var i = insertionIndex + newLines.length - 1; i < attributes.length; ++i)
335             attributes[i] = this._attributes[i - newLines.length + 1];
336         this._attributes = attributes;
337     },
338
339     /**
340      * @param {WebInspector.TextRange} range
341      */
342     _eraseRange: function(range)
343     {
344         if (range.isEmpty())
345             return;
346
347         var prefix = this._lines[range.startLine].substring(0, range.startColumn);
348         var suffix = this._lines[range.endLine].substring(range.endColumn);
349
350         if (range.endLine > range.startLine) {
351             this._lines.splice(range.startLine + 1, range.endLine - range.startLine);
352             // Adjust attributes, attributes move with the first character of line.
353             this._attributes.splice(range.startColumn ? range.startLine + 1 : range.startLine, range.endLine - range.startLine);
354         }
355         this._setLine(range.startLine, prefix + suffix);
356     },
357
358     /**
359      * @param {number} lineNumber
360      * @param {string} text
361      */
362     _setLine: function(lineNumber, text)
363     {
364         this._lines[lineNumber] = text;
365     },
366
367     /**
368      * @param {number} lineNumber
369      * @param {number} column
370      * @return {WebInspector.TextRange}
371      */
372     wordRange: function(lineNumber, column)
373     {
374         return new WebInspector.TextRange(lineNumber, this.wordStart(lineNumber, column, true), lineNumber, this.wordEnd(lineNumber, column, true));
375     },
376
377     /**
378      * @param {number} lineNumber
379      * @param {number} column
380      * @param {boolean} gapless
381      * @return {number}
382      */
383     wordStart: function(lineNumber, column, gapless)
384     {
385         var line = this._lines[lineNumber];
386         var prefix = line.substring(0, column).split("").reverse().join("");
387         var prefixMatch = this._noPunctuationRegex.exec(prefix);
388         return prefixMatch && (!gapless || prefixMatch.index === 0) ? column - prefixMatch.index - prefixMatch[0].length : column;
389     },
390
391     /**
392      * @param {number} lineNumber
393      * @param {number} column
394      * @param {boolean} gapless
395      * @return {number}
396      */
397     wordEnd: function(lineNumber, column, gapless)
398     {
399         var line = this._lines[lineNumber];
400         var suffix = line.substring(column);
401         var suffixMatch = this._noPunctuationRegex.exec(suffix);
402         return suffixMatch && (!gapless || suffixMatch.index === 0) ? column + suffixMatch.index + suffixMatch[0].length : column;
403     },
404
405     /**
406      * @param {WebInspector.TextRange} range
407      * @return {string}  
408      */
409     copyRange: function(range)
410     {
411         if (!range)
412             range = this.range();
413
414         var clip = [];
415         if (range.startLine === range.endLine) {
416             clip.push(this._lines[range.startLine].substring(range.startColumn, range.endColumn));
417             return clip.join(this._lineBreak);
418         }
419         clip.push(this._lines[range.startLine].substring(range.startColumn));
420         for (var i = range.startLine + 1; i < range.endLine; ++i)
421             clip.push(this._lines[i]);
422         clip.push(this._lines[range.endLine].substring(0, range.endColumn));
423         return clip.join(this._lineBreak);
424     },
425
426     /**
427      * @param {number} line
428      * @param {string} name  
429      * @param {Object?} value  
430      */
431     setAttribute: function(line, name, value)
432     {
433         var attrs = this._attributes[line];
434         if (!attrs) {
435             attrs = {};
436             this._attributes[line] = attrs;
437         }
438         attrs[name] = value;
439     },
440
441     /**
442      * @param {number} line
443      * @param {string} name  
444      * @return {Object|null} value  
445      */
446     getAttribute: function(line, name)
447     {
448         var attrs = this._attributes[line];
449         return attrs ? attrs[name] : null;
450     },
451
452     /**
453      * @param {number} line
454      * @param {string} name
455      */
456     removeAttribute: function(line, name)
457     {
458         var attrs = this._attributes[line];
459         if (attrs)
460             delete attrs[name];
461     },
462
463     /**
464      * @param {WebInspector.TextRange} newRange
465      * @param {string} originalText
466      * @return {WebInspector.TextEditorCommand}
467      */
468     _pushUndoableCommand: function(newRange, originalText)
469     {
470         var command = new WebInspector.TextEditorCommand(newRange.clone(), originalText);
471         if (this._inUndo)
472             this._redoStack.push(command);
473         else {
474             if (!this._inRedo)
475                 this._redoStack = [];
476             this._undoStack.push(command);
477         }
478         return command;
479     },
480
481     /**
482      * @return {?WebInspector.TextRange}
483      */
484     undo: function()
485     {
486         if (!this._undoStack.length)
487             return null;
488
489         this._markRedoableState();
490
491         this._inUndo = true;
492         var range = this._doUndo(this._undoStack);
493         delete this._inUndo;
494
495         return range;
496     },
497
498     /**
499      * @return {WebInspector.TextRange}
500      */
501     redo: function()
502     {
503         if (!this._redoStack || !this._redoStack.length)
504             return null;
505         this._markUndoableState();
506
507         this._inRedo = true;
508         var range = this._doUndo(this._redoStack);
509         delete this._inRedo;
510
511         return range;
512     },
513
514     /**
515      * @param {Array.<WebInspector.TextEditorCommand>} stack
516      * @return {WebInspector.TextRange}
517      */
518     _doUndo: function(stack)
519     {
520         var range = null;
521         for (var i = stack.length - 1; i >= 0; --i) {
522             var command = stack[i];
523             stack.length = i;
524             range = this._innerEditRange(command.newRange, command.originalText);
525             if (i > 0 && stack[i - 1].explicit)
526                 return range;
527         }
528         return range;
529     },
530
531     _markUndoableState: function()
532     {
533         if (this._undoStack.length)
534             this._undoStack[this._undoStack.length - 1].explicit = true;
535     },
536
537     _markRedoableState: function()
538     {
539         if (this._redoStack.length)
540             this._redoStack[this._redoStack.length - 1].explicit = true;
541     },
542
543     _resetUndoStack: function()
544     {
545         this._undoStack = [];
546     },
547
548     /**
549      * @param {WebInspector.TextRange} range
550      * @return {WebInspector.TextRange}
551      */
552     indentLines: function(range)
553     {
554         this._markUndoableState();
555
556         var indent = WebInspector.settings.textEditorIndent.get();
557         var newRange = range.clone();
558         // Do not change a selection start position when it is at the beginning of a line
559         if (range.startColumn)
560             newRange.startColumn += indent.length;
561
562         var indentEndLine = range.endLine;
563         if (range.endColumn)
564             newRange.endColumn += indent.length;
565         else
566             indentEndLine--;
567
568         for (var lineNumber = range.startLine; lineNumber <= indentEndLine; lineNumber++)
569             this._innerEditRange(WebInspector.TextRange.createFromLocation(lineNumber, 0), indent);
570
571         return newRange;
572     },
573
574     /**
575      * @param {WebInspector.TextRange} range
576      * @return {WebInspector.TextRange}
577      */
578     unindentLines: function(range)
579     {
580         this._markUndoableState();
581
582         var indent = WebInspector.settings.textEditorIndent.get();
583         var indentLength = indent === WebInspector.TextEditorModel.Indent.TabCharacter ? 4 : indent.length;
584         var lineIndentRegex = new RegExp("^ {1," + indentLength + "}");
585         var newRange = range.clone();
586
587         var indentEndLine = range.endLine;
588         if (!range.endColumn)
589             indentEndLine--;
590
591         for (var lineNumber = range.startLine; lineNumber <= indentEndLine; lineNumber++) {
592             var line = this.line(lineNumber);
593             var firstCharacter = line.charAt(0);
594             var lineIndentLength;
595
596             if (firstCharacter === " ")
597                 lineIndentLength = line.match(lineIndentRegex)[0].length;
598             else if (firstCharacter === "\t")
599                 lineIndentLength = 1;
600             else
601                 continue;
602
603             this._innerEditRange(new WebInspector.TextRange(lineNumber, 0, lineNumber, lineIndentLength), "");
604
605             if (lineNumber === range.startLine)
606                 newRange.startColumn = Math.max(0, newRange.startColumn - lineIndentLength);
607             if (lineNumber === range.endLine)
608                 newRange.endColumn = Math.max(0, newRange.endColumn - lineIndentLength);
609         }
610
611         return newRange;
612     },
613
614     /**
615      * @param {number=} from
616      * @param {number=} to
617      * @return {WebInspector.TextEditorModel}
618      */
619     slice: function(from, to)
620     {
621         var textModel = new WebInspector.TextEditorModel();
622         textModel._lines = this._lines.slice(from, to);
623         textModel._lineBreak = this._lineBreak;
624         return textModel;
625     },
626
627     /**
628      * @param {WebInspector.TextRange} range
629      * @return {WebInspector.TextRange}
630      */
631     growRangeLeft: function(range)
632     {
633         var result = range.clone();
634         if (result.startColumn)
635             --result.startColumn;
636         else if (result.startLine)
637             result.startColumn = this.lineLength(--result.startLine);
638         return result;
639     },
640
641     /**
642      * @param {WebInspector.TextRange} range
643      * @return {WebInspector.TextRange}
644      */
645     growRangeRight: function(range)
646     {
647         var result = range.clone();
648         if (result.endColumn < this.lineLength(result.endLine))
649             ++result.endColumn;
650         else if (result.endLine < this.linesCount) {
651             result.endColumn = 0;
652             ++result.endLine;
653         }
654         return result;
655     },
656
657     __proto__: WebInspector.Object.prototype
658 }