blob: 591eb7fb9c44eae9416fbf52e35b7e3ccb2b64a2 [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;
Mike Frysinger664e9992017-05-19 01:24:24 -040073
74 // Regexes for expanding word selections.
75 this.wordBreakMatchLeft = null;
76 this.wordBreakMatchRight = null;
77 this.wordBreakMatchMiddle = null;
rginda8ba33642011-12-14 12:31:31 -080078};
79
80/**
81 * Return the screen size as an hterm.Size object.
82 *
83 * @return {hterm.Size} hterm.Size object representing the current number
84 * of rows and columns in this screen.
85 */
86hterm.Screen.prototype.getSize = function() {
87 return new hterm.Size(this.columnCount_, this.rowsArray.length);
88};
89
90/**
91 * Return the current number of rows in this screen.
92 *
93 * @return {integer} The number of rows in this screen.
94 */
95hterm.Screen.prototype.getHeight = function() {
96 return this.rowsArray.length;
97};
98
99/**
100 * Return the current number of columns in this screen.
101 *
102 * @return {integer} The number of columns in this screen.
103 */
104hterm.Screen.prototype.getWidth = function() {
105 return this.columnCount_;
106};
107
108/**
109 * Set the maximum number of columns per row.
110 *
rginda8ba33642011-12-14 12:31:31 -0800111 * @param {integer} count The maximum number of columns per row.
112 */
113hterm.Screen.prototype.setColumnCount = function(count) {
rginda2312fff2012-01-05 16:20:52 -0800114 this.columnCount_ = count;
115
rgindacbbd7482012-06-13 15:06:16 -0700116 if (this.cursorPosition.column >= count)
117 this.setCursorPosition(this.cursorPosition.row, count - 1);
rginda8ba33642011-12-14 12:31:31 -0800118};
119
120/**
121 * Remove the first row from the screen and return it.
122 *
123 * @return {HTMLElement} The first row in this screen.
124 */
125hterm.Screen.prototype.shiftRow = function() {
126 return this.shiftRows(1)[0];
rginda87b86462011-12-14 13:48:03 -0800127};
rginda8ba33642011-12-14 12:31:31 -0800128
129/**
130 * Remove rows from the top of the screen and return them as an array.
131 *
132 * @param {integer} count The number of rows to remove.
133 * @return {Array.<HTMLElement>} The selected rows.
134 */
135hterm.Screen.prototype.shiftRows = function(count) {
136 return this.rowsArray.splice(0, count);
137};
138
139/**
140 * Insert a row at the top of the screen.
141 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500142 * @param {HTMLElement} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800143 */
144hterm.Screen.prototype.unshiftRow = function(row) {
145 this.rowsArray.splice(0, 0, row);
146};
147
148/**
149 * Insert rows at the top of the screen.
150 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500151 * @param {Array.<HTMLElement>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800152 */
153hterm.Screen.prototype.unshiftRows = function(rows) {
154 this.rowsArray.unshift.apply(this.rowsArray, rows);
155};
156
157/**
158 * Remove the last row from the screen and return it.
159 *
160 * @return {HTMLElement} The last row in this screen.
161 */
162hterm.Screen.prototype.popRow = function() {
163 return this.popRows(1)[0];
164};
165
166/**
167 * Remove rows from the bottom of the screen and return them as an array.
168 *
169 * @param {integer} count The number of rows to remove.
170 * @return {Array.<HTMLElement>} The selected rows.
171 */
172hterm.Screen.prototype.popRows = function(count) {
173 return this.rowsArray.splice(this.rowsArray.length - count, count);
174};
175
176/**
177 * Insert a row at the bottom of the screen.
178 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500179 * @param {HTMLElement} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800180 */
181hterm.Screen.prototype.pushRow = function(row) {
182 this.rowsArray.push(row);
183};
184
185/**
186 * Insert rows at the bottom of the screen.
187 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500188 * @param {Array.<HTMLElement>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800189 */
190hterm.Screen.prototype.pushRows = function(rows) {
191 rows.push.apply(this.rowsArray, rows);
192};
193
194/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500195 * Insert a row at the specified row of the screen.
rginda8ba33642011-12-14 12:31:31 -0800196 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500197 * @param {integer} index The index to insert the row.
198 * @param {HTMLElement} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800199 */
200hterm.Screen.prototype.insertRow = function(index, row) {
201 this.rowsArray.splice(index, 0, row);
202};
203
204/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500205 * Insert rows at the specified row of the screen.
rginda8ba33642011-12-14 12:31:31 -0800206 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500207 * @param {integer} index The index to insert the rows.
208 * @param {Array.<HTMLElement>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800209 */
210hterm.Screen.prototype.insertRows = function(index, rows) {
211 for (var i = 0; i < rows.length; i++) {
212 this.rowsArray.splice(index + i, 0, rows[i]);
213 }
214};
215
216/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500217 * Remove a row from the screen and return it.
rginda8ba33642011-12-14 12:31:31 -0800218 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500219 * @param {integer} index The index of the row to remove.
rginda8ba33642011-12-14 12:31:31 -0800220 * @return {HTMLElement} The selected row.
221 */
222hterm.Screen.prototype.removeRow = function(index) {
223 return this.rowsArray.splice(index, 1)[0];
224};
225
226/**
227 * Remove rows from the bottom of the screen and return them as an array.
228 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500229 * @param {integer} index The index to start removing rows.
rginda8ba33642011-12-14 12:31:31 -0800230 * @param {integer} count The number of rows to remove.
231 * @return {Array.<HTMLElement>} The selected rows.
232 */
233hterm.Screen.prototype.removeRows = function(index, count) {
234 return this.rowsArray.splice(index, count);
235};
236
237/**
238 * Invalidate the current cursor position.
239 *
rginda87b86462011-12-14 13:48:03 -0800240 * This sets this.cursorPosition to (0, 0) and clears out some internal
rginda8ba33642011-12-14 12:31:31 -0800241 * data.
242 *
243 * Attempting to insert or overwrite text while the cursor position is invalid
244 * will raise an obscure exception.
245 */
246hterm.Screen.prototype.invalidateCursorPosition = function() {
rginda87b86462011-12-14 13:48:03 -0800247 this.cursorPosition.move(0, 0);
rginda8ba33642011-12-14 12:31:31 -0800248 this.cursorRowNode_ = null;
249 this.cursorNode_ = null;
250 this.cursorOffset_ = null;
251};
252
253/**
rginda8ba33642011-12-14 12:31:31 -0800254 * Clear the contents of the cursor row.
rginda8ba33642011-12-14 12:31:31 -0800255 */
256hterm.Screen.prototype.clearCursorRow = function() {
257 this.cursorRowNode_.innerHTML = '';
rgindaa09e7332012-08-17 12:49:51 -0700258 this.cursorRowNode_.removeAttribute('line-overflow');
rginda8ba33642011-12-14 12:31:31 -0800259 this.cursorOffset_ = 0;
rginda8ba33642011-12-14 12:31:31 -0800260 this.cursorPosition.column = 0;
rginda2312fff2012-01-05 16:20:52 -0800261 this.cursorPosition.overflow = false;
Robert Ginda7fd57082012-09-25 14:41:47 -0700262
263 var text;
264 if (this.textAttributes.isDefault()) {
265 text = '';
266 } else {
267 text = lib.f.getWhitespace(this.columnCount_);
268 }
269
Zhu Qunying30d40712017-03-14 16:27:00 -0700270 // We shouldn't honor inverse colors when clearing an area, to match
271 // xterm's back color erase behavior.
Edoardo Spadolini2fd43642014-08-23 22:59:57 +0200272 var inverse = this.textAttributes.inverse;
273 this.textAttributes.inverse = false;
274 this.textAttributes.syncColors();
275
Robert Ginda7fd57082012-09-25 14:41:47 -0700276 var node = this.textAttributes.createContainer(text);
277 this.cursorRowNode_.appendChild(node);
278 this.cursorNode_ = node;
Edoardo Spadolini2fd43642014-08-23 22:59:57 +0200279
280 this.textAttributes.inverse = inverse;
281 this.textAttributes.syncColors();
rginda8ba33642011-12-14 12:31:31 -0800282};
283
284/**
rgindaa09e7332012-08-17 12:49:51 -0700285 * Mark the current row as having overflowed to the next line.
286 *
287 * The line overflow state is used when converting a range of rows into text.
288 * It makes it possible to recombine two or more overflow terminal rows into
289 * a single line.
290 *
291 * This is distinct from the cursor being in the overflow state. Cursor
292 * overflow indicates that printing at the cursor position will commit a
293 * line overflow, unless it is preceded by a repositioning of the cursor
294 * to a non-overflow state.
295 */
296hterm.Screen.prototype.commitLineOverflow = function() {
297 this.cursorRowNode_.setAttribute('line-overflow', true);
298};
299
300/**
rginda8ba33642011-12-14 12:31:31 -0800301 * Relocate the cursor to a give row and column.
302 *
303 * @param {integer} row The zero based row.
304 * @param {integer} column The zero based column.
305 */
306hterm.Screen.prototype.setCursorPosition = function(row, column) {
rginda11057d52012-04-25 12:29:56 -0700307 if (!this.rowsArray.length) {
308 console.warn('Attempt to set cursor position on empty screen.');
309 return;
310 }
311
rginda87b86462011-12-14 13:48:03 -0800312 if (row >= this.rowsArray.length) {
rgindacbbd7482012-06-13 15:06:16 -0700313 console.error('Row out of bounds: ' + row);
rginda87b86462011-12-14 13:48:03 -0800314 row = this.rowsArray.length - 1;
315 } else if (row < 0) {
rgindacbbd7482012-06-13 15:06:16 -0700316 console.error('Row out of bounds: ' + row);
rginda87b86462011-12-14 13:48:03 -0800317 row = 0;
318 }
319
320 if (column >= this.columnCount_) {
rgindacbbd7482012-06-13 15:06:16 -0700321 console.error('Column out of bounds: ' + column);
rginda87b86462011-12-14 13:48:03 -0800322 column = this.columnCount_ - 1;
323 } else if (column < 0) {
rgindacbbd7482012-06-13 15:06:16 -0700324 console.error('Column out of bounds: ' + column);
rginda87b86462011-12-14 13:48:03 -0800325 column = 0;
326 }
rginda8ba33642011-12-14 12:31:31 -0800327
rginda2312fff2012-01-05 16:20:52 -0800328 this.cursorPosition.overflow = false;
329
rginda8ba33642011-12-14 12:31:31 -0800330 var rowNode = this.rowsArray[row];
331 var node = rowNode.firstChild;
332
333 if (!node) {
334 node = rowNode.ownerDocument.createTextNode('');
335 rowNode.appendChild(node);
336 }
337
rgindaa19afe22012-01-25 15:40:22 -0800338 var currentColumn = 0;
339
rginda8ba33642011-12-14 12:31:31 -0800340 if (rowNode == this.cursorRowNode_) {
341 if (column >= this.cursorPosition.column - this.cursorOffset_) {
342 node = this.cursorNode_;
343 currentColumn = this.cursorPosition.column - this.cursorOffset_;
344 }
345 } else {
346 this.cursorRowNode_ = rowNode;
347 }
348
349 this.cursorPosition.move(row, column);
350
351 while (node) {
352 var offset = column - currentColumn;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800353 var width = hterm.TextAttributes.nodeWidth(node);
354 if (!node.nextSibling || width > offset) {
rginda8ba33642011-12-14 12:31:31 -0800355 this.cursorNode_ = node;
356 this.cursorOffset_ = offset;
357 return;
358 }
359
Ricky Liang48f05cb2013-12-31 23:35:29 +0800360 currentColumn += width;
rginda8ba33642011-12-14 12:31:31 -0800361 node = node.nextSibling;
362 }
363};
364
365/**
rginda87b86462011-12-14 13:48:03 -0800366 * Set the provided selection object to be a caret selection at the current
367 * cursor position.
368 */
369hterm.Screen.prototype.syncSelectionCaret = function(selection) {
Rob Spies06533ba2014-04-24 11:20:37 -0700370 try {
371 selection.collapse(this.cursorNode_, this.cursorOffset_);
372 } catch (firefoxIgnoredException) {
373 // FF can throw an exception if the range is off, rather than just not
374 // performing the collapse.
375 }
rginda87b86462011-12-14 13:48:03 -0800376};
377
378/**
rgindaa19afe22012-01-25 15:40:22 -0800379 * Split a single node into two nodes at the given offset.
rginda8ba33642011-12-14 12:31:31 -0800380 *
rgindaa19afe22012-01-25 15:40:22 -0800381 * For example:
382 * Given the DOM fragment '<div><span>Hello World</span></div>', call splitNode_
Zhu Qunying30d40712017-03-14 16:27:00 -0700383 * passing the span and an offset of 6. This would modify the fragment to
rgindaa19afe22012-01-25 15:40:22 -0800384 * become: '<div><span>Hello </span><span>World</span></div>'. If the span
385 * had any attributes they would have been copied to the new span as well.
386 *
387 * The to-be-split node must have a container, so that the new node can be
388 * placed next to it.
389 *
390 * @param {HTMLNode} node The node to split.
391 * @param {integer} offset The offset into the node where the split should
392 * occur.
rginda8ba33642011-12-14 12:31:31 -0800393 */
rgindaa19afe22012-01-25 15:40:22 -0800394hterm.Screen.prototype.splitNode_ = function(node, offset) {
rginda35c456b2012-02-09 17:29:05 -0800395 var afterNode = node.cloneNode(false);
rgindaa19afe22012-01-25 15:40:22 -0800396
rginda35c456b2012-02-09 17:29:05 -0800397 var textContent = node.textContent;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800398 node.textContent = hterm.TextAttributes.nodeSubstr(node, 0, offset);
399 afterNode.textContent = lib.wc.substr(textContent, offset);
rgindaa19afe22012-01-25 15:40:22 -0800400
Ricky Liang48f05cb2013-12-31 23:35:29 +0800401 if (afterNode.textContent)
402 node.parentNode.insertBefore(afterNode, node.nextSibling);
403 if (!node.textContent)
404 node.parentNode.removeChild(node);
rginda8ba33642011-12-14 12:31:31 -0800405};
406
407/**
rgindaa9abdd82012-08-06 18:05:09 -0700408 * Ensure that text is clipped and the cursor is clamped to the column count.
rgindaa19afe22012-01-25 15:40:22 -0800409 */
rgindaa9abdd82012-08-06 18:05:09 -0700410hterm.Screen.prototype.maybeClipCurrentRow = function() {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800411 var width = hterm.TextAttributes.nodeWidth(this.cursorRowNode_);
412
413 if (width <= this.columnCount_) {
rgindaa9abdd82012-08-06 18:05:09 -0700414 // Current row does not need clipping, but may need clamping.
415 if (this.cursorPosition.column >= this.columnCount_) {
416 this.setCursorPosition(this.cursorPosition.row, this.columnCount_ - 1);
417 this.cursorPosition.overflow = true;
418 }
rgindaa19afe22012-01-25 15:40:22 -0800419
rgindaa9abdd82012-08-06 18:05:09 -0700420 return;
421 }
422
423 // Save off the current column so we can maybe restore it later.
424 var currentColumn = this.cursorPosition.column;
425
426 // Move the cursor to the final column.
427 this.setCursorPosition(this.cursorPosition.row, this.columnCount_ - 1);
428
429 // Remove any text that partially overflows.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800430 width = hterm.TextAttributes.nodeWidth(this.cursorNode_);
431
432 if (this.cursorOffset_ < width - 1) {
433 this.cursorNode_.textContent = hterm.TextAttributes.nodeSubstr(
434 this.cursorNode_, 0, this.cursorOffset_ + 1);
rgindaa9abdd82012-08-06 18:05:09 -0700435 }
436
437 // Remove all nodes after the cursor.
rgindaa19afe22012-01-25 15:40:22 -0800438 var rowNode = this.cursorRowNode_;
439 var node = this.cursorNode_.nextSibling;
440
441 while (node) {
rgindaa19afe22012-01-25 15:40:22 -0800442 rowNode.removeChild(node);
443 node = this.cursorNode_.nextSibling;
444 }
445
Robert Ginda7fd57082012-09-25 14:41:47 -0700446 if (currentColumn < this.columnCount_) {
rgindaa9abdd82012-08-06 18:05:09 -0700447 // If the cursor was within the screen before we started then restore its
448 // position.
rgindaa19afe22012-01-25 15:40:22 -0800449 this.setCursorPosition(this.cursorPosition.row, currentColumn);
rgindaa9abdd82012-08-06 18:05:09 -0700450 } else {
451 // Otherwise leave it at the the last column in the overflow state.
452 this.cursorPosition.overflow = true;
rgindaa19afe22012-01-25 15:40:22 -0800453 }
rgindaa19afe22012-01-25 15:40:22 -0800454};
455
456/**
457 * Insert a string at the current character position using the current
458 * text attributes.
459 *
rgindaa09e7332012-08-17 12:49:51 -0700460 * You must call maybeClipCurrentRow() after in order to clip overflowed
461 * text and clamp the cursor.
462 *
463 * It is also up to the caller to properly maintain the line overflow state
464 * using hterm.Screen..commitLineOverflow().
rginda8ba33642011-12-14 12:31:31 -0800465 */
466hterm.Screen.prototype.insertString = function(str) {
rgindaa19afe22012-01-25 15:40:22 -0800467 var cursorNode = this.cursorNode_;
468 var cursorNodeText = cursorNode.textContent;
rginda8ba33642011-12-14 12:31:31 -0800469
Robert Gindaa21dfb32013-10-31 14:17:45 -0700470 this.cursorRowNode_.removeAttribute('line-overflow');
471
Ricky Liang48f05cb2013-12-31 23:35:29 +0800472 // We may alter the width of the string by prepending some missing
473 // whitespaces, so we need to record the string width ahead of time.
474 var strWidth = lib.wc.strWidth(str);
rginda8ba33642011-12-14 12:31:31 -0800475
rgindaa19afe22012-01-25 15:40:22 -0800476 // No matter what, before this function exits the cursor column will have
477 // moved this much.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800478 this.cursorPosition.column += strWidth;
rginda8ba33642011-12-14 12:31:31 -0800479
rgindaa19afe22012-01-25 15:40:22 -0800480 // Local cache of the cursor offset.
481 var offset = this.cursorOffset_;
rginda8ba33642011-12-14 12:31:31 -0800482
rgindaa19afe22012-01-25 15:40:22 -0800483 // Reverse offset is the offset measured from the end of the string.
484 // Zero implies that the cursor is at the end of the cursor node.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800485 var reverseOffset = hterm.TextAttributes.nodeWidth(cursorNode) - offset;
rgindaa19afe22012-01-25 15:40:22 -0800486
487 if (reverseOffset < 0) {
488 // A negative reverse offset means the cursor is positioned past the end
489 // of the characters on this line. We'll need to insert the missing
490 // whitespace.
rgindacbbd7482012-06-13 15:06:16 -0700491 var ws = lib.f.getWhitespace(-reverseOffset);
rgindaa19afe22012-01-25 15:40:22 -0800492
Brad Town7de83302015-03-12 02:10:32 -0700493 // This whitespace should be completely unstyled. Underline, background
494 // color, and strikethrough would be visible on whitespace, so we can't use
495 // one of those spans to hold the text.
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200496 if (!(this.textAttributes.underline ||
Brad Town7de83302015-03-12 02:10:32 -0700497 this.textAttributes.strikethrough ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200498 this.textAttributes.background ||
499 this.textAttributes.wcNode ||
Mike Frysinger1e98c0f2017-08-15 01:21:31 -0400500 !this.textAttributes.asciiNode ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200501 this.textAttributes.tileData != null)) {
rgindaa19afe22012-01-25 15:40:22 -0800502 // Best case scenario, we can just pretend the spaces were part of the
503 // original string.
504 str = ws + str;
505 } else if (cursorNode.nodeType == 3 ||
Ricky Liang48f05cb2013-12-31 23:35:29 +0800506 !(cursorNode.wcNode ||
Mike Frysinger1e98c0f2017-08-15 01:21:31 -0400507 !cursorNode.asciiNode ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200508 cursorNode.tileNode ||
Ricky Liang48f05cb2013-12-31 23:35:29 +0800509 cursorNode.style.textDecoration ||
rgindaa19afe22012-01-25 15:40:22 -0800510 cursorNode.style.backgroundColor)) {
511 // Second best case, the current node is able to hold the whitespace.
512 cursorNode.textContent = (cursorNodeText += ws);
513 } else {
514 // Worst case, we have to create a new node to hold the whitespace.
515 var wsNode = cursorNode.ownerDocument.createTextNode(ws);
516 this.cursorRowNode_.insertBefore(wsNode, cursorNode.nextSibling);
517 this.cursorNode_ = cursorNode = wsNode;
518 this.cursorOffset_ = offset = -reverseOffset;
519 cursorNodeText = ws;
520 }
521
522 // We now know for sure that we're at the last character of the cursor node.
523 reverseOffset = 0;
rginda8ba33642011-12-14 12:31:31 -0800524 }
525
rgindaa19afe22012-01-25 15:40:22 -0800526 if (this.textAttributes.matchesContainer(cursorNode)) {
527 // The new text can be placed directly in the cursor node.
528 if (reverseOffset == 0) {
529 cursorNode.textContent = cursorNodeText + str;
530 } else if (offset == 0) {
531 cursorNode.textContent = str + cursorNodeText;
532 } else {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800533 cursorNode.textContent =
534 hterm.TextAttributes.nodeSubstr(cursorNode, 0, offset) +
535 str + hterm.TextAttributes.nodeSubstr(cursorNode, offset);
rgindaa19afe22012-01-25 15:40:22 -0800536 }
rginda8ba33642011-12-14 12:31:31 -0800537
Ricky Liang48f05cb2013-12-31 23:35:29 +0800538 this.cursorOffset_ += strWidth;
rgindaa19afe22012-01-25 15:40:22 -0800539 return;
rginda87b86462011-12-14 13:48:03 -0800540 }
541
rgindaa19afe22012-01-25 15:40:22 -0800542 // The cursor node is the wrong style for the new text. If we're at the
543 // beginning or end of the cursor node, then the adjacent node is also a
544 // potential candidate.
rginda8ba33642011-12-14 12:31:31 -0800545
rgindaa19afe22012-01-25 15:40:22 -0800546 if (offset == 0) {
547 // At the beginning of the cursor node, the check the previous sibling.
548 var previousSibling = cursorNode.previousSibling;
549 if (previousSibling &&
550 this.textAttributes.matchesContainer(previousSibling)) {
551 previousSibling.textContent += str;
552 this.cursorNode_ = previousSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800553 this.cursorOffset_ = lib.wc.strWidth(previousSibling.textContent);
rgindaa19afe22012-01-25 15:40:22 -0800554 return;
555 }
556
557 var newNode = this.textAttributes.createContainer(str);
558 this.cursorRowNode_.insertBefore(newNode, cursorNode);
559 this.cursorNode_ = newNode;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800560 this.cursorOffset_ = strWidth;
rgindaa19afe22012-01-25 15:40:22 -0800561 return;
562 }
563
564 if (reverseOffset == 0) {
565 // At the end of the cursor node, the check the next sibling.
566 var nextSibling = cursorNode.nextSibling;
567 if (nextSibling &&
568 this.textAttributes.matchesContainer(nextSibling)) {
569 nextSibling.textContent = str + nextSibling.textContent;
570 this.cursorNode_ = nextSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800571 this.cursorOffset_ = lib.wc.strWidth(str);
rgindaa19afe22012-01-25 15:40:22 -0800572 return;
573 }
574
575 var newNode = this.textAttributes.createContainer(str);
576 this.cursorRowNode_.insertBefore(newNode, nextSibling);
577 this.cursorNode_ = newNode;
578 // We specifically need to include any missing whitespace here, since it's
579 // going in a new node.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800580 this.cursorOffset_ = hterm.TextAttributes.nodeWidth(newNode);
rgindaa19afe22012-01-25 15:40:22 -0800581 return;
582 }
583
584 // Worst case, we're somewhere in the middle of the cursor node. We'll
585 // have to split it into two nodes and insert our new container in between.
586 this.splitNode_(cursorNode, offset);
587 var newNode = this.textAttributes.createContainer(str);
588 this.cursorRowNode_.insertBefore(newNode, cursorNode.nextSibling);
589 this.cursorNode_ = newNode;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800590 this.cursorOffset_ = strWidth;
rgindaa19afe22012-01-25 15:40:22 -0800591};
592
593/**
rginda8ba33642011-12-14 12:31:31 -0800594 * Overwrite the text at the current cursor position.
595 *
rgindaa09e7332012-08-17 12:49:51 -0700596 * You must call maybeClipCurrentRow() after in order to clip overflowed
597 * text and clamp the cursor.
598 *
599 * It is also up to the caller to properly maintain the line overflow state
600 * using hterm.Screen..commitLineOverflow().
rginda8ba33642011-12-14 12:31:31 -0800601 */
602hterm.Screen.prototype.overwriteString = function(str) {
603 var maxLength = this.columnCount_ - this.cursorPosition.column;
604 if (!maxLength)
rgindaa19afe22012-01-25 15:40:22 -0800605 return [str];
606
Ricky Liang48f05cb2013-12-31 23:35:29 +0800607 var width = lib.wc.strWidth(str);
608 if (this.textAttributes.matchesContainer(this.cursorNode_) &&
609 this.cursorNode_.textContent.substr(this.cursorOffset_) == str) {
rgindaa19afe22012-01-25 15:40:22 -0800610 // This overwrite would be a no-op, just move the cursor and return.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800611 this.cursorOffset_ += width;
612 this.cursorPosition.column += width;
rgindaa19afe22012-01-25 15:40:22 -0800613 return;
614 }
rginda8ba33642011-12-14 12:31:31 -0800615
Ricky Liang48f05cb2013-12-31 23:35:29 +0800616 this.deleteChars(Math.min(width, maxLength));
rgindaa19afe22012-01-25 15:40:22 -0800617 this.insertString(str);
rginda8ba33642011-12-14 12:31:31 -0800618};
619
620/**
621 * Forward-delete one or more characters at the current cursor position.
622 *
623 * Text to the right of the deleted characters is shifted left. Only affects
624 * characters on the same row as the cursor.
625 *
Ricky Liang48f05cb2013-12-31 23:35:29 +0800626 * @param {integer} count The column width of characters to delete. This is
627 * clamped to the column width minus the cursor column.
628 * @return {integer} The column width of the characters actually deleted.
rginda8ba33642011-12-14 12:31:31 -0800629 */
630hterm.Screen.prototype.deleteChars = function(count) {
631 var node = this.cursorNode_;
632 var offset = this.cursorOffset_;
633
Robert Ginda7fd57082012-09-25 14:41:47 -0700634 var currentCursorColumn = this.cursorPosition.column;
635 count = Math.min(count, this.columnCount_ - currentCursorColumn);
636 if (!count)
637 return 0;
638
639 var rv = count;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800640 var startLength, endLength;
rgindaa19afe22012-01-25 15:40:22 -0800641
rginda8ba33642011-12-14 12:31:31 -0800642 while (node && count) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800643 startLength = hterm.TextAttributes.nodeWidth(node);
644 node.textContent = hterm.TextAttributes.nodeSubstr(node, 0, offset) +
645 hterm.TextAttributes.nodeSubstr(node, offset + count);
646 endLength = hterm.TextAttributes.nodeWidth(node);
rginda8ba33642011-12-14 12:31:31 -0800647 count -= startLength - endLength;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800648 if (offset < startLength && endLength && startLength == endLength) {
649 // No characters were deleted when there should be. We're probably trying
650 // to delete one column width from a wide character node. We remove the
651 // wide character node here and replace it with a single space.
652 var spaceNode = this.textAttributes.createContainer(' ');
653 node.parentNode.insertBefore(spaceNode, node.nextSibling);
654 node.textContent = '';
655 endLength = 0;
656 count -= 1;
rginda8ba33642011-12-14 12:31:31 -0800657 }
658
Ricky Liang48f05cb2013-12-31 23:35:29 +0800659 var nextNode = node.nextSibling;
660 if (endLength == 0 && node != this.cursorNode_) {
661 node.parentNode.removeChild(node);
662 }
663 node = nextNode;
rginda8ba33642011-12-14 12:31:31 -0800664 offset = 0;
665 }
Robert Ginda7fd57082012-09-25 14:41:47 -0700666
Ricky Liang48f05cb2013-12-31 23:35:29 +0800667 // Remove this.cursorNode_ if it is an empty non-text node.
668 if (this.cursorNode_.nodeType != 3 && !this.cursorNode_.textContent) {
669 var cursorNode = this.cursorNode_;
670 if (cursorNode.previousSibling) {
671 this.cursorNode_ = cursorNode.previousSibling;
672 this.cursorOffset_ = hterm.TextAttributes.nodeWidth(
673 cursorNode.previousSibling);
674 } else if (cursorNode.nextSibling) {
675 this.cursorNode_ = cursorNode.nextSibling;
676 this.cursorOffset_ = 0;
677 } else {
678 var emptyNode = this.cursorRowNode_.ownerDocument.createTextNode('');
679 this.cursorRowNode_.appendChild(emptyNode);
680 this.cursorNode_ = emptyNode;
681 this.cursorOffset_ = 0;
682 }
683 this.cursorRowNode_.removeChild(cursorNode);
684 }
685
Robert Ginda7fd57082012-09-25 14:41:47 -0700686 return rv;
rginda8ba33642011-12-14 12:31:31 -0800687};
John Macinnesfb683832013-07-22 14:46:30 -0400688
689/**
690 * Finds first X-ROW of a line containing specified X-ROW.
691 * Used to support line overflow.
692 *
693 * @param {Node} row X-ROW to begin search for first row of line.
694 * @return {Node} The X-ROW that is at the beginning of the line.
695 **/
696hterm.Screen.prototype.getLineStartRow_ = function(row) {
697 while (row.previousSibling &&
698 row.previousSibling.hasAttribute('line-overflow')) {
699 row = row.previousSibling;
700 }
701 return row;
702};
703
704/**
705 * Gets text of a line beginning with row.
706 * Supports line overflow.
707 *
708 * @param {Node} row First X-ROW of line.
709 * @return {string} Text content of line.
710 **/
711hterm.Screen.prototype.getLineText_ = function(row) {
712 var rowText = "";
713 while (row) {
714 rowText += row.textContent;
715 if (row.hasAttribute('line-overflow')) {
716 row = row.nextSibling;
717 } else {
718 break;
719 }
720 }
721 return rowText;
722};
723
724/**
725 * Returns X-ROW that is ancestor of the node.
726 *
727 * @param {Node} node Node to get X-ROW ancestor for.
728 * @return {Node} X-ROW ancestor of node, or null if not found.
729 **/
730hterm.Screen.prototype.getXRowAncestor_ = function(node) {
731 while (node) {
732 if (node.nodeName === 'X-ROW')
733 break;
734 node = node.parentNode;
735 }
736 return node;
737};
738
739/**
740 * Returns position within line of character at offset within node.
741 * Supports line overflow.
742 *
743 * @param {Node} row X-ROW at beginning of line.
744 * @param {Node} node Node to get position of.
745 * @param {integer} offset Offset into node.
746 *
747 * @return {integer} Position within line of character at offset within node.
748 **/
749hterm.Screen.prototype.getPositionWithOverflow_ = function(row, node, offset) {
750 if (!node)
751 return -1;
752 var ancestorRow = this.getXRowAncestor_(node);
753 if (!ancestorRow)
754 return -1;
755 var position = 0;
756 while (ancestorRow != row) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800757 position += hterm.TextAttributes.nodeWidth(row);
John Macinnesfb683832013-07-22 14:46:30 -0400758 if (row.hasAttribute('line-overflow') && row.nextSibling) {
759 row = row.nextSibling;
760 } else {
761 return -1;
762 }
763 }
764 return position + this.getPositionWithinRow_(row, node, offset);
765};
766
767/**
768 * Returns position within row of character at offset within node.
769 * Does not support line overflow.
770 *
771 * @param {Node} row X-ROW to get position within.
772 * @param {Node} node Node to get position for.
773 * @param {integer} offset Offset within node to get position for.
774 * @return {integer} Position within row of character at offset within node.
775 **/
776hterm.Screen.prototype.getPositionWithinRow_ = function(row, node, offset) {
777 if (node.parentNode != row) {
Mike Frysinger498192d2017-06-26 18:23:31 -0400778 // If we traversed to the top node, then there's nothing to find here.
779 if (node.parentNode == null)
780 return -1;
781
John Macinnesfb683832013-07-22 14:46:30 -0400782 return this.getPositionWithinRow_(node.parentNode, node, offset) +
783 this.getPositionWithinRow_(row, node.parentNode, 0);
784 }
785 var position = 0;
786 for (var i = 0; i < row.childNodes.length; i++) {
787 var currentNode = row.childNodes[i];
788 if (currentNode == node)
789 return position + offset;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800790 position += hterm.TextAttributes.nodeWidth(currentNode);
John Macinnesfb683832013-07-22 14:46:30 -0400791 }
792 return -1;
793};
794
795/**
796 * Returns the node and offset corresponding to position within line.
797 * Supports line overflow.
798 *
799 * @param {Node} row X-ROW at beginning of line.
800 * @param {integer} position Position within line to retrieve node and offset.
801 * @return {Array} Two element array containing node and offset respectively.
802 **/
803hterm.Screen.prototype.getNodeAndOffsetWithOverflow_ = function(row, position) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800804 while (row && position > hterm.TextAttributes.nodeWidth(row)) {
John Macinnesfb683832013-07-22 14:46:30 -0400805 if (row.hasAttribute('line-overflow') && row.nextSibling) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800806 position -= hterm.TextAttributes.nodeWidth(row);
John Macinnesfb683832013-07-22 14:46:30 -0400807 row = row.nextSibling;
808 } else {
809 return -1;
810 }
811 }
812 return this.getNodeAndOffsetWithinRow_(row, position);
813};
814
815/**
816 * Returns the node and offset corresponding to position within row.
817 * Does not support line overflow.
818 *
819 * @param {Node} row X-ROW to get position within.
820 * @param {integer} position Position within row to retrieve node and offset.
821 * @return {Array} Two element array containing node and offset respectively.
822 **/
823hterm.Screen.prototype.getNodeAndOffsetWithinRow_ = function(row, position) {
824 for (var i = 0; i < row.childNodes.length; i++) {
825 var node = row.childNodes[i];
Ricky Liang48f05cb2013-12-31 23:35:29 +0800826 var nodeTextWidth = hterm.TextAttributes.nodeWidth(node);
827 if (position <= nodeTextWidth) {
John Macinnesfb683832013-07-22 14:46:30 -0400828 if (node.nodeName === 'SPAN') {
829 /** Drill down to node contained by SPAN. **/
830 return this.getNodeAndOffsetWithinRow_(node, position);
831 } else {
832 return [node, position];
833 }
834 }
Ricky Liang48f05cb2013-12-31 23:35:29 +0800835 position -= nodeTextWidth;
John Macinnesfb683832013-07-22 14:46:30 -0400836 }
837 return null;
838};
839
840/**
841 * Returns the node and offset corresponding to position within line.
842 * Supports line overflow.
843 *
844 * @param {Node} row X-ROW at beginning of line.
845 * @param {integer} start Start position of range within line.
846 * @param {integer} end End position of range within line.
847 * @param {Range} range Range to modify.
848 **/
849hterm.Screen.prototype.setRange_ = function(row, start, end, range) {
850 var startNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, start);
851 if (startNodeAndOffset == null)
852 return;
853 var endNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, end);
854 if (endNodeAndOffset == null)
855 return;
856 range.setStart(startNodeAndOffset[0], startNodeAndOffset[1]);
857 range.setEnd(endNodeAndOffset[0], endNodeAndOffset[1]);
858};
859
860/**
861 * Expands selection to surround URLs.
862 *
John Macinnesfb683832013-07-22 14:46:30 -0400863 * @param {Selection} selection Selection to expand.
864 **/
865hterm.Screen.prototype.expandSelection = function(selection) {
866 if (!selection)
867 return;
868
869 var range = selection.getRangeAt(0);
870 if (!range || range.toString().match(/\s/))
871 return;
872
873 var row = this.getLineStartRow_(this.getXRowAncestor_(range.startContainer));
874 if (!row)
875 return;
876
877 var startPosition = this.getPositionWithOverflow_(row,
878 range.startContainer,
879 range.startOffset);
880 if (startPosition == -1)
881 return;
882 var endPosition = this.getPositionWithOverflow_(row,
883 range.endContainer,
884 range.endOffset);
885 if (endPosition == -1)
886 return;
887
Mike Frysinger664e9992017-05-19 01:24:24 -0400888 // Use the user configurable match settings.
889 var leftMatch = this.wordBreakMatchLeft;
890 var rightMatch = this.wordBreakMatchRight;
891 var insideMatch = this.wordBreakMatchMiddle;
John Macinnesfb683832013-07-22 14:46:30 -0400892
893 //Move start to the left.
894 var rowText = this.getLineText_(row);
Ricky Liang48f05cb2013-12-31 23:35:29 +0800895 var lineUpToRange = lib.wc.substring(rowText, 0, endPosition);
Robert Ginda5eba4562014-08-11 11:05:54 -0700896 var leftRegularExpression = new RegExp(leftMatch + insideMatch + "$");
John Macinnesfb683832013-07-22 14:46:30 -0400897 var expandedStart = lineUpToRange.search(leftRegularExpression);
898 if (expandedStart == -1 || expandedStart > startPosition)
899 return;
900
901 //Move end to the right.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800902 var lineFromRange = lib.wc.substring(rowText, startPosition,
903 lib.wc.strWidth(rowText));
Robert Ginda5eba4562014-08-11 11:05:54 -0700904 var rightRegularExpression = new RegExp("^" + insideMatch + rightMatch);
John Macinnesfb683832013-07-22 14:46:30 -0400905 var found = lineFromRange.match(rightRegularExpression);
906 if (!found)
907 return;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800908 var expandedEnd = startPosition + lib.wc.strWidth(found[0]);
John Macinnesfb683832013-07-22 14:46:30 -0400909 if (expandedEnd == -1 || expandedEnd < endPosition)
910 return;
911
912 this.setRange_(row, expandedStart, expandedEnd, range);
913 selection.addRange(range);
914};