• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
31WebInspector.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
39WebInspector.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
56WebInspector.TextEditorModel = function()
57{
58    this._lines = [""];
59    this._attributes = [];
60    this._undoStack = [];
61    this._noPunctuationRegex = /[^ !%&()*+,-.:;<=>?\[\]\^{|}~]+/;
62    this._lineBreak = "\n";
63}
64
65WebInspector.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