Home | History | Annotate | Download | only in front-end
      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 WebInspector.TextRange = function(startLine, startColumn, endLine, endColumn)
     32 {
     33     this.startLine = startLine;
     34     this.startColumn = startColumn;
     35     this.endLine = endLine;
     36     this.endColumn = endColumn;
     37 }
     38 
     39 WebInspector.TextRange.prototype = {
     40     isEmpty: function()
     41     {
     42         return this.startLine === this.endLine && this.startColumn === this.endColumn;
     43     },
     44 
     45     get linesCount()
     46     {
     47         return this.endLine - this.startLine;
     48     },
     49 
     50     clone: function()
     51     {
     52         return new WebInspector.TextRange(this.startLine, this.startColumn, this.endLine, this.endColumn);
     53     }
     54 }
     55 
     56 WebInspector.TextEditorModel = function()
     57 {
     58     this._lines = [""];
     59     this._attributes = [];
     60     this._undoStack = [];
     61     this._noPunctuationRegex = /[^ !%&()*+,-.:;<=>?\[\]\^{|}~]+/;
     62     this._lineBreak = "\n";
     63 }
     64 
     65 WebInspector.TextEditorModel.prototype = {
     66     set changeListener(changeListener)
     67     {
     68         this._changeListener = changeListener;
     69     },
     70 
     71     get linesCount()
     72     {
     73         return this._lines.length;
     74     },
     75 
     76     get text()
     77     {
     78         return this._lines.join(this._lineBreak);
     79     },
     80 
     81     line: function(lineNumber)
     82     {
     83         if (lineNumber >= this._lines.length)
     84             throw "Out of bounds:" + lineNumber;
     85         return this._lines[lineNumber];
     86     },
     87 
     88     lineLength: function(lineNumber)
     89     {
     90         return this._lines[lineNumber].length;
     91     },
     92 
     93     setText: function(range, text)
     94     {
     95         text = text || "";
     96         if (!range) {
     97             range = new WebInspector.TextRange(0, 0, this._lines.length - 1, this._lines[this._lines.length - 1].length);
     98             this._lineBreak = /\r\n/.test(text) ? "\r\n" : "\n";
     99         }
    100         var command = this._pushUndoableCommand(range);
    101         var newRange = this._innerSetText(range, text);
    102         command.range = newRange.clone();
    103 
    104         if (this._changeListener)
    105             this._changeListener(range, newRange, command.text, text);
    106         return newRange;
    107     },
    108 
    109     set replaceTabsWithSpaces(replaceTabsWithSpaces)
    110     {
    111         this._replaceTabsWithSpaces = replaceTabsWithSpaces;
    112     },
    113 
    114     _innerSetText: function(range, text)
    115     {
    116         this._eraseRange(range);
    117         if (text === "")
    118             return new WebInspector.TextRange(range.startLine, range.startColumn, range.startLine, range.startColumn);
    119 
    120         var newLines = text.split(/\r?\n/);
    121         this._replaceTabsIfNeeded(newLines);
    122 
    123         var prefix = this._lines[range.startLine].substring(0, range.startColumn);
    124         var suffix = this._lines[range.startLine].substring(range.startColumn);
    125 
    126         var postCaret = prefix.length;
    127         // Insert text.
    128         if (newLines.length === 1) {
    129             this._setLine(range.startLine, prefix + newLines[0] + suffix);
    130             postCaret += newLines[0].length;
    131         } else {
    132             this._setLine(range.startLine, prefix + newLines[0]);
    133             for (var i = 1; i < newLines.length; ++i)
    134                 this._insertLine(range.startLine + i, newLines[i]);
    135             this._setLine(range.startLine + newLines.length - 1, newLines[newLines.length - 1] + suffix);
    136             postCaret = newLines[newLines.length - 1].length;
    137         }
    138         return new WebInspector.TextRange(range.startLine, range.startColumn,
    139                                           range.startLine + newLines.length - 1, postCaret);
    140     },
    141 
    142     _replaceTabsIfNeeded: function(lines)
    143     {
    144         if (!this._replaceTabsWithSpaces)
    145             return;
    146         var spaces = [ "    ", "   ", "  ", " "];
    147         for (var i = 0; i < lines.length; ++i) {
    148             var line = lines[i];
    149             var index = line.indexOf("\t");
    150             while (index !== -1) {
    151                 line = line.substring(0, index) + spaces[index % 4] + line.substring(index + 1);
    152                 index = line.indexOf("\t", index + 1);
    153             }
    154             lines[i] = line;
    155         }
    156     },
    157 
    158     _eraseRange: function(range)
    159     {
    160         if (range.isEmpty())
    161             return;
    162 
    163         var prefix = this._lines[range.startLine].substring(0, range.startColumn);
    164         var suffix = this._lines[range.endLine].substring(range.endColumn);
    165 
    166         if (range.endLine > range.startLine)
    167             this._removeLines(range.startLine + 1, range.endLine - range.startLine);
    168         this._setLine(range.startLine, prefix + suffix);
    169     },
    170 
    171     _setLine: function(lineNumber, text)
    172     {
    173         this._lines[lineNumber] = text;
    174     },
    175 
    176     _removeLines: function(fromLine, count)
    177     {
    178         this._lines.splice(fromLine, count);
    179         this._attributes.splice(fromLine, count);
    180     },
    181 
    182     _insertLine: function(lineNumber, text)
    183     {
    184         this._lines.splice(lineNumber, 0, text);
    185         this._attributes.splice(lineNumber, 0, {});
    186     },
    187 
    188     wordRange: function(lineNumber, column)
    189     {
    190         return new WebInspector.TextRange(lineNumber, this.wordStart(lineNumber, column, true), lineNumber, this.wordEnd(lineNumber, column, true));
    191     },
    192 
    193     wordStart: function(lineNumber, column, gapless)
    194     {
    195         var line = this._lines[lineNumber];
    196         var prefix = line.substring(0, column).split("").reverse().join("");
    197         var prefixMatch = this._noPunctuationRegex.exec(prefix);
    198         return prefixMatch && (!gapless || prefixMatch.index === 0) ? column - prefixMatch.index - prefixMatch[0].length : column;
    199     },
    200 
    201     wordEnd: function(lineNumber, column, gapless)
    202     {
    203         var line = this._lines[lineNumber];
    204         var suffix = line.substring(column);
    205         var suffixMatch = this._noPunctuationRegex.exec(suffix);
    206         return suffixMatch && (!gapless || suffixMatch.index === 0) ? column + suffixMatch.index + suffixMatch[0].length : column;
    207     },
    208 
    209     copyRange: function(range)
    210     {
    211         if (!range)
    212             range = new WebInspector.TextRange(0, 0, this._lines.length - 1, this._lines[this._lines.length - 1].length);
    213 
    214         var clip = [];
    215         if (range.startLine === range.endLine) {
    216             clip.push(this._lines[range.startLine].substring(range.startColumn, range.endColumn));
    217             return clip.join(this._lineBreak);
    218         }
    219         clip.push(this._lines[range.startLine].substring(range.startColumn));
    220         for (var i = range.startLine + 1; i < range.endLine; ++i)
    221             clip.push(this._lines[i]);
    222         clip.push(this._lines[range.endLine].substring(0, range.endColumn));
    223         return clip.join(this._lineBreak);
    224     },
    225 
    226     setAttribute: function(line, name, value)
    227     {
    228         var attrs = this._attributes[line];
    229         if (!attrs) {
    230             attrs = {};
    231             this._attributes[line] = attrs;
    232         }
    233         attrs[name] = value;
    234     },
    235 
    236     getAttribute: function(line, name)
    237     {
    238         var attrs = this._attributes[line];
    239         return attrs ? attrs[name] : null;
    240     },
    241 
    242     removeAttribute: function(line, name)
    243     {
    244         var attrs = this._attributes[line];
    245         if (attrs)
    246             delete attrs[name];
    247     },
    248 
    249     _pushUndoableCommand: function(range)
    250     {
    251         var command = {
    252             text: this.copyRange(range),
    253             startLine: range.startLine,
    254             startColumn: range.startColumn,
    255             endLine: range.startLine,
    256             endColumn: range.startColumn
    257         };
    258         if (this._inUndo)
    259             this._redoStack.push(command);
    260         else {
    261             if (!this._inRedo)
    262                 this._redoStack = [];
    263             this._undoStack.push(command);
    264         }
    265         return command;
    266     },
    267 
    268     undo: function(callback)
    269     {
    270         this._markRedoableState();
    271 
    272         this._inUndo = true;
    273         var range = this._doUndo(this._undoStack, callback);
    274         delete this._inUndo;
    275 
    276         return range;
    277     },
    278 
    279     redo: function(callback)
    280     {
    281         this.markUndoableState();
    282 
    283         this._inRedo = true;
    284         var range = this._doUndo(this._redoStack, callback);
    285         delete this._inRedo;
    286 
    287         return range;
    288     },
    289 
    290     _doUndo: function(stack, callback)
    291     {
    292         var range = null;
    293         for (var i = stack.length - 1; i >= 0; --i) {
    294             var command = stack[i];
    295             stack.length = i;
    296 
    297             range = this.setText(command.range, command.text);
    298             if (callback)
    299                 callback(command.range, range);
    300             if (i > 0 && stack[i - 1].explicit)
    301                 return range;
    302         }
    303         return range;
    304     },
    305 
    306     markUndoableState: function()
    307     {
    308         if (this._undoStack.length)
    309             this._undoStack[this._undoStack.length - 1].explicit = true;
    310     },
    311 
    312     _markRedoableState: function()
    313     {
    314         if (this._redoStack.length)
    315             this._redoStack[this._redoStack.length - 1].explicit = true;
    316     },
    317 
    318     resetUndoStack: function()
    319     {
    320         this._undoStack = [];
    321     }
    322 }
    323