blob: f7a12f80400f9d287be1584c556eda19c81f4993 [file] [log] [blame]
rginda87b86462011-12-14 13:48:03 -08001// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
rginda8ba33642011-12-14 12:31:31 -08002// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
rgindacbbd7482012-06-13 15:06:16 -07005'use strict';
6
Ricky Liang48f05cb2013-12-31 23:35:29 +08007lib.rtdep('lib.f', 'lib.wc',
rgindacbbd7482012-06-13 15:06:16 -07008 'hterm.RowCol', 'hterm.Size', 'hterm.TextAttributes');
9
rginda8ba33642011-12-14 12:31:31 -080010/**
11 * @fileoverview This class represents a single terminal screen full of text.
12 *
13 * It maintains the current cursor position and has basic methods for text
14 * insert and overwrite, and adding or removing rows from the screen.
15 *
16 * This class has no knowledge of the scrollback buffer.
17 *
18 * The number of rows on the screen is determined only by the number of rows
19 * that the caller inserts into the screen. If a caller wants to ensure a
20 * constant number of rows on the screen, it's their responsibility to remove a
21 * row for each row inserted.
22 *
23 * The screen width, in contrast, is enforced locally.
24 *
25 *
26 * In practice...
27 * - The hterm.Terminal class holds two hterm.Screen instances. One for the
28 * primary screen and one for the alternate screen.
29 *
30 * - The html.Screen class only cares that rows are HTMLElements. In the
31 * larger context of hterm, however, the rows happen to be displayed by an
32 * hterm.ScrollPort and have to follow a few rules as a result. Each
33 * row must be rooted by the custom HTML tag 'x-row', and each must have a
34 * rowIndex property that corresponds to the index of the row in the context
35 * of the scrollback buffer. These invariants are enforced by hterm.Terminal
36 * because that is the class using the hterm.Screen in the context of an
37 * hterm.ScrollPort.
38 */
39
40/**
41 * Create a new screen instance.
42 *
43 * The screen initially has no rows and a maximum column count of 0.
44 *
45 * @param {integer} opt_columnCount The maximum number of columns for this
46 * screen. See insertString() and overwriteString() for information about
47 * what happens when too many characters are added too a row. Defaults to
48 * 0 if not provided.
49 */
50hterm.Screen = function(opt_columnCount) {
51 /**
52 * Public, read-only access to the rows in this screen.
53 */
54 this.rowsArray = [];
55
56 // The max column width for this screen.
rginda87b86462011-12-14 13:48:03 -080057 this.columnCount_ = opt_columnCount || 80;
rginda8ba33642011-12-14 12:31:31 -080058
rgindaa19afe22012-01-25 15:40:22 -080059 // The current color, bold, underline and blink attributes.
60 this.textAttributes = new hterm.TextAttributes(window.document);
61
rginda87b86462011-12-14 13:48:03 -080062 // Current zero-based cursor coordinates.
63 this.cursorPosition = new hterm.RowCol(0, 0);
rginda8ba33642011-12-14 12:31:31 -080064
65 // The node containing the row that the cursor is positioned on.
66 this.cursorRowNode_ = null;
67
68 // The node containing the span of text that the cursor is positioned on.
69 this.cursorNode_ = null;
70
Ricky Liang48f05cb2013-12-31 23:35:29 +080071 // The offset in column width into cursorNode_ where the cursor is positioned.
rginda8ba33642011-12-14 12:31:31 -080072 this.cursorOffset_ = null;
73};
74
75/**
76 * Return the screen size as an hterm.Size object.
77 *
78 * @return {hterm.Size} hterm.Size object representing the current number
79 * of rows and columns in this screen.
80 */
81hterm.Screen.prototype.getSize = function() {
82 return new hterm.Size(this.columnCount_, this.rowsArray.length);
83};
84
85/**
86 * Return the current number of rows in this screen.
87 *
88 * @return {integer} The number of rows in this screen.
89 */
90hterm.Screen.prototype.getHeight = function() {
91 return this.rowsArray.length;
92};
93
94/**
95 * Return the current number of columns in this screen.
96 *
97 * @return {integer} The number of columns in this screen.
98 */
99hterm.Screen.prototype.getWidth = function() {
100 return this.columnCount_;
101};
102
103/**
104 * Set the maximum number of columns per row.
105 *
rginda8ba33642011-12-14 12:31:31 -0800106 * @param {integer} count The maximum number of columns per row.
107 */
108hterm.Screen.prototype.setColumnCount = function(count) {
rginda2312fff2012-01-05 16:20:52 -0800109 this.columnCount_ = count;
110
rgindacbbd7482012-06-13 15:06:16 -0700111 if (this.cursorPosition.column >= count)
112 this.setCursorPosition(this.cursorPosition.row, count - 1);
rginda8ba33642011-12-14 12:31:31 -0800113};
114
115/**
116 * Remove the first row from the screen and return it.
117 *
118 * @return {HTMLElement} The first row in this screen.
119 */
120hterm.Screen.prototype.shiftRow = function() {
121 return this.shiftRows(1)[0];
rginda87b86462011-12-14 13:48:03 -0800122};
rginda8ba33642011-12-14 12:31:31 -0800123
124/**
125 * Remove rows from the top of the screen and return them as an array.
126 *
127 * @param {integer} count The number of rows to remove.
128 * @return {Array.<HTMLElement>} The selected rows.
129 */
130hterm.Screen.prototype.shiftRows = function(count) {
131 return this.rowsArray.splice(0, count);
132};
133
134/**
135 * Insert a row at the top of the screen.
136 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500137 * @param {HTMLElement} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800138 */
139hterm.Screen.prototype.unshiftRow = function(row) {
140 this.rowsArray.splice(0, 0, row);
141};
142
143/**
144 * Insert rows at the top of the screen.
145 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500146 * @param {Array.<HTMLElement>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800147 */
148hterm.Screen.prototype.unshiftRows = function(rows) {
149 this.rowsArray.unshift.apply(this.rowsArray, rows);
150};
151
152/**
153 * Remove the last row from the screen and return it.
154 *
155 * @return {HTMLElement} The last row in this screen.
156 */
157hterm.Screen.prototype.popRow = function() {
158 return this.popRows(1)[0];
159};
160
161/**
162 * Remove rows from the bottom of the screen and return them as an array.
163 *
164 * @param {integer} count The number of rows to remove.
165 * @return {Array.<HTMLElement>} The selected rows.
166 */
167hterm.Screen.prototype.popRows = function(count) {
168 return this.rowsArray.splice(this.rowsArray.length - count, count);
169};
170
171/**
172 * Insert a row at the bottom of the screen.
173 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500174 * @param {HTMLElement} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800175 */
176hterm.Screen.prototype.pushRow = function(row) {
177 this.rowsArray.push(row);
178};
179
180/**
181 * Insert rows at the bottom of the screen.
182 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500183 * @param {Array.<HTMLElement>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800184 */
185hterm.Screen.prototype.pushRows = function(rows) {
186 rows.push.apply(this.rowsArray, rows);
187};
188
189/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500190 * Insert a row at the specified row of the screen.
rginda8ba33642011-12-14 12:31:31 -0800191 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500192 * @param {integer} index The index to insert the row.
193 * @param {HTMLElement} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800194 */
195hterm.Screen.prototype.insertRow = function(index, row) {
196 this.rowsArray.splice(index, 0, row);
197};
198
199/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500200 * Insert rows at the specified row of the screen.
rginda8ba33642011-12-14 12:31:31 -0800201 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500202 * @param {integer} index The index to insert the rows.
203 * @param {Array.<HTMLElement>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800204 */
205hterm.Screen.prototype.insertRows = function(index, rows) {
206 for (var i = 0; i < rows.length; i++) {
207 this.rowsArray.splice(index + i, 0, rows[i]);
208 }
209};
210
211/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500212 * Remove a row from the screen and return it.
rginda8ba33642011-12-14 12:31:31 -0800213 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500214 * @param {integer} index The index of the row to remove.
rginda8ba33642011-12-14 12:31:31 -0800215 * @return {HTMLElement} The selected row.
216 */
217hterm.Screen.prototype.removeRow = function(index) {
218 return this.rowsArray.splice(index, 1)[0];
219};
220
221/**
222 * Remove rows from the bottom of the screen and return them as an array.
223 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500224 * @param {integer} index The index to start removing rows.
rginda8ba33642011-12-14 12:31:31 -0800225 * @param {integer} count The number of rows to remove.
226 * @return {Array.<HTMLElement>} The selected rows.
227 */
228hterm.Screen.prototype.removeRows = function(index, count) {
229 return this.rowsArray.splice(index, count);
230};
231
232/**
233 * Invalidate the current cursor position.
234 *
rginda87b86462011-12-14 13:48:03 -0800235 * This sets this.cursorPosition to (0, 0) and clears out some internal
rginda8ba33642011-12-14 12:31:31 -0800236 * data.
237 *
238 * Attempting to insert or overwrite text while the cursor position is invalid
239 * will raise an obscure exception.
240 */
241hterm.Screen.prototype.invalidateCursorPosition = function() {
rginda87b86462011-12-14 13:48:03 -0800242 this.cursorPosition.move(0, 0);
rginda8ba33642011-12-14 12:31:31 -0800243 this.cursorRowNode_ = null;
244 this.cursorNode_ = null;
245 this.cursorOffset_ = null;
246};
247
248/**
rginda8ba33642011-12-14 12:31:31 -0800249 * Clear the contents of the cursor row.
rginda8ba33642011-12-14 12:31:31 -0800250 */
251hterm.Screen.prototype.clearCursorRow = function() {
252 this.cursorRowNode_.innerHTML = '';
rgindaa09e7332012-08-17 12:49:51 -0700253 this.cursorRowNode_.removeAttribute('line-overflow');
rginda8ba33642011-12-14 12:31:31 -0800254 this.cursorOffset_ = 0;
rginda8ba33642011-12-14 12:31:31 -0800255 this.cursorPosition.column = 0;
rginda2312fff2012-01-05 16:20:52 -0800256 this.cursorPosition.overflow = false;
Robert Ginda7fd57082012-09-25 14:41:47 -0700257
258 var text;
259 if (this.textAttributes.isDefault()) {
260 text = '';
261 } else {
262 text = lib.f.getWhitespace(this.columnCount_);
263 }
264
Zhu Qunying30d40712017-03-14 16:27:00 -0700265 // We shouldn't honor inverse colors when clearing an area, to match
266 // xterm's back color erase behavior.
Edoardo Spadolini2fd43642014-08-23 22:59:57 +0200267 var inverse = this.textAttributes.inverse;
268 this.textAttributes.inverse = false;
269 this.textAttributes.syncColors();
270
Robert Ginda7fd57082012-09-25 14:41:47 -0700271 var node = this.textAttributes.createContainer(text);
272 this.cursorRowNode_.appendChild(node);
273 this.cursorNode_ = node;
Edoardo Spadolini2fd43642014-08-23 22:59:57 +0200274
275 this.textAttributes.inverse = inverse;
276 this.textAttributes.syncColors();
rginda8ba33642011-12-14 12:31:31 -0800277};
278
279/**
rgindaa09e7332012-08-17 12:49:51 -0700280 * Mark the current row as having overflowed to the next line.
281 *
282 * The line overflow state is used when converting a range of rows into text.
283 * It makes it possible to recombine two or more overflow terminal rows into
284 * a single line.
285 *
286 * This is distinct from the cursor being in the overflow state. Cursor
287 * overflow indicates that printing at the cursor position will commit a
288 * line overflow, unless it is preceded by a repositioning of the cursor
289 * to a non-overflow state.
290 */
291hterm.Screen.prototype.commitLineOverflow = function() {
292 this.cursorRowNode_.setAttribute('line-overflow', true);
293};
294
295/**
rginda8ba33642011-12-14 12:31:31 -0800296 * Relocate the cursor to a give row and column.
297 *
298 * @param {integer} row The zero based row.
299 * @param {integer} column The zero based column.
300 */
301hterm.Screen.prototype.setCursorPosition = function(row, column) {
rginda11057d52012-04-25 12:29:56 -0700302 if (!this.rowsArray.length) {
303 console.warn('Attempt to set cursor position on empty screen.');
304 return;
305 }
306
rginda87b86462011-12-14 13:48:03 -0800307 if (row >= this.rowsArray.length) {
rgindacbbd7482012-06-13 15:06:16 -0700308 console.error('Row out of bounds: ' + row);
rginda87b86462011-12-14 13:48:03 -0800309 row = this.rowsArray.length - 1;
310 } else if (row < 0) {
rgindacbbd7482012-06-13 15:06:16 -0700311 console.error('Row out of bounds: ' + row);
rginda87b86462011-12-14 13:48:03 -0800312 row = 0;
313 }
314
315 if (column >= this.columnCount_) {
rgindacbbd7482012-06-13 15:06:16 -0700316 console.error('Column out of bounds: ' + column);
rginda87b86462011-12-14 13:48:03 -0800317 column = this.columnCount_ - 1;
318 } else if (column < 0) {
rgindacbbd7482012-06-13 15:06:16 -0700319 console.error('Column out of bounds: ' + column);
rginda87b86462011-12-14 13:48:03 -0800320 column = 0;
321 }
rginda8ba33642011-12-14 12:31:31 -0800322
rginda2312fff2012-01-05 16:20:52 -0800323 this.cursorPosition.overflow = false;
324
rginda8ba33642011-12-14 12:31:31 -0800325 var rowNode = this.rowsArray[row];
326 var node = rowNode.firstChild;
327
328 if (!node) {
329 node = rowNode.ownerDocument.createTextNode('');
330 rowNode.appendChild(node);
331 }
332
rgindaa19afe22012-01-25 15:40:22 -0800333 var currentColumn = 0;
334
rginda8ba33642011-12-14 12:31:31 -0800335 if (rowNode == this.cursorRowNode_) {
336 if (column >= this.cursorPosition.column - this.cursorOffset_) {
337 node = this.cursorNode_;
338 currentColumn = this.cursorPosition.column - this.cursorOffset_;
339 }
340 } else {
341 this.cursorRowNode_ = rowNode;
342 }
343
344 this.cursorPosition.move(row, column);
345
346 while (node) {
347 var offset = column - currentColumn;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800348 var width = hterm.TextAttributes.nodeWidth(node);
349 if (!node.nextSibling || width > offset) {
rginda8ba33642011-12-14 12:31:31 -0800350 this.cursorNode_ = node;
351 this.cursorOffset_ = offset;
352 return;
353 }
354
Ricky Liang48f05cb2013-12-31 23:35:29 +0800355 currentColumn += width;
rginda8ba33642011-12-14 12:31:31 -0800356 node = node.nextSibling;
357 }
358};
359
360/**
rginda87b86462011-12-14 13:48:03 -0800361 * Set the provided selection object to be a caret selection at the current
362 * cursor position.
363 */
364hterm.Screen.prototype.syncSelectionCaret = function(selection) {
Rob Spies06533ba2014-04-24 11:20:37 -0700365 try {
366 selection.collapse(this.cursorNode_, this.cursorOffset_);
367 } catch (firefoxIgnoredException) {
368 // FF can throw an exception if the range is off, rather than just not
369 // performing the collapse.
370 }
rginda87b86462011-12-14 13:48:03 -0800371};
372
373/**
rgindaa19afe22012-01-25 15:40:22 -0800374 * Split a single node into two nodes at the given offset.
rginda8ba33642011-12-14 12:31:31 -0800375 *
rgindaa19afe22012-01-25 15:40:22 -0800376 * For example:
377 * Given the DOM fragment '<div><span>Hello World</span></div>', call splitNode_
Zhu Qunying30d40712017-03-14 16:27:00 -0700378 * passing the span and an offset of 6. This would modify the fragment to
rgindaa19afe22012-01-25 15:40:22 -0800379 * become: '<div><span>Hello </span><span>World</span></div>'. If the span
380 * had any attributes they would have been copied to the new span as well.
381 *
382 * The to-be-split node must have a container, so that the new node can be
383 * placed next to it.
384 *
385 * @param {HTMLNode} node The node to split.
386 * @param {integer} offset The offset into the node where the split should
387 * occur.
rginda8ba33642011-12-14 12:31:31 -0800388 */
rgindaa19afe22012-01-25 15:40:22 -0800389hterm.Screen.prototype.splitNode_ = function(node, offset) {
rginda35c456b2012-02-09 17:29:05 -0800390 var afterNode = node.cloneNode(false);
rgindaa19afe22012-01-25 15:40:22 -0800391
rginda35c456b2012-02-09 17:29:05 -0800392 var textContent = node.textContent;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800393 node.textContent = hterm.TextAttributes.nodeSubstr(node, 0, offset);
394 afterNode.textContent = lib.wc.substr(textContent, offset);
rgindaa19afe22012-01-25 15:40:22 -0800395
Ricky Liang48f05cb2013-12-31 23:35:29 +0800396 if (afterNode.textContent)
397 node.parentNode.insertBefore(afterNode, node.nextSibling);
398 if (!node.textContent)
399 node.parentNode.removeChild(node);
rginda8ba33642011-12-14 12:31:31 -0800400};
401
402/**
rgindaa9abdd82012-08-06 18:05:09 -0700403 * Ensure that text is clipped and the cursor is clamped to the column count.
rgindaa19afe22012-01-25 15:40:22 -0800404 */
rgindaa9abdd82012-08-06 18:05:09 -0700405hterm.Screen.prototype.maybeClipCurrentRow = function() {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800406 var width = hterm.TextAttributes.nodeWidth(this.cursorRowNode_);
407
408 if (width <= this.columnCount_) {
rgindaa9abdd82012-08-06 18:05:09 -0700409 // Current row does not need clipping, but may need clamping.
410 if (this.cursorPosition.column >= this.columnCount_) {
411 this.setCursorPosition(this.cursorPosition.row, this.columnCount_ - 1);
412 this.cursorPosition.overflow = true;
413 }
rgindaa19afe22012-01-25 15:40:22 -0800414
rgindaa9abdd82012-08-06 18:05:09 -0700415 return;
416 }
417
418 // Save off the current column so we can maybe restore it later.
419 var currentColumn = this.cursorPosition.column;
420
421 // Move the cursor to the final column.
422 this.setCursorPosition(this.cursorPosition.row, this.columnCount_ - 1);
423
424 // Remove any text that partially overflows.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800425 width = hterm.TextAttributes.nodeWidth(this.cursorNode_);
426
427 if (this.cursorOffset_ < width - 1) {
428 this.cursorNode_.textContent = hterm.TextAttributes.nodeSubstr(
429 this.cursorNode_, 0, this.cursorOffset_ + 1);
rgindaa9abdd82012-08-06 18:05:09 -0700430 }
431
432 // Remove all nodes after the cursor.
rgindaa19afe22012-01-25 15:40:22 -0800433 var rowNode = this.cursorRowNode_;
434 var node = this.cursorNode_.nextSibling;
435
436 while (node) {
rgindaa19afe22012-01-25 15:40:22 -0800437 rowNode.removeChild(node);
438 node = this.cursorNode_.nextSibling;
439 }
440
Robert Ginda7fd57082012-09-25 14:41:47 -0700441 if (currentColumn < this.columnCount_) {
rgindaa9abdd82012-08-06 18:05:09 -0700442 // If the cursor was within the screen before we started then restore its
443 // position.
rgindaa19afe22012-01-25 15:40:22 -0800444 this.setCursorPosition(this.cursorPosition.row, currentColumn);
rgindaa9abdd82012-08-06 18:05:09 -0700445 } else {
446 // Otherwise leave it at the the last column in the overflow state.
447 this.cursorPosition.overflow = true;
rgindaa19afe22012-01-25 15:40:22 -0800448 }
rgindaa19afe22012-01-25 15:40:22 -0800449};
450
451/**
452 * Insert a string at the current character position using the current
453 * text attributes.
454 *
rgindaa09e7332012-08-17 12:49:51 -0700455 * You must call maybeClipCurrentRow() after in order to clip overflowed
456 * text and clamp the cursor.
457 *
458 * It is also up to the caller to properly maintain the line overflow state
459 * using hterm.Screen..commitLineOverflow().
rginda8ba33642011-12-14 12:31:31 -0800460 */
461hterm.Screen.prototype.insertString = function(str) {
rgindaa19afe22012-01-25 15:40:22 -0800462 var cursorNode = this.cursorNode_;
463 var cursorNodeText = cursorNode.textContent;
rginda8ba33642011-12-14 12:31:31 -0800464
Robert Gindaa21dfb32013-10-31 14:17:45 -0700465 this.cursorRowNode_.removeAttribute('line-overflow');
466
Ricky Liang48f05cb2013-12-31 23:35:29 +0800467 // We may alter the width of the string by prepending some missing
468 // whitespaces, so we need to record the string width ahead of time.
469 var strWidth = lib.wc.strWidth(str);
rginda8ba33642011-12-14 12:31:31 -0800470
rgindaa19afe22012-01-25 15:40:22 -0800471 // No matter what, before this function exits the cursor column will have
472 // moved this much.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800473 this.cursorPosition.column += strWidth;
rginda8ba33642011-12-14 12:31:31 -0800474
rgindaa19afe22012-01-25 15:40:22 -0800475 // Local cache of the cursor offset.
476 var offset = this.cursorOffset_;
rginda8ba33642011-12-14 12:31:31 -0800477
rgindaa19afe22012-01-25 15:40:22 -0800478 // Reverse offset is the offset measured from the end of the string.
479 // Zero implies that the cursor is at the end of the cursor node.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800480 var reverseOffset = hterm.TextAttributes.nodeWidth(cursorNode) - offset;
rgindaa19afe22012-01-25 15:40:22 -0800481
482 if (reverseOffset < 0) {
483 // A negative reverse offset means the cursor is positioned past the end
484 // of the characters on this line. We'll need to insert the missing
485 // whitespace.
rgindacbbd7482012-06-13 15:06:16 -0700486 var ws = lib.f.getWhitespace(-reverseOffset);
rgindaa19afe22012-01-25 15:40:22 -0800487
Brad Town7de83302015-03-12 02:10:32 -0700488 // This whitespace should be completely unstyled. Underline, background
489 // color, and strikethrough would be visible on whitespace, so we can't use
490 // one of those spans to hold the text.
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200491 if (!(this.textAttributes.underline ||
Brad Town7de83302015-03-12 02:10:32 -0700492 this.textAttributes.strikethrough ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200493 this.textAttributes.background ||
494 this.textAttributes.wcNode ||
495 this.textAttributes.tileData != null)) {
rgindaa19afe22012-01-25 15:40:22 -0800496 // Best case scenario, we can just pretend the spaces were part of the
497 // original string.
498 str = ws + str;
499 } else if (cursorNode.nodeType == 3 ||
Ricky Liang48f05cb2013-12-31 23:35:29 +0800500 !(cursorNode.wcNode ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200501 cursorNode.tileNode ||
Ricky Liang48f05cb2013-12-31 23:35:29 +0800502 cursorNode.style.textDecoration ||
rgindaa19afe22012-01-25 15:40:22 -0800503 cursorNode.style.backgroundColor)) {
504 // Second best case, the current node is able to hold the whitespace.
505 cursorNode.textContent = (cursorNodeText += ws);
506 } else {
507 // Worst case, we have to create a new node to hold the whitespace.
508 var wsNode = cursorNode.ownerDocument.createTextNode(ws);
509 this.cursorRowNode_.insertBefore(wsNode, cursorNode.nextSibling);
510 this.cursorNode_ = cursorNode = wsNode;
511 this.cursorOffset_ = offset = -reverseOffset;
512 cursorNodeText = ws;
513 }
514
515 // We now know for sure that we're at the last character of the cursor node.
516 reverseOffset = 0;
rginda8ba33642011-12-14 12:31:31 -0800517 }
518
rgindaa19afe22012-01-25 15:40:22 -0800519 if (this.textAttributes.matchesContainer(cursorNode)) {
520 // The new text can be placed directly in the cursor node.
521 if (reverseOffset == 0) {
522 cursorNode.textContent = cursorNodeText + str;
523 } else if (offset == 0) {
524 cursorNode.textContent = str + cursorNodeText;
525 } else {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800526 cursorNode.textContent =
527 hterm.TextAttributes.nodeSubstr(cursorNode, 0, offset) +
528 str + hterm.TextAttributes.nodeSubstr(cursorNode, offset);
rgindaa19afe22012-01-25 15:40:22 -0800529 }
rginda8ba33642011-12-14 12:31:31 -0800530
Ricky Liang48f05cb2013-12-31 23:35:29 +0800531 this.cursorOffset_ += strWidth;
rgindaa19afe22012-01-25 15:40:22 -0800532 return;
rginda87b86462011-12-14 13:48:03 -0800533 }
534
rgindaa19afe22012-01-25 15:40:22 -0800535 // The cursor node is the wrong style for the new text. If we're at the
536 // beginning or end of the cursor node, then the adjacent node is also a
537 // potential candidate.
rginda8ba33642011-12-14 12:31:31 -0800538
rgindaa19afe22012-01-25 15:40:22 -0800539 if (offset == 0) {
540 // At the beginning of the cursor node, the check the previous sibling.
541 var previousSibling = cursorNode.previousSibling;
542 if (previousSibling &&
543 this.textAttributes.matchesContainer(previousSibling)) {
544 previousSibling.textContent += str;
545 this.cursorNode_ = previousSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800546 this.cursorOffset_ = lib.wc.strWidth(previousSibling.textContent);
rgindaa19afe22012-01-25 15:40:22 -0800547 return;
548 }
549
550 var newNode = this.textAttributes.createContainer(str);
551 this.cursorRowNode_.insertBefore(newNode, cursorNode);
552 this.cursorNode_ = newNode;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800553 this.cursorOffset_ = strWidth;
rgindaa19afe22012-01-25 15:40:22 -0800554 return;
555 }
556
557 if (reverseOffset == 0) {
558 // At the end of the cursor node, the check the next sibling.
559 var nextSibling = cursorNode.nextSibling;
560 if (nextSibling &&
561 this.textAttributes.matchesContainer(nextSibling)) {
562 nextSibling.textContent = str + nextSibling.textContent;
563 this.cursorNode_ = nextSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800564 this.cursorOffset_ = lib.wc.strWidth(str);
rgindaa19afe22012-01-25 15:40:22 -0800565 return;
566 }
567
568 var newNode = this.textAttributes.createContainer(str);
569 this.cursorRowNode_.insertBefore(newNode, nextSibling);
570 this.cursorNode_ = newNode;
571 // We specifically need to include any missing whitespace here, since it's
572 // going in a new node.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800573 this.cursorOffset_ = hterm.TextAttributes.nodeWidth(newNode);
rgindaa19afe22012-01-25 15:40:22 -0800574 return;
575 }
576
577 // Worst case, we're somewhere in the middle of the cursor node. We'll
578 // have to split it into two nodes and insert our new container in between.
579 this.splitNode_(cursorNode, offset);
580 var newNode = this.textAttributes.createContainer(str);
581 this.cursorRowNode_.insertBefore(newNode, cursorNode.nextSibling);
582 this.cursorNode_ = newNode;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800583 this.cursorOffset_ = strWidth;
rgindaa19afe22012-01-25 15:40:22 -0800584};
585
586/**
rginda8ba33642011-12-14 12:31:31 -0800587 * Overwrite the text at the current cursor position.
588 *
rgindaa09e7332012-08-17 12:49:51 -0700589 * You must call maybeClipCurrentRow() after in order to clip overflowed
590 * text and clamp the cursor.
591 *
592 * It is also up to the caller to properly maintain the line overflow state
593 * using hterm.Screen..commitLineOverflow().
rginda8ba33642011-12-14 12:31:31 -0800594 */
595hterm.Screen.prototype.overwriteString = function(str) {
596 var maxLength = this.columnCount_ - this.cursorPosition.column;
597 if (!maxLength)
rgindaa19afe22012-01-25 15:40:22 -0800598 return [str];
599
Ricky Liang48f05cb2013-12-31 23:35:29 +0800600 var width = lib.wc.strWidth(str);
601 if (this.textAttributes.matchesContainer(this.cursorNode_) &&
602 this.cursorNode_.textContent.substr(this.cursorOffset_) == str) {
rgindaa19afe22012-01-25 15:40:22 -0800603 // This overwrite would be a no-op, just move the cursor and return.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800604 this.cursorOffset_ += width;
605 this.cursorPosition.column += width;
rgindaa19afe22012-01-25 15:40:22 -0800606 return;
607 }
rginda8ba33642011-12-14 12:31:31 -0800608
Ricky Liang48f05cb2013-12-31 23:35:29 +0800609 this.deleteChars(Math.min(width, maxLength));
rgindaa19afe22012-01-25 15:40:22 -0800610 this.insertString(str);
rginda8ba33642011-12-14 12:31:31 -0800611};
612
613/**
614 * Forward-delete one or more characters at the current cursor position.
615 *
616 * Text to the right of the deleted characters is shifted left. Only affects
617 * characters on the same row as the cursor.
618 *
Ricky Liang48f05cb2013-12-31 23:35:29 +0800619 * @param {integer} count The column width of characters to delete. This is
620 * clamped to the column width minus the cursor column.
621 * @return {integer} The column width of the characters actually deleted.
rginda8ba33642011-12-14 12:31:31 -0800622 */
623hterm.Screen.prototype.deleteChars = function(count) {
624 var node = this.cursorNode_;
625 var offset = this.cursorOffset_;
626
Robert Ginda7fd57082012-09-25 14:41:47 -0700627 var currentCursorColumn = this.cursorPosition.column;
628 count = Math.min(count, this.columnCount_ - currentCursorColumn);
629 if (!count)
630 return 0;
631
632 var rv = count;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800633 var startLength, endLength;
rgindaa19afe22012-01-25 15:40:22 -0800634
rginda8ba33642011-12-14 12:31:31 -0800635 while (node && count) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800636 startLength = hterm.TextAttributes.nodeWidth(node);
637 node.textContent = hterm.TextAttributes.nodeSubstr(node, 0, offset) +
638 hterm.TextAttributes.nodeSubstr(node, offset + count);
639 endLength = hterm.TextAttributes.nodeWidth(node);
rginda8ba33642011-12-14 12:31:31 -0800640 count -= startLength - endLength;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800641 if (offset < startLength && endLength && startLength == endLength) {
642 // No characters were deleted when there should be. We're probably trying
643 // to delete one column width from a wide character node. We remove the
644 // wide character node here and replace it with a single space.
645 var spaceNode = this.textAttributes.createContainer(' ');
646 node.parentNode.insertBefore(spaceNode, node.nextSibling);
647 node.textContent = '';
648 endLength = 0;
649 count -= 1;
rginda8ba33642011-12-14 12:31:31 -0800650 }
651
Ricky Liang48f05cb2013-12-31 23:35:29 +0800652 var nextNode = node.nextSibling;
653 if (endLength == 0 && node != this.cursorNode_) {
654 node.parentNode.removeChild(node);
655 }
656 node = nextNode;
rginda8ba33642011-12-14 12:31:31 -0800657 offset = 0;
658 }
Robert Ginda7fd57082012-09-25 14:41:47 -0700659
Ricky Liang48f05cb2013-12-31 23:35:29 +0800660 // Remove this.cursorNode_ if it is an empty non-text node.
661 if (this.cursorNode_.nodeType != 3 && !this.cursorNode_.textContent) {
662 var cursorNode = this.cursorNode_;
663 if (cursorNode.previousSibling) {
664 this.cursorNode_ = cursorNode.previousSibling;
665 this.cursorOffset_ = hterm.TextAttributes.nodeWidth(
666 cursorNode.previousSibling);
667 } else if (cursorNode.nextSibling) {
668 this.cursorNode_ = cursorNode.nextSibling;
669 this.cursorOffset_ = 0;
670 } else {
671 var emptyNode = this.cursorRowNode_.ownerDocument.createTextNode('');
672 this.cursorRowNode_.appendChild(emptyNode);
673 this.cursorNode_ = emptyNode;
674 this.cursorOffset_ = 0;
675 }
676 this.cursorRowNode_.removeChild(cursorNode);
677 }
678
Robert Ginda7fd57082012-09-25 14:41:47 -0700679 return rv;
rginda8ba33642011-12-14 12:31:31 -0800680};
John Macinnesfb683832013-07-22 14:46:30 -0400681
682/**
683 * Finds first X-ROW of a line containing specified X-ROW.
684 * Used to support line overflow.
685 *
686 * @param {Node} row X-ROW to begin search for first row of line.
687 * @return {Node} The X-ROW that is at the beginning of the line.
688 **/
689hterm.Screen.prototype.getLineStartRow_ = function(row) {
690 while (row.previousSibling &&
691 row.previousSibling.hasAttribute('line-overflow')) {
692 row = row.previousSibling;
693 }
694 return row;
695};
696
697/**
698 * Gets text of a line beginning with row.
699 * Supports line overflow.
700 *
701 * @param {Node} row First X-ROW of line.
702 * @return {string} Text content of line.
703 **/
704hterm.Screen.prototype.getLineText_ = function(row) {
705 var rowText = "";
706 while (row) {
707 rowText += row.textContent;
708 if (row.hasAttribute('line-overflow')) {
709 row = row.nextSibling;
710 } else {
711 break;
712 }
713 }
714 return rowText;
715};
716
717/**
718 * Returns X-ROW that is ancestor of the node.
719 *
720 * @param {Node} node Node to get X-ROW ancestor for.
721 * @return {Node} X-ROW ancestor of node, or null if not found.
722 **/
723hterm.Screen.prototype.getXRowAncestor_ = function(node) {
724 while (node) {
725 if (node.nodeName === 'X-ROW')
726 break;
727 node = node.parentNode;
728 }
729 return node;
730};
731
732/**
733 * Returns position within line of character at offset within node.
734 * Supports line overflow.
735 *
736 * @param {Node} row X-ROW at beginning of line.
737 * @param {Node} node Node to get position of.
738 * @param {integer} offset Offset into node.
739 *
740 * @return {integer} Position within line of character at offset within node.
741 **/
742hterm.Screen.prototype.getPositionWithOverflow_ = function(row, node, offset) {
743 if (!node)
744 return -1;
745 var ancestorRow = this.getXRowAncestor_(node);
746 if (!ancestorRow)
747 return -1;
748 var position = 0;
749 while (ancestorRow != row) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800750 position += hterm.TextAttributes.nodeWidth(row);
John Macinnesfb683832013-07-22 14:46:30 -0400751 if (row.hasAttribute('line-overflow') && row.nextSibling) {
752 row = row.nextSibling;
753 } else {
754 return -1;
755 }
756 }
757 return position + this.getPositionWithinRow_(row, node, offset);
758};
759
760/**
761 * Returns position within row of character at offset within node.
762 * Does not support line overflow.
763 *
764 * @param {Node} row X-ROW to get position within.
765 * @param {Node} node Node to get position for.
766 * @param {integer} offset Offset within node to get position for.
767 * @return {integer} Position within row of character at offset within node.
768 **/
769hterm.Screen.prototype.getPositionWithinRow_ = function(row, node, offset) {
770 if (node.parentNode != row) {
771 return this.getPositionWithinRow_(node.parentNode, node, offset) +
772 this.getPositionWithinRow_(row, node.parentNode, 0);
773 }
774 var position = 0;
775 for (var i = 0; i < row.childNodes.length; i++) {
776 var currentNode = row.childNodes[i];
777 if (currentNode == node)
778 return position + offset;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800779 position += hterm.TextAttributes.nodeWidth(currentNode);
John Macinnesfb683832013-07-22 14:46:30 -0400780 }
781 return -1;
782};
783
784/**
785 * Returns the node and offset corresponding to position within line.
786 * Supports line overflow.
787 *
788 * @param {Node} row X-ROW at beginning of line.
789 * @param {integer} position Position within line to retrieve node and offset.
790 * @return {Array} Two element array containing node and offset respectively.
791 **/
792hterm.Screen.prototype.getNodeAndOffsetWithOverflow_ = function(row, position) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800793 while (row && position > hterm.TextAttributes.nodeWidth(row)) {
John Macinnesfb683832013-07-22 14:46:30 -0400794 if (row.hasAttribute('line-overflow') && row.nextSibling) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800795 position -= hterm.TextAttributes.nodeWidth(row);
John Macinnesfb683832013-07-22 14:46:30 -0400796 row = row.nextSibling;
797 } else {
798 return -1;
799 }
800 }
801 return this.getNodeAndOffsetWithinRow_(row, position);
802};
803
804/**
805 * Returns the node and offset corresponding to position within row.
806 * Does not support line overflow.
807 *
808 * @param {Node} row X-ROW to get position within.
809 * @param {integer} position Position within row to retrieve node and offset.
810 * @return {Array} Two element array containing node and offset respectively.
811 **/
812hterm.Screen.prototype.getNodeAndOffsetWithinRow_ = function(row, position) {
813 for (var i = 0; i < row.childNodes.length; i++) {
814 var node = row.childNodes[i];
Ricky Liang48f05cb2013-12-31 23:35:29 +0800815 var nodeTextWidth = hterm.TextAttributes.nodeWidth(node);
816 if (position <= nodeTextWidth) {
John Macinnesfb683832013-07-22 14:46:30 -0400817 if (node.nodeName === 'SPAN') {
818 /** Drill down to node contained by SPAN. **/
819 return this.getNodeAndOffsetWithinRow_(node, position);
820 } else {
821 return [node, position];
822 }
823 }
Ricky Liang48f05cb2013-12-31 23:35:29 +0800824 position -= nodeTextWidth;
John Macinnesfb683832013-07-22 14:46:30 -0400825 }
826 return null;
827};
828
829/**
830 * Returns the node and offset corresponding to position within line.
831 * Supports line overflow.
832 *
833 * @param {Node} row X-ROW at beginning of line.
834 * @param {integer} start Start position of range within line.
835 * @param {integer} end End position of range within line.
836 * @param {Range} range Range to modify.
837 **/
838hterm.Screen.prototype.setRange_ = function(row, start, end, range) {
839 var startNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, start);
840 if (startNodeAndOffset == null)
841 return;
842 var endNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, end);
843 if (endNodeAndOffset == null)
844 return;
845 range.setStart(startNodeAndOffset[0], startNodeAndOffset[1]);
846 range.setEnd(endNodeAndOffset[0], endNodeAndOffset[1]);
847};
848
849/**
850 * Expands selection to surround URLs.
851 *
John Macinnesfb683832013-07-22 14:46:30 -0400852 * @param {Selection} selection Selection to expand.
853 **/
854hterm.Screen.prototype.expandSelection = function(selection) {
855 if (!selection)
856 return;
857
858 var range = selection.getRangeAt(0);
859 if (!range || range.toString().match(/\s/))
860 return;
861
862 var row = this.getLineStartRow_(this.getXRowAncestor_(range.startContainer));
863 if (!row)
864 return;
865
866 var startPosition = this.getPositionWithOverflow_(row,
867 range.startContainer,
868 range.startOffset);
869 if (startPosition == -1)
870 return;
871 var endPosition = this.getPositionWithOverflow_(row,
872 range.endContainer,
873 range.endOffset);
874 if (endPosition == -1)
875 return;
876
Robert Ginda5eba4562014-08-11 11:05:54 -0700877 // Matches can start with '~' or '.', since paths frequently do.
878 var leftMatch = '[^\\s\\[\\](){}<>"\'\\^!@#$%&*,;:`]';
879 var rightMatch = '[^\\s\\[\\](){}<>"\'\\^!@#$%&*,;:~.`]';
John Macinnesfb683832013-07-22 14:46:30 -0400880 var insideMatch = '[^\\s\\[\\](){}<>"\'\\^]*';
881
882 //Move start to the left.
883 var rowText = this.getLineText_(row);
Ricky Liang48f05cb2013-12-31 23:35:29 +0800884 var lineUpToRange = lib.wc.substring(rowText, 0, endPosition);
Robert Ginda5eba4562014-08-11 11:05:54 -0700885 var leftRegularExpression = new RegExp(leftMatch + insideMatch + "$");
John Macinnesfb683832013-07-22 14:46:30 -0400886 var expandedStart = lineUpToRange.search(leftRegularExpression);
887 if (expandedStart == -1 || expandedStart > startPosition)
888 return;
889
890 //Move end to the right.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800891 var lineFromRange = lib.wc.substring(rowText, startPosition,
892 lib.wc.strWidth(rowText));
Robert Ginda5eba4562014-08-11 11:05:54 -0700893 var rightRegularExpression = new RegExp("^" + insideMatch + rightMatch);
John Macinnesfb683832013-07-22 14:46:30 -0400894 var found = lineFromRange.match(rightRegularExpression);
895 if (!found)
896 return;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800897 var expandedEnd = startPosition + lib.wc.strWidth(found[0]);
John Macinnesfb683832013-07-22 14:46:30 -0400898 if (expandedEnd == -1 || expandedEnd < endPosition)
899 return;
900
901 this.setRange_(row, expandedStart, expandedEnd, range);
902 selection.addRange(range);
903};