OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 /** | |
6 * @fileoverview This class represents a single terminal screen full of text. | |
7 * | |
8 * It maintains the current cursor position and has basic methods for text | |
9 * insert and overwrite, and adding or removing rows from the screen. | |
10 * | |
11 * This class has no knowledge of the scrollback buffer. | |
12 * | |
13 * The number of rows on the screen is determined only by the number of rows | |
14 * that the caller inserts into the screen. If a caller wants to ensure a | |
15 * constant number of rows on the screen, it's their responsibility to remove a | |
16 * row for each row inserted. | |
17 * | |
18 * The screen width, in contrast, is enforced locally. | |
19 * | |
20 * | |
21 * In practice... | |
22 * - The hterm.Terminal class holds two hterm.Screen instances. One for the | |
23 * primary screen and one for the alternate screen. | |
24 * | |
25 * - The html.Screen class only cares that rows are HTMLElements. In the | |
26 * larger context of hterm, however, the rows happen to be displayed by an | |
27 * hterm.ScrollPort and have to follow a few rules as a result. Each | |
28 * row must be rooted by the custom HTML tag 'x-row', and each must have a | |
29 * rowIndex property that corresponds to the index of the row in the context | |
30 * of the scrollback buffer. These invariants are enforced by hterm.Terminal | |
31 * because that is the class using the hterm.Screen in the context of an | |
32 * hterm.ScrollPort. | |
33 */ | |
34 | |
35 /** | |
36 * Create a new screen instance. | |
37 * | |
38 * The screen initially has no rows and a maximum column count of 0. | |
39 * | |
40 * @param {integer} opt_columnCount The maximum number of columns for this | |
41 * screen. See insertString() and overwriteString() for information about | |
42 * what happens when too many characters are added too a row. Defaults to | |
43 * 0 if not provided. | |
44 */ | |
45 hterm.Screen = function(opt_columnCount) { | |
46 /** | |
47 * Public, read-only access to the rows in this screen. | |
48 */ | |
49 this.rowsArray = []; | |
50 | |
51 // The max column width for this screen. | |
52 this.columnCount_ = opt_columnCount || 0; | |
53 | |
54 // Current zero-based cursor coordinates. (-1, -1) implies that the cursor | |
55 // is uninitialized. | |
56 this.cursorPosition = new hterm.RowCol(-1, -1); | |
57 | |
58 // The node containing the row that the cursor is positioned on. | |
59 this.cursorRowNode_ = null; | |
60 | |
61 // The node containing the span of text that the cursor is positioned on. | |
62 this.cursorNode_ = null; | |
63 | |
64 // The offset into cursorNode_ where the cursor is positioned. | |
65 this.cursorOffset_ = null; | |
66 }; | |
67 | |
68 /** | |
69 * Return the screen size as an hterm.Size object. | |
70 * | |
71 * @return {hterm.Size} hterm.Size object representing the current number | |
72 * of rows and columns in this screen. | |
73 */ | |
74 hterm.Screen.prototype.getSize = function() { | |
75 return new hterm.Size(this.columnCount_, this.rowsArray.length); | |
76 }; | |
77 | |
78 /** | |
79 * Return the current number of rows in this screen. | |
80 * | |
81 * @return {integer} The number of rows in this screen. | |
82 */ | |
83 hterm.Screen.prototype.getHeight = function() { | |
84 return this.rowsArray.length; | |
85 }; | |
86 | |
87 /** | |
88 * Return the current number of columns in this screen. | |
89 * | |
90 * @return {integer} The number of columns in this screen. | |
91 */ | |
92 hterm.Screen.prototype.getWidth = function() { | |
93 return this.columnCount_; | |
94 }; | |
95 | |
96 /** | |
97 * Set the maximum number of columns per row. | |
98 * | |
99 * TODO(rginda): This should probably clip existing rows if the count is | |
100 * decreased. | |
101 * | |
102 * @param {integer} count The maximum number of columns per row. | |
103 */ | |
104 hterm.Screen.prototype.setColumnCount = function(count) { | |
105 this.columnCount_ = count; | |
106 }; | |
107 | |
108 /** | |
109 * Remove the first row from the screen and return it. | |
110 * | |
111 * @return {HTMLElement} The first row in this screen. | |
112 */ | |
113 hterm.Screen.prototype.shiftRow = function() { | |
114 return this.shiftRows(1)[0]; | |
115 } | |
116 | |
117 /** | |
118 * Remove rows from the top of the screen and return them as an array. | |
119 * | |
120 * @param {integer} count The number of rows to remove. | |
121 * @return {Array.<HTMLElement>} The selected rows. | |
122 */ | |
123 hterm.Screen.prototype.shiftRows = function(count) { | |
124 return this.rowsArray.splice(0, count); | |
125 }; | |
126 | |
127 /** | |
128 * Insert a row at the top of the screen. | |
129 * | |
130 * @param {HTMLElement} The row to insert. | |
131 */ | |
132 hterm.Screen.prototype.unshiftRow = function(row) { | |
133 this.rowsArray.splice(0, 0, row); | |
134 }; | |
135 | |
136 /** | |
137 * Insert rows at the top of the screen. | |
138 * | |
139 * @param {Array.<HTMLElement>} The rows to insert. | |
140 */ | |
141 hterm.Screen.prototype.unshiftRows = function(rows) { | |
142 rows.unshift.apply(this.rowsArray, rows); | |
dgozman
2011/11/29 11:35:42
Why rows.unshift.apply, and not this.rowsArray.uns
rginda
2011/11/29 19:26:12
Done.
| |
143 }; | |
144 | |
145 /** | |
146 * Remove the last row from the screen and return it. | |
147 * | |
148 * @return {HTMLElement} The last row in this screen. | |
149 */ | |
150 hterm.Screen.prototype.popRow = function() { | |
151 return this.popRows(1)[0]; | |
152 }; | |
153 | |
154 /** | |
155 * Remove rows from the bottom of the screen and return them as an array. | |
156 * | |
157 * @param {integer} count The number of rows to remove. | |
158 * @return {Array.<HTMLElement>} The selected rows. | |
159 */ | |
160 hterm.Screen.prototype.popRows = function(count) { | |
161 return this.rowsArray.splice(this.rowsArray.length - count, count); | |
162 }; | |
163 | |
164 /** | |
165 * Insert a row at the bottom of the screen. | |
166 * | |
167 * @param {HTMLElement} The row to insert. | |
168 */ | |
169 hterm.Screen.prototype.pushRow = function(row) { | |
170 var start = this.rowsArray.length; | |
dgozman
2011/11/29 11:35:42
start not used
rginda
2011/11/29 19:26:12
Done.
| |
171 this.rowsArray.push(row); | |
172 }; | |
173 | |
174 /** | |
175 * Insert rows at the bottom of the screen. | |
176 * | |
177 * @param {Array.<HTMLElement>} The rows to insert. | |
178 */ | |
179 hterm.Screen.prototype.pushRows = function(rows) { | |
180 var start = this.rowsArray.length; | |
dgozman
2011/11/29 11:35:42
start not used
rginda
2011/11/29 19:26:12
Done.
| |
181 rows.push.apply(this.rowsArray, rows); | |
182 }; | |
183 | |
184 /** | |
185 * Insert a row at the specified column of the screen. | |
186 * | |
187 * @param {HTMLElement} The row to insert. | |
188 */ | |
189 hterm.Screen.prototype.insertRow = function(index, row) { | |
190 this.rowsArray.splice(index, 0, row); | |
191 }; | |
192 | |
193 /** | |
194 * Insert rows at the specified column of the screen. | |
195 * | |
196 * @param {Array.<HTMLElement>} The rows to insert. | |
197 */ | |
198 hterm.Screen.prototype.insertRows = function(index, rows) { | |
199 for (var i = 0; i < rows.length; i++) { | |
200 this.rowsArray.splice(index + i, 0, rows[i]); | |
arv (Not doing code reviews)
2011/11/28 18:21:00
Array.prototype.splice.apply(this.rowsArray, index
rginda
2011/11/28 20:39:47
That's the way I had written it at first, but it's
arv (Not doing code reviews)
2011/11/28 21:33:22
My bad.
You could use bind but at this point I'm
dgozman
2011/11/29 11:35:42
Maybe, splice.apply will be faster? It's just two
rginda
2011/11/29 19:26:12
If I modify the rows array it'll trash something t
| |
201 } | |
202 }; | |
203 | |
204 /** | |
205 * Remove a last row from the specified column of the screen and return it. | |
206 * | |
207 * @return {HTMLElement} The selected row. | |
208 */ | |
209 hterm.Screen.prototype.removeRow = function(index) { | |
210 return this.rowsArray.splice(index, 1)[0]; | |
211 }; | |
212 | |
213 /** | |
214 * Remove rows from the bottom of the screen and return them as an array. | |
215 * | |
216 * @param {integer} count The number of rows to remove. | |
217 * @return {Array.<HTMLElement>} The selected rows. | |
218 */ | |
219 hterm.Screen.prototype.removeRows = function(index, count) { | |
220 return this.rowsArray.splice(index, count); | |
221 }; | |
222 | |
223 /** | |
224 * Invalidate the current cursor position. | |
225 * | |
226 * This sets this.cursorPosition to (-1, -1) and clears out some internal | |
227 * data. | |
228 * | |
229 * Attempting to insert or overwrite text while the cursor position is invalid | |
230 * will raise an obscure exception. | |
231 */ | |
232 hterm.Screen.prototype.invalidateCursorPosition = function() { | |
233 this.cursorPosition.move(-1, -1); | |
234 this.cursorRowNode_ = null; | |
235 this.cursorNode_ = null; | |
236 this.cursorOffset_ = null; | |
237 }; | |
238 | |
239 /** | |
240 * Clear the contents of a selected row. | |
241 * | |
242 * TODO: Make this clear in the current style... somehow. We can't just | |
243 * fill the row with spaces, since they would have potential to mess up the | |
244 * terminal (for example, in insert mode, they might wrap around to the next | |
245 * line. | |
246 * | |
247 * @param {integer} index The zero-based index to clear. | |
248 */ | |
249 hterm.Screen.prototype.clearRow = function(index) { | |
250 if (index == this.cursorPosition.row) { | |
251 this.clearCursorRow(); | |
252 } else { | |
253 var row = this.rowsArray[index]; | |
254 row.innerHTML = ''; | |
255 row.appendChild(row.ownerDocument.createTextNode('')) | |
dgozman
2011/11/29 11:35:42
semicolon
rginda
2011/11/29 19:26:12
Done.
| |
256 } | |
257 }; | |
258 | |
259 /** | |
260 * Clear the contents of the cursor row. | |
261 * | |
262 * TODO: Same comment as clearRow(). | |
263 */ | |
264 hterm.Screen.prototype.clearCursorRow = function() { | |
265 this.cursorRowNode_.innerHTML = ''; | |
266 var text = this.cursorRowNode_.ownerDocument.createTextNode(''); | |
267 this.cursorRowNode_.appendChild(text); | |
268 this.cursorOffset_ = 0; | |
269 this.cursorNode_ = text; | |
270 this.cursorPosition.column = 0; | |
271 }; | |
272 | |
273 /** | |
274 * Relocate the cursor to a give row and column. | |
275 * | |
276 * @param {integer} row The zero based row. | |
277 * @param {integer} column The zero based column. | |
278 */ | |
279 hterm.Screen.prototype.setCursorPosition = function(row, column) { | |
280 var currentColumn = 0; | |
281 if (row >= this.rowsArray.length) | |
282 throw 'Row out of bounds: ' + row; | |
283 | |
284 var rowNode = this.rowsArray[row]; | |
285 var node = rowNode.firstChild; | |
286 | |
287 if (!node) { | |
288 node = rowNode.ownerDocument.createTextNode(''); | |
289 rowNode.appendChild(node); | |
290 } | |
291 | |
292 if (rowNode == this.cursorRowNode_) { | |
293 if (column >= this.cursorPosition.column - this.cursorOffset_) { | |
294 node = this.cursorNode_; | |
295 currentColumn = this.cursorPosition.column - this.cursorOffset_; | |
296 } | |
297 } else { | |
298 this.cursorRowNode_ = rowNode; | |
299 } | |
300 | |
301 this.cursorPosition.move(row, column); | |
302 | |
303 while (node) { | |
304 var offset = column - currentColumn; | |
305 var textContent = node.textContent; | |
306 if (!node.nextSibling || textContent.length > offset) { | |
307 this.cursorNode_ = node; | |
308 this.cursorOffset_ = offset; | |
309 return; | |
310 } | |
311 | |
312 currentColumn += textContent.length; | |
313 node = node.nextSibling; | |
dgozman
2011/11/29 11:35:42
This code assumes that each row may contain only d
rginda
2011/11/29 19:26:12
We're going to want to keep this requirement to si
| |
314 } | |
315 }; | |
316 | |
317 /** | |
318 * Insert the given string at the cursor position, with the understanding that | |
319 * the insert will cause the column to overflow, and the overflow will be | |
320 * in a different text style than where the cursor is currently located. | |
321 * | |
322 * TODO: Implement this. | |
323 */ | |
324 hterm.Screen.prototype.spliceStringAndWrap_ = function(str) { | |
325 throw 'NOT IMPLEMENTED'; | |
326 }; | |
327 | |
328 /** | |
329 * Insert a string at the current cursor position. | |
330 * | |
331 * If the insert causes the column to overflow, the extra text is returned. | |
332 * | |
333 * @return {string} Text that overflowed the column, or null if nothing | |
334 * overflowed. | |
335 */ | |
336 hterm.Screen.prototype.insertString = function(str) { | |
337 if (this.cursorPosition.column == this.columnCount_) | |
338 return str; | |
339 | |
340 var totalRowText = this.cursorRowNode_.textContent; | |
341 | |
342 // There may not be underlying characters to support the current cursor | |
343 // position, since they don't get inserted until they're necessary. | |
344 var missingSpaceCount = Math.max(this.cursorPosition.column - | |
345 totalRowText.length, | |
346 0); | |
347 | |
348 var overflowCount = Math.max(totalRowText.length + missingSpaceCount + | |
349 str.length - this.columnCount_, | |
klimek
2011/11/24 19:13:51
Indent.
rginda
2011/11/28 20:39:47
Done.
| |
350 0); | |
351 | |
352 if (overflowCount > 0 && this.cursorNode_.nextSibling) { | |
353 // We're going to overflow, but there is text after the cursor with a | |
354 // different set of attributes. This is going to take some effort. | |
355 return this.spliceStringAndWrap_(str); | |
356 } | |
357 | |
358 // Wrapping is simple since the cursor is located in the last block of text | |
359 // on the line. | |
360 | |
361 var cursorNodeText = this.cursorNode_.textContent; | |
362 var leadingText = cursorNodeText.substr(0, this.cursorOffset_); | |
363 var trailingText = str + cursorNodeText.substr(this.cursorOffset_); | |
364 var overflowText = trailingText.substr(trailingText.length - overflowCount); | |
365 trailingText = trailingText.substr(0, trailingText.length - overflowCount); | |
366 | |
367 this.cursorNode_.textContent = ( | |
368 leadingText + | |
369 hterm.getWhitespace(missingSpaceCount) + | |
370 trailingText | |
371 ); | |
372 | |
373 var cursorDelta = Math.min(str.length, trailingText.length); | |
374 this.cursorOffset_ += cursorDelta; | |
375 this.cursorPosition.column += cursorDelta; | |
376 | |
377 return overflowText || null; | |
378 }; | |
379 | |
380 /** | |
381 * Overwrite the text at the current cursor position. | |
382 * | |
383 * If the text causes the column to overflow, the extra text is returned. | |
384 * | |
385 * @return {string} Text that overflowed the column, or null if nothing | |
386 * overflowed. | |
387 */ | |
388 hterm.Screen.prototype.overwriteString = function(str) { | |
389 var maxLength = this.columnCount_ - this.cursorPosition.column; | |
390 if (!maxLength) | |
391 return str; | |
392 | |
393 this.deleteChars(Math.min(str.length, maxLength)); | |
394 return this.insertString(str); | |
395 }; | |
396 | |
397 /** | |
398 * Forward-delete one or more characters at the current cursor position. | |
399 * | |
400 * Text to the right of the deleted characters is shifted left. Only affects | |
401 * characters on the same row as the cursor. | |
402 * | |
403 * @param {integer} count The number of characters to delete. This is clamped | |
404 * to the column width minus the cursor column. | |
405 */ | |
406 hterm.Screen.prototype.deleteChars = function(count) { | |
407 var node = this.cursorNode_; | |
408 var offset = this.cursorOffset_; | |
409 | |
410 while (node && count) { | |
411 var startLength = node.textContent.length; | |
412 | |
413 node.textContent = node.textContent.substr(0, offset) + | |
414 node.textContent.substr(offset + count); | |
415 | |
416 var endLength = node.textContent.length; | |
417 count -= startLength - endLength; | |
418 | |
419 if (endLength == 0 && node != this.cursorNode_) { | |
420 var nextNode = node.nextSibling; | |
421 node.parentNode.removeChild(node); | |
422 node = nextNode; | |
423 } else { | |
424 node = node.nextSibling; | |
425 } | |
426 | |
427 offset = 0; | |
428 } | |
429 }; | |
OLD | NEW |