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}
63
64WebInspector.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