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