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