blob: 3656ae71609fbe3c62a6b18c005baf1c7b3f8d69 [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
rginda8ba33642011-12-14 12:31:31 -08007/**
8 * @fileoverview This class represents a single terminal screen full of text.
9 *
10 * It maintains the current cursor position and has basic methods for text
11 * insert and overwrite, and adding or removing rows from the screen.
12 *
13 * This class has no knowledge of the scrollback buffer.
14 *
15 * The number of rows on the screen is determined only by the number of rows
16 * that the caller inserts into the screen. If a caller wants to ensure a
17 * constant number of rows on the screen, it's their responsibility to remove a
18 * row for each row inserted.
19 *
20 * The screen width, in contrast, is enforced locally.
21 *
22 *
23 * In practice...
24 * - The hterm.Terminal class holds two hterm.Screen instances. One for the
25 * primary screen and one for the alternate screen.
26 *
Joel Hockey0f933582019-08-27 18:01:51 -070027 * - The html.Screen class only cares that rows are HTML Elements. In the
rginda8ba33642011-12-14 12:31:31 -080028 * larger context of hterm, however, the rows happen to be displayed by an
29 * hterm.ScrollPort and have to follow a few rules as a result. Each
30 * row must be rooted by the custom HTML tag 'x-row', and each must have a
31 * rowIndex property that corresponds to the index of the row in the context
32 * of the scrollback buffer. These invariants are enforced by hterm.Terminal
33 * because that is the class using the hterm.Screen in the context of an
34 * hterm.ScrollPort.
35 */
36
37/**
38 * Create a new screen instance.
39 *
40 * The screen initially has no rows and a maximum column count of 0.
41 *
Joel Hockey0f933582019-08-27 18:01:51 -070042 * @param {number=} columnCount The maximum number of columns for this
rginda8ba33642011-12-14 12:31:31 -080043 * screen. See insertString() and overwriteString() for information about
44 * what happens when too many characters are added too a row. Defaults to
45 * 0 if not provided.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -070046 * @constructor
rginda8ba33642011-12-14 12:31:31 -080047 */
Mike Frysinger60a156d2019-06-13 10:15:45 -040048hterm.Screen = function(columnCount=0) {
rginda8ba33642011-12-14 12:31:31 -080049 /**
50 * Public, read-only access to the rows in this screen.
Mike Frysinger23b5b832019-10-01 17:05:29 -040051 *
Joel Hockeyadd2f7e2019-09-20 16:37:35 -070052 * @type {!Array<!Element>}
rginda8ba33642011-12-14 12:31:31 -080053 */
54 this.rowsArray = [];
55
56 // The max column width for this screen.
Mike Frysinger60a156d2019-06-13 10:15:45 -040057 this.columnCount_ = columnCount;
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
Mike Frysingera2cacaa2017-11-29 13:51:09 -080065 // Saved state used by DECSC and related settings. This is only for saving
66 // and restoring specific state, not for the current/active state.
67 this.cursorState_ = new hterm.Screen.CursorState(this);
68
rginda8ba33642011-12-14 12:31:31 -080069 // The node containing the row that the cursor is positioned on.
70 this.cursorRowNode_ = null;
71
72 // The node containing the span of text that the cursor is positioned on.
73 this.cursorNode_ = null;
74
Ricky Liang48f05cb2013-12-31 23:35:29 +080075 // The offset in column width into cursorNode_ where the cursor is positioned.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -070076 this.cursorOffset_ = 0;
Mike Frysinger664e9992017-05-19 01:24:24 -040077
78 // Regexes for expanding word selections.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -070079 /** @type {?string} */
Mike Frysinger664e9992017-05-19 01:24:24 -040080 this.wordBreakMatchLeft = null;
Joel Hockeyadd2f7e2019-09-20 16:37:35 -070081 /** @type {?string} */
Mike Frysinger664e9992017-05-19 01:24:24 -040082 this.wordBreakMatchRight = null;
Joel Hockeyadd2f7e2019-09-20 16:37:35 -070083 /** @type {?string} */
Mike Frysinger664e9992017-05-19 01:24:24 -040084 this.wordBreakMatchMiddle = null;
rginda8ba33642011-12-14 12:31:31 -080085};
86
87/**
88 * Return the screen size as an hterm.Size object.
89 *
Joel Hockey0f933582019-08-27 18:01:51 -070090 * @return {!hterm.Size} hterm.Size object representing the current number
rginda8ba33642011-12-14 12:31:31 -080091 * of rows and columns in this screen.
92 */
93hterm.Screen.prototype.getSize = function() {
94 return new hterm.Size(this.columnCount_, this.rowsArray.length);
95};
96
97/**
98 * Return the current number of rows in this screen.
99 *
Joel Hockey0f933582019-08-27 18:01:51 -0700100 * @return {number} The number of rows in this screen.
rginda8ba33642011-12-14 12:31:31 -0800101 */
102hterm.Screen.prototype.getHeight = function() {
103 return this.rowsArray.length;
104};
105
106/**
107 * Return the current number of columns in this screen.
108 *
Joel Hockey0f933582019-08-27 18:01:51 -0700109 * @return {number} The number of columns in this screen.
rginda8ba33642011-12-14 12:31:31 -0800110 */
111hterm.Screen.prototype.getWidth = function() {
112 return this.columnCount_;
113};
114
115/**
116 * Set the maximum number of columns per row.
117 *
Joel Hockey0f933582019-08-27 18:01:51 -0700118 * @param {number} count The maximum number of columns per row.
rginda8ba33642011-12-14 12:31:31 -0800119 */
120hterm.Screen.prototype.setColumnCount = function(count) {
rginda2312fff2012-01-05 16:20:52 -0800121 this.columnCount_ = count;
122
Mike Frysingerbdb34802020-04-07 03:47:32 -0400123 if (this.cursorPosition.column >= count) {
rgindacbbd7482012-06-13 15:06:16 -0700124 this.setCursorPosition(this.cursorPosition.row, count - 1);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400125 }
rginda8ba33642011-12-14 12:31:31 -0800126};
127
128/**
129 * Remove the first row from the screen and return it.
130 *
Joel Hockey0f933582019-08-27 18:01:51 -0700131 * @return {!Element} The first row in this screen.
rginda8ba33642011-12-14 12:31:31 -0800132 */
133hterm.Screen.prototype.shiftRow = function() {
134 return this.shiftRows(1)[0];
rginda87b86462011-12-14 13:48:03 -0800135};
rginda8ba33642011-12-14 12:31:31 -0800136
137/**
138 * Remove rows from the top of the screen and return them as an array.
139 *
Joel Hockey0f933582019-08-27 18:01:51 -0700140 * @param {number} count The number of rows to remove.
141 * @return {!Array<!Element>} The selected rows.
rginda8ba33642011-12-14 12:31:31 -0800142 */
143hterm.Screen.prototype.shiftRows = function(count) {
144 return this.rowsArray.splice(0, count);
145};
146
147/**
148 * Insert a row at the top of the screen.
149 *
Joel Hockey0f933582019-08-27 18:01:51 -0700150 * @param {!Element} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800151 */
152hterm.Screen.prototype.unshiftRow = function(row) {
153 this.rowsArray.splice(0, 0, row);
154};
155
156/**
157 * Insert rows at the top of the screen.
158 *
Joel Hockey0f933582019-08-27 18:01:51 -0700159 * @param {!Array<!Element>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800160 */
161hterm.Screen.prototype.unshiftRows = function(rows) {
162 this.rowsArray.unshift.apply(this.rowsArray, rows);
163};
164
165/**
166 * Remove the last row from the screen and return it.
167 *
Joel Hockey0f933582019-08-27 18:01:51 -0700168 * @return {!Element} The last row in this screen.
rginda8ba33642011-12-14 12:31:31 -0800169 */
170hterm.Screen.prototype.popRow = function() {
171 return this.popRows(1)[0];
172};
173
174/**
175 * Remove rows from the bottom of the screen and return them as an array.
176 *
Joel Hockey0f933582019-08-27 18:01:51 -0700177 * @param {number} count The number of rows to remove.
178 * @return {!Array<!Element>} The selected rows.
rginda8ba33642011-12-14 12:31:31 -0800179 */
180hterm.Screen.prototype.popRows = function(count) {
181 return this.rowsArray.splice(this.rowsArray.length - count, count);
182};
183
184/**
185 * Insert a row at the bottom of the screen.
186 *
Joel Hockey0f933582019-08-27 18:01:51 -0700187 * @param {!Element} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800188 */
189hterm.Screen.prototype.pushRow = function(row) {
190 this.rowsArray.push(row);
191};
192
193/**
194 * Insert rows at the bottom of the screen.
195 *
Joel Hockey0f933582019-08-27 18:01:51 -0700196 * @param {!Array<!Element>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800197 */
198hterm.Screen.prototype.pushRows = function(rows) {
199 rows.push.apply(this.rowsArray, rows);
200};
201
202/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500203 * Insert a row at the specified row of the screen.
rginda8ba33642011-12-14 12:31:31 -0800204 *
Joel Hockey0f933582019-08-27 18:01:51 -0700205 * @param {number} index The index to insert the row.
206 * @param {!Element} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800207 */
208hterm.Screen.prototype.insertRow = function(index, row) {
209 this.rowsArray.splice(index, 0, row);
210};
211
212/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500213 * Insert rows at the specified row of the screen.
rginda8ba33642011-12-14 12:31:31 -0800214 *
Joel Hockey0f933582019-08-27 18:01:51 -0700215 * @param {number} index The index to insert the rows.
216 * @param {!Array<!Element>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800217 */
218hterm.Screen.prototype.insertRows = function(index, rows) {
219 for (var i = 0; i < rows.length; i++) {
220 this.rowsArray.splice(index + i, 0, rows[i]);
221 }
222};
223
224/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500225 * Remove a row from the screen and return it.
rginda8ba33642011-12-14 12:31:31 -0800226 *
Joel Hockey0f933582019-08-27 18:01:51 -0700227 * @param {number} index The index of the row to remove.
228 * @return {!Element} The selected row.
rginda8ba33642011-12-14 12:31:31 -0800229 */
230hterm.Screen.prototype.removeRow = function(index) {
231 return this.rowsArray.splice(index, 1)[0];
232};
233
234/**
235 * Remove rows from the bottom of the screen and return them as an array.
236 *
Joel Hockey0f933582019-08-27 18:01:51 -0700237 * @param {number} index The index to start removing rows.
238 * @param {number} count The number of rows to remove.
239 * @return {!Array<!Element>} The selected rows.
rginda8ba33642011-12-14 12:31:31 -0800240 */
241hterm.Screen.prototype.removeRows = function(index, count) {
242 return this.rowsArray.splice(index, count);
243};
244
245/**
246 * Invalidate the current cursor position.
247 *
rginda87b86462011-12-14 13:48:03 -0800248 * This sets this.cursorPosition to (0, 0) and clears out some internal
rginda8ba33642011-12-14 12:31:31 -0800249 * data.
250 *
251 * Attempting to insert or overwrite text while the cursor position is invalid
252 * will raise an obscure exception.
253 */
254hterm.Screen.prototype.invalidateCursorPosition = function() {
rginda87b86462011-12-14 13:48:03 -0800255 this.cursorPosition.move(0, 0);
rginda8ba33642011-12-14 12:31:31 -0800256 this.cursorRowNode_ = null;
257 this.cursorNode_ = null;
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700258 this.cursorOffset_ = 0;
rginda8ba33642011-12-14 12:31:31 -0800259};
260
261/**
rginda8ba33642011-12-14 12:31:31 -0800262 * Clear the contents of the cursor row.
rginda8ba33642011-12-14 12:31:31 -0800263 */
264hterm.Screen.prototype.clearCursorRow = function() {
265 this.cursorRowNode_.innerHTML = '';
rgindaa09e7332012-08-17 12:49:51 -0700266 this.cursorRowNode_.removeAttribute('line-overflow');
rginda8ba33642011-12-14 12:31:31 -0800267 this.cursorOffset_ = 0;
rginda8ba33642011-12-14 12:31:31 -0800268 this.cursorPosition.column = 0;
rginda2312fff2012-01-05 16:20:52 -0800269 this.cursorPosition.overflow = false;
Robert Ginda7fd57082012-09-25 14:41:47 -0700270
271 var text;
272 if (this.textAttributes.isDefault()) {
273 text = '';
274 } else {
Mike Frysinger73e56462019-07-17 00:23:46 -0500275 text = ' '.repeat(this.columnCount_);
Robert Ginda7fd57082012-09-25 14:41:47 -0700276 }
277
Zhu Qunying30d40712017-03-14 16:27:00 -0700278 // We shouldn't honor inverse colors when clearing an area, to match
279 // xterm's back color erase behavior.
Edoardo Spadolini2fd43642014-08-23 22:59:57 +0200280 var inverse = this.textAttributes.inverse;
281 this.textAttributes.inverse = false;
282 this.textAttributes.syncColors();
283
Robert Ginda7fd57082012-09-25 14:41:47 -0700284 var node = this.textAttributes.createContainer(text);
285 this.cursorRowNode_.appendChild(node);
286 this.cursorNode_ = node;
Edoardo Spadolini2fd43642014-08-23 22:59:57 +0200287
288 this.textAttributes.inverse = inverse;
289 this.textAttributes.syncColors();
rginda8ba33642011-12-14 12:31:31 -0800290};
291
292/**
rgindaa09e7332012-08-17 12:49:51 -0700293 * Mark the current row as having overflowed to the next line.
294 *
295 * The line overflow state is used when converting a range of rows into text.
296 * It makes it possible to recombine two or more overflow terminal rows into
297 * a single line.
298 *
299 * This is distinct from the cursor being in the overflow state. Cursor
300 * overflow indicates that printing at the cursor position will commit a
301 * line overflow, unless it is preceded by a repositioning of the cursor
302 * to a non-overflow state.
303 */
304hterm.Screen.prototype.commitLineOverflow = function() {
305 this.cursorRowNode_.setAttribute('line-overflow', true);
306};
307
308/**
rginda8ba33642011-12-14 12:31:31 -0800309 * Relocate the cursor to a give row and column.
310 *
Joel Hockey0f933582019-08-27 18:01:51 -0700311 * @param {number} row The zero based row.
312 * @param {number} column The zero based column.
rginda8ba33642011-12-14 12:31:31 -0800313 */
314hterm.Screen.prototype.setCursorPosition = function(row, column) {
rginda11057d52012-04-25 12:29:56 -0700315 if (!this.rowsArray.length) {
316 console.warn('Attempt to set cursor position on empty screen.');
317 return;
318 }
319
rginda87b86462011-12-14 13:48:03 -0800320 if (row >= this.rowsArray.length) {
rgindacbbd7482012-06-13 15:06:16 -0700321 console.error('Row out of bounds: ' + row);
rginda87b86462011-12-14 13:48:03 -0800322 row = this.rowsArray.length - 1;
323 } else if (row < 0) {
rgindacbbd7482012-06-13 15:06:16 -0700324 console.error('Row out of bounds: ' + row);
rginda87b86462011-12-14 13:48:03 -0800325 row = 0;
326 }
327
328 if (column >= this.columnCount_) {
rgindacbbd7482012-06-13 15:06:16 -0700329 console.error('Column out of bounds: ' + column);
rginda87b86462011-12-14 13:48:03 -0800330 column = this.columnCount_ - 1;
331 } else if (column < 0) {
rgindacbbd7482012-06-13 15:06:16 -0700332 console.error('Column out of bounds: ' + column);
rginda87b86462011-12-14 13:48:03 -0800333 column = 0;
334 }
rginda8ba33642011-12-14 12:31:31 -0800335
rginda2312fff2012-01-05 16:20:52 -0800336 this.cursorPosition.overflow = false;
337
rginda8ba33642011-12-14 12:31:31 -0800338 var rowNode = this.rowsArray[row];
339 var node = rowNode.firstChild;
340
341 if (!node) {
342 node = rowNode.ownerDocument.createTextNode('');
343 rowNode.appendChild(node);
344 }
345
rgindaa19afe22012-01-25 15:40:22 -0800346 var currentColumn = 0;
347
rginda8ba33642011-12-14 12:31:31 -0800348 if (rowNode == this.cursorRowNode_) {
349 if (column >= this.cursorPosition.column - this.cursorOffset_) {
350 node = this.cursorNode_;
351 currentColumn = this.cursorPosition.column - this.cursorOffset_;
352 }
353 } else {
354 this.cursorRowNode_ = rowNode;
355 }
356
357 this.cursorPosition.move(row, column);
358
359 while (node) {
360 var offset = column - currentColumn;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800361 var width = hterm.TextAttributes.nodeWidth(node);
362 if (!node.nextSibling || width > offset) {
rginda8ba33642011-12-14 12:31:31 -0800363 this.cursorNode_ = node;
364 this.cursorOffset_ = offset;
365 return;
366 }
367
Ricky Liang48f05cb2013-12-31 23:35:29 +0800368 currentColumn += width;
rginda8ba33642011-12-14 12:31:31 -0800369 node = node.nextSibling;
370 }
371};
372
373/**
rginda87b86462011-12-14 13:48:03 -0800374 * Set the provided selection object to be a caret selection at the current
375 * cursor position.
Joel Hockey0f933582019-08-27 18:01:51 -0700376 *
377 * @param {!Selection} selection
rginda87b86462011-12-14 13:48:03 -0800378 */
379hterm.Screen.prototype.syncSelectionCaret = function(selection) {
Rob Spies06533ba2014-04-24 11:20:37 -0700380 try {
381 selection.collapse(this.cursorNode_, this.cursorOffset_);
382 } catch (firefoxIgnoredException) {
383 // FF can throw an exception if the range is off, rather than just not
384 // performing the collapse.
385 }
rginda87b86462011-12-14 13:48:03 -0800386};
387
388/**
rgindaa19afe22012-01-25 15:40:22 -0800389 * Split a single node into two nodes at the given offset.
rginda8ba33642011-12-14 12:31:31 -0800390 *
rgindaa19afe22012-01-25 15:40:22 -0800391 * For example:
392 * Given the DOM fragment '<div><span>Hello World</span></div>', call splitNode_
Zhu Qunying30d40712017-03-14 16:27:00 -0700393 * passing the span and an offset of 6. This would modify the fragment to
rgindaa19afe22012-01-25 15:40:22 -0800394 * become: '<div><span>Hello </span><span>World</span></div>'. If the span
395 * had any attributes they would have been copied to the new span as well.
396 *
397 * The to-be-split node must have a container, so that the new node can be
398 * placed next to it.
399 *
Joel Hockey0f933582019-08-27 18:01:51 -0700400 * @param {!Node} node The node to split.
401 * @param {number} offset The offset into the node where the split should
rgindaa19afe22012-01-25 15:40:22 -0800402 * occur.
rginda8ba33642011-12-14 12:31:31 -0800403 */
rgindaa19afe22012-01-25 15:40:22 -0800404hterm.Screen.prototype.splitNode_ = function(node, offset) {
rginda35c456b2012-02-09 17:29:05 -0800405 var afterNode = node.cloneNode(false);
rgindaa19afe22012-01-25 15:40:22 -0800406
rginda35c456b2012-02-09 17:29:05 -0800407 var textContent = node.textContent;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800408 node.textContent = hterm.TextAttributes.nodeSubstr(node, 0, offset);
409 afterNode.textContent = lib.wc.substr(textContent, offset);
rgindaa19afe22012-01-25 15:40:22 -0800410
Mike Frysingerbdb34802020-04-07 03:47:32 -0400411 if (afterNode.textContent) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800412 node.parentNode.insertBefore(afterNode, node.nextSibling);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400413 }
414 if (!node.textContent) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800415 node.parentNode.removeChild(node);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400416 }
rginda8ba33642011-12-14 12:31:31 -0800417};
418
419/**
rgindaa9abdd82012-08-06 18:05:09 -0700420 * Ensure that text is clipped and the cursor is clamped to the column count.
rgindaa19afe22012-01-25 15:40:22 -0800421 */
rgindaa9abdd82012-08-06 18:05:09 -0700422hterm.Screen.prototype.maybeClipCurrentRow = function() {
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700423 var width = hterm.TextAttributes.nodeWidth(lib.notNull(this.cursorRowNode_));
Ricky Liang48f05cb2013-12-31 23:35:29 +0800424
425 if (width <= this.columnCount_) {
rgindaa9abdd82012-08-06 18:05:09 -0700426 // Current row does not need clipping, but may need clamping.
427 if (this.cursorPosition.column >= this.columnCount_) {
428 this.setCursorPosition(this.cursorPosition.row, this.columnCount_ - 1);
429 this.cursorPosition.overflow = true;
430 }
rgindaa19afe22012-01-25 15:40:22 -0800431
rgindaa9abdd82012-08-06 18:05:09 -0700432 return;
433 }
434
435 // Save off the current column so we can maybe restore it later.
436 var currentColumn = this.cursorPosition.column;
437
438 // Move the cursor to the final column.
439 this.setCursorPosition(this.cursorPosition.row, this.columnCount_ - 1);
440
441 // Remove any text that partially overflows.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700442 width = hterm.TextAttributes.nodeWidth(lib.notNull(this.cursorNode_));
Ricky Liang48f05cb2013-12-31 23:35:29 +0800443
444 if (this.cursorOffset_ < width - 1) {
445 this.cursorNode_.textContent = hterm.TextAttributes.nodeSubstr(
446 this.cursorNode_, 0, this.cursorOffset_ + 1);
rgindaa9abdd82012-08-06 18:05:09 -0700447 }
448
449 // Remove all nodes after the cursor.
rgindaa19afe22012-01-25 15:40:22 -0800450 var rowNode = this.cursorRowNode_;
451 var node = this.cursorNode_.nextSibling;
452
453 while (node) {
rgindaa19afe22012-01-25 15:40:22 -0800454 rowNode.removeChild(node);
455 node = this.cursorNode_.nextSibling;
456 }
457
Robert Ginda7fd57082012-09-25 14:41:47 -0700458 if (currentColumn < this.columnCount_) {
rgindaa9abdd82012-08-06 18:05:09 -0700459 // If the cursor was within the screen before we started then restore its
460 // position.
rgindaa19afe22012-01-25 15:40:22 -0800461 this.setCursorPosition(this.cursorPosition.row, currentColumn);
rgindaa9abdd82012-08-06 18:05:09 -0700462 } else {
463 // Otherwise leave it at the the last column in the overflow state.
464 this.cursorPosition.overflow = true;
rgindaa19afe22012-01-25 15:40:22 -0800465 }
rgindaa19afe22012-01-25 15:40:22 -0800466};
467
468/**
469 * Insert a string at the current character position using the current
470 * text attributes.
471 *
rgindaa09e7332012-08-17 12:49:51 -0700472 * You must call maybeClipCurrentRow() after in order to clip overflowed
473 * text and clamp the cursor.
474 *
475 * It is also up to the caller to properly maintain the line overflow state
476 * using hterm.Screen..commitLineOverflow().
Joel Hockey0f933582019-08-27 18:01:51 -0700477 *
478 * @param {string} str The string to insert.
479 * @param {number=} wcwidth The cached lib.wc.strWidth value for |str|. Will be
480 * calculated on demand if need be. Passing in a cached value helps speed
481 * up processing as this is a hot codepath.
rginda8ba33642011-12-14 12:31:31 -0800482 */
Mike Frysinger6380bed2017-08-24 18:46:39 -0400483hterm.Screen.prototype.insertString = function(str, wcwidth=undefined) {
rgindaa19afe22012-01-25 15:40:22 -0800484 var cursorNode = this.cursorNode_;
485 var cursorNodeText = cursorNode.textContent;
rginda8ba33642011-12-14 12:31:31 -0800486
Robert Gindaa21dfb32013-10-31 14:17:45 -0700487 this.cursorRowNode_.removeAttribute('line-overflow');
488
Ricky Liang48f05cb2013-12-31 23:35:29 +0800489 // We may alter the width of the string by prepending some missing
490 // whitespaces, so we need to record the string width ahead of time.
Mike Frysingerbdb34802020-04-07 03:47:32 -0400491 if (wcwidth === undefined) {
Mike Frysinger6380bed2017-08-24 18:46:39 -0400492 wcwidth = lib.wc.strWidth(str);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400493 }
rginda8ba33642011-12-14 12:31:31 -0800494
rgindaa19afe22012-01-25 15:40:22 -0800495 // No matter what, before this function exits the cursor column will have
496 // moved this much.
Mike Frysinger6380bed2017-08-24 18:46:39 -0400497 this.cursorPosition.column += wcwidth;
rginda8ba33642011-12-14 12:31:31 -0800498
rgindaa19afe22012-01-25 15:40:22 -0800499 // Local cache of the cursor offset.
500 var offset = this.cursorOffset_;
rginda8ba33642011-12-14 12:31:31 -0800501
rgindaa19afe22012-01-25 15:40:22 -0800502 // Reverse offset is the offset measured from the end of the string.
503 // Zero implies that the cursor is at the end of the cursor node.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800504 var reverseOffset = hterm.TextAttributes.nodeWidth(cursorNode) - offset;
rgindaa19afe22012-01-25 15:40:22 -0800505
506 if (reverseOffset < 0) {
507 // A negative reverse offset means the cursor is positioned past the end
508 // of the characters on this line. We'll need to insert the missing
509 // whitespace.
Mike Frysinger73e56462019-07-17 00:23:46 -0500510 const ws = ' '.repeat(-reverseOffset);
rgindaa19afe22012-01-25 15:40:22 -0800511
Brad Town7de83302015-03-12 02:10:32 -0700512 // This whitespace should be completely unstyled. Underline, background
513 // color, and strikethrough would be visible on whitespace, so we can't use
514 // one of those spans to hold the text.
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200515 if (!(this.textAttributes.underline ||
Brad Town7de83302015-03-12 02:10:32 -0700516 this.textAttributes.strikethrough ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200517 this.textAttributes.background ||
518 this.textAttributes.wcNode ||
Mike Frysinger1e98c0f2017-08-15 01:21:31 -0400519 !this.textAttributes.asciiNode ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200520 this.textAttributes.tileData != null)) {
rgindaa19afe22012-01-25 15:40:22 -0800521 // Best case scenario, we can just pretend the spaces were part of the
522 // original string.
523 str = ws + str;
Mike Frysinger6a4f2412017-08-31 01:11:25 -0400524 } else if (cursorNode.nodeType == Node.TEXT_NODE ||
Ricky Liang48f05cb2013-12-31 23:35:29 +0800525 !(cursorNode.wcNode ||
Mike Frysinger1e98c0f2017-08-15 01:21:31 -0400526 !cursorNode.asciiNode ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200527 cursorNode.tileNode ||
Ricky Liang48f05cb2013-12-31 23:35:29 +0800528 cursorNode.style.textDecoration ||
Mike Frysinger09c54f42017-12-15 01:12:30 -0500529 cursorNode.style.textDecorationStyle ||
530 cursorNode.style.textDecorationLine ||
rgindaa19afe22012-01-25 15:40:22 -0800531 cursorNode.style.backgroundColor)) {
532 // Second best case, the current node is able to hold the whitespace.
533 cursorNode.textContent = (cursorNodeText += ws);
534 } else {
535 // Worst case, we have to create a new node to hold the whitespace.
536 var wsNode = cursorNode.ownerDocument.createTextNode(ws);
537 this.cursorRowNode_.insertBefore(wsNode, cursorNode.nextSibling);
538 this.cursorNode_ = cursorNode = wsNode;
539 this.cursorOffset_ = offset = -reverseOffset;
540 cursorNodeText = ws;
541 }
542
543 // We now know for sure that we're at the last character of the cursor node.
544 reverseOffset = 0;
rginda8ba33642011-12-14 12:31:31 -0800545 }
546
rgindaa19afe22012-01-25 15:40:22 -0800547 if (this.textAttributes.matchesContainer(cursorNode)) {
548 // The new text can be placed directly in the cursor node.
549 if (reverseOffset == 0) {
550 cursorNode.textContent = cursorNodeText + str;
551 } else if (offset == 0) {
552 cursorNode.textContent = str + cursorNodeText;
553 } else {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800554 cursorNode.textContent =
555 hterm.TextAttributes.nodeSubstr(cursorNode, 0, offset) +
556 str + hterm.TextAttributes.nodeSubstr(cursorNode, offset);
rgindaa19afe22012-01-25 15:40:22 -0800557 }
rginda8ba33642011-12-14 12:31:31 -0800558
Mike Frysinger6380bed2017-08-24 18:46:39 -0400559 this.cursorOffset_ += wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800560 return;
rginda87b86462011-12-14 13:48:03 -0800561 }
562
rgindaa19afe22012-01-25 15:40:22 -0800563 // The cursor node is the wrong style for the new text. If we're at the
564 // beginning or end of the cursor node, then the adjacent node is also a
565 // potential candidate.
rginda8ba33642011-12-14 12:31:31 -0800566
rgindaa19afe22012-01-25 15:40:22 -0800567 if (offset == 0) {
568 // At the beginning of the cursor node, the check the previous sibling.
569 var previousSibling = cursorNode.previousSibling;
570 if (previousSibling &&
571 this.textAttributes.matchesContainer(previousSibling)) {
572 previousSibling.textContent += str;
573 this.cursorNode_ = previousSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800574 this.cursorOffset_ = lib.wc.strWidth(previousSibling.textContent);
rgindaa19afe22012-01-25 15:40:22 -0800575 return;
576 }
577
578 var newNode = this.textAttributes.createContainer(str);
579 this.cursorRowNode_.insertBefore(newNode, cursorNode);
580 this.cursorNode_ = newNode;
Mike Frysinger6380bed2017-08-24 18:46:39 -0400581 this.cursorOffset_ = wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800582 return;
583 }
584
585 if (reverseOffset == 0) {
586 // At the end of the cursor node, the check the next sibling.
587 var nextSibling = cursorNode.nextSibling;
588 if (nextSibling &&
589 this.textAttributes.matchesContainer(nextSibling)) {
590 nextSibling.textContent = str + nextSibling.textContent;
591 this.cursorNode_ = nextSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800592 this.cursorOffset_ = lib.wc.strWidth(str);
rgindaa19afe22012-01-25 15:40:22 -0800593 return;
594 }
595
596 var newNode = this.textAttributes.createContainer(str);
597 this.cursorRowNode_.insertBefore(newNode, nextSibling);
598 this.cursorNode_ = newNode;
599 // We specifically need to include any missing whitespace here, since it's
600 // going in a new node.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800601 this.cursorOffset_ = hterm.TextAttributes.nodeWidth(newNode);
rgindaa19afe22012-01-25 15:40:22 -0800602 return;
603 }
604
605 // Worst case, we're somewhere in the middle of the cursor node. We'll
606 // have to split it into two nodes and insert our new container in between.
607 this.splitNode_(cursorNode, offset);
608 var newNode = this.textAttributes.createContainer(str);
609 this.cursorRowNode_.insertBefore(newNode, cursorNode.nextSibling);
610 this.cursorNode_ = newNode;
Mike Frysinger6380bed2017-08-24 18:46:39 -0400611 this.cursorOffset_ = wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800612};
613
614/**
rginda8ba33642011-12-14 12:31:31 -0800615 * Overwrite the text at the current cursor position.
616 *
rgindaa09e7332012-08-17 12:49:51 -0700617 * You must call maybeClipCurrentRow() after in order to clip overflowed
618 * text and clamp the cursor.
619 *
620 * It is also up to the caller to properly maintain the line overflow state
621 * using hterm.Screen..commitLineOverflow().
Joel Hockey0f933582019-08-27 18:01:51 -0700622 *
623 * @param {string} str The source string for overwriting existing content.
624 * @param {number=} wcwidth The cached lib.wc.strWidth value for |str|. Will be
625 * calculated on demand if need be. Passing in a cached value helps speed
626 * up processing as this is a hot codepath.
rginda8ba33642011-12-14 12:31:31 -0800627 */
Mike Frysinger6380bed2017-08-24 18:46:39 -0400628hterm.Screen.prototype.overwriteString = function(str, wcwidth=undefined) {
rginda8ba33642011-12-14 12:31:31 -0800629 var maxLength = this.columnCount_ - this.cursorPosition.column;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400630 if (!maxLength) {
Mike Frysinger159b7392019-03-26 11:08:32 -0700631 return;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400632 }
rgindaa19afe22012-01-25 15:40:22 -0800633
Mike Frysingerbdb34802020-04-07 03:47:32 -0400634 if (wcwidth === undefined) {
Mike Frysinger6380bed2017-08-24 18:46:39 -0400635 wcwidth = lib.wc.strWidth(str);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400636 }
Mike Frysinger6380bed2017-08-24 18:46:39 -0400637
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700638 if (this.textAttributes.matchesContainer(lib.notNull(this.cursorNode_)) &&
639 this.cursorNode_.textContent.substr(this.cursorOffset_) ==
640 str) {
rgindaa19afe22012-01-25 15:40:22 -0800641 // This overwrite would be a no-op, just move the cursor and return.
Mike Frysinger6380bed2017-08-24 18:46:39 -0400642 this.cursorOffset_ += wcwidth;
643 this.cursorPosition.column += wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800644 return;
645 }
rginda8ba33642011-12-14 12:31:31 -0800646
Mike Frysinger6380bed2017-08-24 18:46:39 -0400647 this.deleteChars(Math.min(wcwidth, maxLength));
648 this.insertString(str, wcwidth);
rginda8ba33642011-12-14 12:31:31 -0800649};
650
651/**
652 * Forward-delete one or more characters at the current cursor position.
653 *
654 * Text to the right of the deleted characters is shifted left. Only affects
655 * characters on the same row as the cursor.
656 *
Joel Hockey0f933582019-08-27 18:01:51 -0700657 * @param {number} count The column width of characters to delete. This is
Ricky Liang48f05cb2013-12-31 23:35:29 +0800658 * clamped to the column width minus the cursor column.
Joel Hockey0f933582019-08-27 18:01:51 -0700659 * @return {number} The column width of the characters actually deleted.
rginda8ba33642011-12-14 12:31:31 -0800660 */
661hterm.Screen.prototype.deleteChars = function(count) {
662 var node = this.cursorNode_;
663 var offset = this.cursorOffset_;
664
Robert Ginda7fd57082012-09-25 14:41:47 -0700665 var currentCursorColumn = this.cursorPosition.column;
666 count = Math.min(count, this.columnCount_ - currentCursorColumn);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400667 if (!count) {
Robert Ginda7fd57082012-09-25 14:41:47 -0700668 return 0;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400669 }
Robert Ginda7fd57082012-09-25 14:41:47 -0700670
671 var rv = count;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800672 var startLength, endLength;
rgindaa19afe22012-01-25 15:40:22 -0800673
rginda8ba33642011-12-14 12:31:31 -0800674 while (node && count) {
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400675 // Sanity check so we don't loop forever, but we don't also go quietly.
676 if (count < 0) {
677 console.error(`Deleting ${rv} chars went negative: ${count}`);
678 break;
679 }
680
Ricky Liang48f05cb2013-12-31 23:35:29 +0800681 startLength = hterm.TextAttributes.nodeWidth(node);
682 node.textContent = hterm.TextAttributes.nodeSubstr(node, 0, offset) +
683 hterm.TextAttributes.nodeSubstr(node, offset + count);
684 endLength = hterm.TextAttributes.nodeWidth(node);
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400685
686 // Deal with splitting wide characters. There are two ways: we could delete
687 // the first column or the second column. In both cases, we delete the wide
688 // character and replace one of the columns with a space (since the other
689 // was deleted). If there are more chars to delete, the next loop will pick
690 // up the slack.
691 if (node.wcNode && offset < startLength &&
Joel Hockeyd36efd62019-09-30 14:16:20 -0700692 ((endLength && startLength == endLength) ||
693 (!endLength && offset == 1))) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800694 // No characters were deleted when there should be. We're probably trying
695 // to delete one column width from a wide character node. We remove the
696 // wide character node here and replace it with a single space.
697 var spaceNode = this.textAttributes.createContainer(' ');
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400698 node.parentNode.insertBefore(spaceNode, offset ? node : node.nextSibling);
Ricky Liang48f05cb2013-12-31 23:35:29 +0800699 node.textContent = '';
700 endLength = 0;
701 count -= 1;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400702 } else {
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400703 count -= startLength - endLength;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400704 }
rginda8ba33642011-12-14 12:31:31 -0800705
Ricky Liang48f05cb2013-12-31 23:35:29 +0800706 var nextNode = node.nextSibling;
707 if (endLength == 0 && node != this.cursorNode_) {
708 node.parentNode.removeChild(node);
709 }
710 node = nextNode;
rginda8ba33642011-12-14 12:31:31 -0800711 offset = 0;
712 }
Robert Ginda7fd57082012-09-25 14:41:47 -0700713
Ricky Liang48f05cb2013-12-31 23:35:29 +0800714 // Remove this.cursorNode_ if it is an empty non-text node.
Mike Frysinger6a4f2412017-08-31 01:11:25 -0400715 if (this.cursorNode_.nodeType != Node.TEXT_NODE &&
716 !this.cursorNode_.textContent) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800717 var cursorNode = this.cursorNode_;
718 if (cursorNode.previousSibling) {
719 this.cursorNode_ = cursorNode.previousSibling;
720 this.cursorOffset_ = hterm.TextAttributes.nodeWidth(
721 cursorNode.previousSibling);
722 } else if (cursorNode.nextSibling) {
723 this.cursorNode_ = cursorNode.nextSibling;
724 this.cursorOffset_ = 0;
725 } else {
726 var emptyNode = this.cursorRowNode_.ownerDocument.createTextNode('');
727 this.cursorRowNode_.appendChild(emptyNode);
728 this.cursorNode_ = emptyNode;
729 this.cursorOffset_ = 0;
730 }
731 this.cursorRowNode_.removeChild(cursorNode);
732 }
733
Robert Ginda7fd57082012-09-25 14:41:47 -0700734 return rv;
rginda8ba33642011-12-14 12:31:31 -0800735};
John Macinnesfb683832013-07-22 14:46:30 -0400736
737/**
738 * Finds first X-ROW of a line containing specified X-ROW.
739 * Used to support line overflow.
740 *
Joel Hockey0f933582019-08-27 18:01:51 -0700741 * @param {!Node} row X-ROW to begin search for first row of line.
742 * @return {!Node} The X-ROW that is at the beginning of the line.
John Macinnesfb683832013-07-22 14:46:30 -0400743 **/
744hterm.Screen.prototype.getLineStartRow_ = function(row) {
745 while (row.previousSibling &&
746 row.previousSibling.hasAttribute('line-overflow')) {
747 row = row.previousSibling;
748 }
749 return row;
750};
751
752/**
753 * Gets text of a line beginning with row.
754 * Supports line overflow.
755 *
Joel Hockey0f933582019-08-27 18:01:51 -0700756 * @param {!Node} row First X-ROW of line.
John Macinnesfb683832013-07-22 14:46:30 -0400757 * @return {string} Text content of line.
758 **/
759hterm.Screen.prototype.getLineText_ = function(row) {
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700760 let rowText = '';
761 let rowOrNull = row;
762 while (rowOrNull) {
763 rowText += rowOrNull.textContent;
764 if (rowOrNull.hasAttribute('line-overflow')) {
765 rowOrNull = rowOrNull.nextSibling;
John Macinnesfb683832013-07-22 14:46:30 -0400766 } else {
767 break;
768 }
769 }
770 return rowText;
771};
772
773/**
774 * Returns X-ROW that is ancestor of the node.
775 *
Joel Hockey0f933582019-08-27 18:01:51 -0700776 * @param {!Node} node Node to get X-ROW ancestor for.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700777 * @return {?Node} X-ROW ancestor of node, or null if not found.
John Macinnesfb683832013-07-22 14:46:30 -0400778 **/
779hterm.Screen.prototype.getXRowAncestor_ = function(node) {
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700780 let nodeOrNull = node;
781 while (nodeOrNull) {
Mike Frysingerbdb34802020-04-07 03:47:32 -0400782 if (nodeOrNull.nodeName === 'X-ROW') {
John Macinnesfb683832013-07-22 14:46:30 -0400783 break;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400784 }
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700785 nodeOrNull = nodeOrNull.parentNode;
John Macinnesfb683832013-07-22 14:46:30 -0400786 }
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700787 return nodeOrNull;
John Macinnesfb683832013-07-22 14:46:30 -0400788};
789
790/**
791 * Returns position within line of character at offset within node.
792 * Supports line overflow.
793 *
Joel Hockey0f933582019-08-27 18:01:51 -0700794 * @param {!Node} row X-ROW at beginning of line.
795 * @param {!Node} node Node to get position of.
796 * @param {number} offset Offset into node.
797 * @return {number} Position within line of character at offset within node.
John Macinnesfb683832013-07-22 14:46:30 -0400798 **/
799hterm.Screen.prototype.getPositionWithOverflow_ = function(row, node, offset) {
Mike Frysingerbdb34802020-04-07 03:47:32 -0400800 if (!node) {
John Macinnesfb683832013-07-22 14:46:30 -0400801 return -1;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400802 }
John Macinnesfb683832013-07-22 14:46:30 -0400803 var ancestorRow = this.getXRowAncestor_(node);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400804 if (!ancestorRow) {
John Macinnesfb683832013-07-22 14:46:30 -0400805 return -1;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400806 }
John Macinnesfb683832013-07-22 14:46:30 -0400807 var position = 0;
808 while (ancestorRow != row) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800809 position += hterm.TextAttributes.nodeWidth(row);
John Macinnesfb683832013-07-22 14:46:30 -0400810 if (row.hasAttribute('line-overflow') && row.nextSibling) {
811 row = row.nextSibling;
812 } else {
813 return -1;
814 }
815 }
816 return position + this.getPositionWithinRow_(row, node, offset);
817};
818
819/**
820 * Returns position within row of character at offset within node.
821 * Does not support line overflow.
822 *
Joel Hockey0f933582019-08-27 18:01:51 -0700823 * @param {!Node} row X-ROW to get position within.
824 * @param {!Node} node Node to get position for.
825 * @param {number} offset Offset within node to get position for.
826 * @return {number} Position within row of character at offset within node.
John Macinnesfb683832013-07-22 14:46:30 -0400827 **/
828hterm.Screen.prototype.getPositionWithinRow_ = function(row, node, offset) {
829 if (node.parentNode != row) {
Mike Frysinger498192d2017-06-26 18:23:31 -0400830 // If we traversed to the top node, then there's nothing to find here.
Mike Frysingerbdb34802020-04-07 03:47:32 -0400831 if (node.parentNode == null) {
Mike Frysinger498192d2017-06-26 18:23:31 -0400832 return -1;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400833 }
Mike Frysinger498192d2017-06-26 18:23:31 -0400834
John Macinnesfb683832013-07-22 14:46:30 -0400835 return this.getPositionWithinRow_(node.parentNode, node, offset) +
836 this.getPositionWithinRow_(row, node.parentNode, 0);
837 }
838 var position = 0;
839 for (var i = 0; i < row.childNodes.length; i++) {
840 var currentNode = row.childNodes[i];
Mike Frysingerbdb34802020-04-07 03:47:32 -0400841 if (currentNode == node) {
John Macinnesfb683832013-07-22 14:46:30 -0400842 return position + offset;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400843 }
Ricky Liang48f05cb2013-12-31 23:35:29 +0800844 position += hterm.TextAttributes.nodeWidth(currentNode);
John Macinnesfb683832013-07-22 14:46:30 -0400845 }
846 return -1;
847};
848
849/**
850 * Returns the node and offset corresponding to position within line.
851 * Supports line overflow.
852 *
Joel Hockey0f933582019-08-27 18:01:51 -0700853 * @param {!Node} row X-ROW at beginning of line.
854 * @param {number} position Position within line to retrieve node and offset.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700855 * @return {?Array} Two element array containing node and offset respectively.
John Macinnesfb683832013-07-22 14:46:30 -0400856 **/
857hterm.Screen.prototype.getNodeAndOffsetWithOverflow_ = function(row, position) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800858 while (row && position > hterm.TextAttributes.nodeWidth(row)) {
John Macinnesfb683832013-07-22 14:46:30 -0400859 if (row.hasAttribute('line-overflow') && row.nextSibling) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800860 position -= hterm.TextAttributes.nodeWidth(row);
John Macinnesfb683832013-07-22 14:46:30 -0400861 row = row.nextSibling;
862 } else {
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700863 return [null, -1];
John Macinnesfb683832013-07-22 14:46:30 -0400864 }
865 }
866 return this.getNodeAndOffsetWithinRow_(row, position);
867};
868
869/**
870 * Returns the node and offset corresponding to position within row.
871 * Does not support line overflow.
872 *
Joel Hockey0f933582019-08-27 18:01:51 -0700873 * @param {!Node} row X-ROW to get position within.
874 * @param {number} position Position within row to retrieve node and offset.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700875 * @return {?Array} Two element array containing node and offset respectively.
John Macinnesfb683832013-07-22 14:46:30 -0400876 **/
877hterm.Screen.prototype.getNodeAndOffsetWithinRow_ = function(row, position) {
878 for (var i = 0; i < row.childNodes.length; i++) {
879 var node = row.childNodes[i];
Ricky Liang48f05cb2013-12-31 23:35:29 +0800880 var nodeTextWidth = hterm.TextAttributes.nodeWidth(node);
881 if (position <= nodeTextWidth) {
John Macinnesfb683832013-07-22 14:46:30 -0400882 if (node.nodeName === 'SPAN') {
883 /** Drill down to node contained by SPAN. **/
884 return this.getNodeAndOffsetWithinRow_(node, position);
885 } else {
886 return [node, position];
887 }
888 }
Ricky Liang48f05cb2013-12-31 23:35:29 +0800889 position -= nodeTextWidth;
John Macinnesfb683832013-07-22 14:46:30 -0400890 }
891 return null;
892};
893
894/**
895 * Returns the node and offset corresponding to position within line.
896 * Supports line overflow.
897 *
Joel Hockey0f933582019-08-27 18:01:51 -0700898 * @param {!Node} row X-ROW at beginning of line.
899 * @param {number} start Start position of range within line.
900 * @param {number} end End position of range within line.
901 * @param {!Range} range Range to modify.
John Macinnesfb683832013-07-22 14:46:30 -0400902 **/
903hterm.Screen.prototype.setRange_ = function(row, start, end, range) {
904 var startNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, start);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400905 if (startNodeAndOffset == null) {
John Macinnesfb683832013-07-22 14:46:30 -0400906 return;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400907 }
John Macinnesfb683832013-07-22 14:46:30 -0400908 var endNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, end);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400909 if (endNodeAndOffset == null) {
John Macinnesfb683832013-07-22 14:46:30 -0400910 return;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400911 }
John Macinnesfb683832013-07-22 14:46:30 -0400912 range.setStart(startNodeAndOffset[0], startNodeAndOffset[1]);
913 range.setEnd(endNodeAndOffset[0], endNodeAndOffset[1]);
914};
915
916/**
John Lincae9b732018-03-08 13:56:35 +0800917 * Expands selection to surrounding string with word break matches.
John Macinnesfb683832013-07-22 14:46:30 -0400918 *
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700919 * @param {?Selection} selection Selection to expand.
John Lincae9b732018-03-08 13:56:35 +0800920 * @param {string} leftMatch left word break match.
921 * @param {string} rightMatch right word break match.
922 * @param {string} insideMatch inside word break match.
923 */
924hterm.Screen.prototype.expandSelectionWithWordBreakMatches_ =
925 function(selection, leftMatch, rightMatch, insideMatch) {
Mike Frysingerbdb34802020-04-07 03:47:32 -0400926 if (!selection) {
John Macinnesfb683832013-07-22 14:46:30 -0400927 return;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400928 }
John Macinnesfb683832013-07-22 14:46:30 -0400929
930 var range = selection.getRangeAt(0);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400931 if (!range || range.toString().match(/\s/)) {
John Macinnesfb683832013-07-22 14:46:30 -0400932 return;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400933 }
John Macinnesfb683832013-07-22 14:46:30 -0400934
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700935 const rowElement = this.getXRowAncestor_(lib.notNull(range.startContainer));
Mike Frysingerbdb34802020-04-07 03:47:32 -0400936 if (!rowElement) {
Raymes Khoury334625a2018-06-25 10:29:40 +1000937 return;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400938 }
Raymes Khoury334625a2018-06-25 10:29:40 +1000939 const row = this.getLineStartRow_(rowElement);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400940 if (!row) {
John Macinnesfb683832013-07-22 14:46:30 -0400941 return;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400942 }
John Macinnesfb683832013-07-22 14:46:30 -0400943
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700944 var startPosition = this.getPositionWithOverflow_(
945 row, lib.notNull(range.startContainer), range.startOffset);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400946 if (startPosition == -1) {
John Macinnesfb683832013-07-22 14:46:30 -0400947 return;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400948 }
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700949 var endPosition = this.getPositionWithOverflow_(
950 row, lib.notNull(range.endContainer), range.endOffset);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400951 if (endPosition == -1) {
John Macinnesfb683832013-07-22 14:46:30 -0400952 return;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400953 }
John Macinnesfb683832013-07-22 14:46:30 -0400954
Mike Frysinger390878a2020-04-07 20:04:51 -0400955 // Move start to the left.
John Macinnesfb683832013-07-22 14:46:30 -0400956 var rowText = this.getLineText_(row);
Ricky Liang48f05cb2013-12-31 23:35:29 +0800957 var lineUpToRange = lib.wc.substring(rowText, 0, endPosition);
Mike Frysingerd6e0d432019-12-02 04:54:41 -0500958 var leftRegularExpression = new RegExp(leftMatch + insideMatch + '$');
John Macinnesfb683832013-07-22 14:46:30 -0400959 var expandedStart = lineUpToRange.search(leftRegularExpression);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400960 if (expandedStart == -1 || expandedStart > startPosition) {
John Macinnesfb683832013-07-22 14:46:30 -0400961 return;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400962 }
John Macinnesfb683832013-07-22 14:46:30 -0400963
Mike Frysinger390878a2020-04-07 20:04:51 -0400964 // Move end to the right.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800965 var lineFromRange = lib.wc.substring(rowText, startPosition,
966 lib.wc.strWidth(rowText));
Mike Frysingerd6e0d432019-12-02 04:54:41 -0500967 var rightRegularExpression = new RegExp('^' + insideMatch + rightMatch);
John Macinnesfb683832013-07-22 14:46:30 -0400968 var found = lineFromRange.match(rightRegularExpression);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400969 if (!found) {
John Macinnesfb683832013-07-22 14:46:30 -0400970 return;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400971 }
Ricky Liang48f05cb2013-12-31 23:35:29 +0800972 var expandedEnd = startPosition + lib.wc.strWidth(found[0]);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400973 if (expandedEnd == -1 || expandedEnd < endPosition) {
John Macinnesfb683832013-07-22 14:46:30 -0400974 return;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400975 }
John Macinnesfb683832013-07-22 14:46:30 -0400976
977 this.setRange_(row, expandedStart, expandedEnd, range);
978 selection.addRange(range);
979};
Mike Frysingera2cacaa2017-11-29 13:51:09 -0800980
981/**
John Lincae9b732018-03-08 13:56:35 +0800982 * Expands selection to surrounding string using the user's settings.
983 *
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700984 * @param {?Selection} selection Selection to expand.
John Lincae9b732018-03-08 13:56:35 +0800985 */
986hterm.Screen.prototype.expandSelection = function(selection) {
987 this.expandSelectionWithWordBreakMatches_(
988 selection,
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700989 lib.notNull(this.wordBreakMatchLeft),
990 lib.notNull(this.wordBreakMatchRight),
991 lib.notNull(this.wordBreakMatchMiddle));
Mike Frysinger8416e0a2017-05-17 09:09:46 -0400992};
John Lincae9b732018-03-08 13:56:35 +0800993
994/**
995 * Expands selection to surrounding URL using a set of fixed match settings.
996 *
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700997 * @param {?Selection} selection Selection to expand.
John Lincae9b732018-03-08 13:56:35 +0800998 */
999hterm.Screen.prototype.expandSelectionForUrl = function(selection) {
1000 this.expandSelectionWithWordBreakMatches_(
1001 selection,
Mike Frysinger1a1a1802020-01-29 21:38:55 -05001002 '[^\\s[\\](){}<>"\'^!@#$%&*,;:`\u{2018}\u{201c}\u{2039}\u{ab}]',
1003 '[^\\s[\\](){}<>"\'^!@#$%&*,;:~.`\u{2019}\u{201d}\u{203a}\u{bb}]',
Mike Frysinger9e11e492020-01-06 14:29:57 +05451004 '[^\\s[\\](){}<>"\'^]*');
John Lincae9b732018-03-08 13:56:35 +08001005};
1006
1007/**
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001008 * Save the current cursor state to the corresponding screens.
1009 *
Joel Hockey0f933582019-08-27 18:01:51 -07001010 * @param {!hterm.VT} vt The VT object to read graphic codeset details from.
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001011 */
1012hterm.Screen.prototype.saveCursorAndState = function(vt) {
1013 this.cursorState_.save(vt);
1014};
1015
1016/**
1017 * Restore the saved cursor state in the corresponding screens.
1018 *
Joel Hockey0f933582019-08-27 18:01:51 -07001019 * @param {!hterm.VT} vt The VT object to write graphic codeset details to.
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001020 */
1021hterm.Screen.prototype.restoreCursorAndState = function(vt) {
1022 this.cursorState_.restore(vt);
1023};
1024
1025/**
1026 * Track all the things related to the current "cursor".
1027 *
1028 * The set of things saved & restored here is defined by DEC:
1029 * https://vt100.net/docs/vt510-rm/DECSC.html
1030 * - Cursor position
1031 * - Character attributes set by the SGR command
1032 * - Character sets (G0, G1, G2, or G3) currently in GL and GR
1033 * - Wrap flag (autowrap or no autowrap)
1034 * - State of origin mode (DECOM)
1035 * - Selective erase attribute
1036 * - Any single shift 2 (SS2) or single shift 3 (SS3) functions sent
1037 *
1038 * These are done on a per-screen basis.
Joel Hockey0f933582019-08-27 18:01:51 -07001039 *
1040 * @param {!hterm.Screen} screen The screen this cursor is tied to.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -07001041 * @constructor
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001042 */
1043hterm.Screen.CursorState = function(screen) {
1044 this.screen_ = screen;
1045 this.cursor = null;
1046 this.textAttributes = null;
1047 this.GL = this.GR = this.G0 = this.G1 = this.G2 = this.G3 = null;
1048};
1049
1050/**
1051 * Save all the cursor state.
1052 *
Joel Hockey0f933582019-08-27 18:01:51 -07001053 * @param {!hterm.VT} vt The VT object to read graphic codeset details from.
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001054 */
1055hterm.Screen.CursorState.prototype.save = function(vt) {
1056 this.cursor = vt.terminal.saveCursor();
1057
1058 this.textAttributes = this.screen_.textAttributes.clone();
1059
1060 this.GL = vt.GL;
1061 this.GR = vt.GR;
1062
1063 this.G0 = vt.G0;
1064 this.G1 = vt.G1;
1065 this.G2 = vt.G2;
1066 this.G3 = vt.G3;
1067};
1068
1069/**
1070 * Restore the previously saved cursor state.
1071 *
Joel Hockey0f933582019-08-27 18:01:51 -07001072 * @param {!hterm.VT} vt The VT object to write graphic codeset details to.
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001073 */
1074hterm.Screen.CursorState.prototype.restore = function(vt) {
1075 vt.terminal.restoreCursor(this.cursor);
1076
1077 // Cursor restore includes char attributes (bold/etc...), but does not change
1078 // the color palette (which are a terminal setting).
1079 const tattrs = this.textAttributes.clone();
Joel Hockey42dba8f2020-03-26 16:21:11 -07001080 tattrs.colorPaletteOverrides =
1081 this.screen_.textAttributes.colorPaletteOverrides;
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001082 tattrs.syncColors();
1083
1084 this.screen_.textAttributes = tattrs;
1085
1086 vt.GL = this.GL;
1087 vt.GR = this.GR;
1088
1089 vt.G0 = this.G0;
1090 vt.G1 = this.G1;
1091 vt.G2 = this.G2;
1092 vt.G3 = this.G3;
1093};