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