blob: dc696354dae978967c4d20f75c06239b1c020800 [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.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -070051 * @type {!Array<!Element>}
rginda8ba33642011-12-14 12:31:31 -080052 */
53 this.rowsArray = [];
54
55 // The max column width for this screen.
Mike Frysinger60a156d2019-06-13 10:15:45 -040056 this.columnCount_ = columnCount;
rginda8ba33642011-12-14 12:31:31 -080057
rgindaa19afe22012-01-25 15:40:22 -080058 // The current color, bold, underline and blink attributes.
59 this.textAttributes = new hterm.TextAttributes(window.document);
60
rginda87b86462011-12-14 13:48:03 -080061 // Current zero-based cursor coordinates.
62 this.cursorPosition = new hterm.RowCol(0, 0);
rginda8ba33642011-12-14 12:31:31 -080063
Mike Frysingera2cacaa2017-11-29 13:51:09 -080064 // Saved state used by DECSC and related settings. This is only for saving
65 // and restoring specific state, not for the current/active state.
66 this.cursorState_ = new hterm.Screen.CursorState(this);
67
rginda8ba33642011-12-14 12:31:31 -080068 // The node containing the row that the cursor is positioned on.
69 this.cursorRowNode_ = null;
70
71 // The node containing the span of text that the cursor is positioned on.
72 this.cursorNode_ = null;
73
Ricky Liang48f05cb2013-12-31 23:35:29 +080074 // The offset in column width into cursorNode_ where the cursor is positioned.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -070075 this.cursorOffset_ = 0;
Mike Frysinger664e9992017-05-19 01:24:24 -040076
77 // Regexes for expanding word selections.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -070078 /** @type {?string} */
Mike Frysinger664e9992017-05-19 01:24:24 -040079 this.wordBreakMatchLeft = null;
Joel Hockeyadd2f7e2019-09-20 16:37:35 -070080 /** @type {?string} */
Mike Frysinger664e9992017-05-19 01:24:24 -040081 this.wordBreakMatchRight = null;
Joel Hockeyadd2f7e2019-09-20 16:37:35 -070082 /** @type {?string} */
Mike Frysinger664e9992017-05-19 01:24:24 -040083 this.wordBreakMatchMiddle = null;
rginda8ba33642011-12-14 12:31:31 -080084};
85
86/**
87 * Return the screen size as an hterm.Size object.
88 *
Joel Hockey0f933582019-08-27 18:01:51 -070089 * @return {!hterm.Size} hterm.Size object representing the current number
rginda8ba33642011-12-14 12:31:31 -080090 * of rows and columns in this screen.
91 */
92hterm.Screen.prototype.getSize = function() {
93 return new hterm.Size(this.columnCount_, this.rowsArray.length);
94};
95
96/**
97 * Return the current number of rows in this screen.
98 *
Joel Hockey0f933582019-08-27 18:01:51 -070099 * @return {number} The number of rows in this screen.
rginda8ba33642011-12-14 12:31:31 -0800100 */
101hterm.Screen.prototype.getHeight = function() {
102 return this.rowsArray.length;
103};
104
105/**
106 * Return the current number of columns in this screen.
107 *
Joel Hockey0f933582019-08-27 18:01:51 -0700108 * @return {number} The number of columns in this screen.
rginda8ba33642011-12-14 12:31:31 -0800109 */
110hterm.Screen.prototype.getWidth = function() {
111 return this.columnCount_;
112};
113
114/**
115 * Set the maximum number of columns per row.
116 *
Joel Hockey0f933582019-08-27 18:01:51 -0700117 * @param {number} count The maximum number of columns per row.
rginda8ba33642011-12-14 12:31:31 -0800118 */
119hterm.Screen.prototype.setColumnCount = function(count) {
rginda2312fff2012-01-05 16:20:52 -0800120 this.columnCount_ = count;
121
rgindacbbd7482012-06-13 15:06:16 -0700122 if (this.cursorPosition.column >= count)
123 this.setCursorPosition(this.cursorPosition.row, count - 1);
rginda8ba33642011-12-14 12:31:31 -0800124};
125
126/**
127 * Remove the first row from the screen and return it.
128 *
Joel Hockey0f933582019-08-27 18:01:51 -0700129 * @return {!Element} The first row in this screen.
rginda8ba33642011-12-14 12:31:31 -0800130 */
131hterm.Screen.prototype.shiftRow = function() {
132 return this.shiftRows(1)[0];
rginda87b86462011-12-14 13:48:03 -0800133};
rginda8ba33642011-12-14 12:31:31 -0800134
135/**
136 * Remove rows from the top of the screen and return them as an array.
137 *
Joel Hockey0f933582019-08-27 18:01:51 -0700138 * @param {number} count The number of rows to remove.
139 * @return {!Array<!Element>} The selected rows.
rginda8ba33642011-12-14 12:31:31 -0800140 */
141hterm.Screen.prototype.shiftRows = function(count) {
142 return this.rowsArray.splice(0, count);
143};
144
145/**
146 * Insert a row at the top of the screen.
147 *
Joel Hockey0f933582019-08-27 18:01:51 -0700148 * @param {!Element} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800149 */
150hterm.Screen.prototype.unshiftRow = function(row) {
151 this.rowsArray.splice(0, 0, row);
152};
153
154/**
155 * Insert rows at the top of the screen.
156 *
Joel Hockey0f933582019-08-27 18:01:51 -0700157 * @param {!Array<!Element>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800158 */
159hterm.Screen.prototype.unshiftRows = function(rows) {
160 this.rowsArray.unshift.apply(this.rowsArray, rows);
161};
162
163/**
164 * Remove the last row from the screen and return it.
165 *
Joel Hockey0f933582019-08-27 18:01:51 -0700166 * @return {!Element} The last row in this screen.
rginda8ba33642011-12-14 12:31:31 -0800167 */
168hterm.Screen.prototype.popRow = function() {
169 return this.popRows(1)[0];
170};
171
172/**
173 * Remove rows from the bottom of the screen and return them as an array.
174 *
Joel Hockey0f933582019-08-27 18:01:51 -0700175 * @param {number} count The number of rows to remove.
176 * @return {!Array<!Element>} The selected rows.
rginda8ba33642011-12-14 12:31:31 -0800177 */
178hterm.Screen.prototype.popRows = function(count) {
179 return this.rowsArray.splice(this.rowsArray.length - count, count);
180};
181
182/**
183 * Insert a row at the bottom of the screen.
184 *
Joel Hockey0f933582019-08-27 18:01:51 -0700185 * @param {!Element} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800186 */
187hterm.Screen.prototype.pushRow = function(row) {
188 this.rowsArray.push(row);
189};
190
191/**
192 * Insert rows at the bottom of the screen.
193 *
Joel Hockey0f933582019-08-27 18:01:51 -0700194 * @param {!Array<!Element>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800195 */
196hterm.Screen.prototype.pushRows = function(rows) {
197 rows.push.apply(this.rowsArray, rows);
198};
199
200/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500201 * Insert a row at the specified row of the screen.
rginda8ba33642011-12-14 12:31:31 -0800202 *
Joel Hockey0f933582019-08-27 18:01:51 -0700203 * @param {number} index The index to insert the row.
204 * @param {!Element} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800205 */
206hterm.Screen.prototype.insertRow = function(index, row) {
207 this.rowsArray.splice(index, 0, row);
208};
209
210/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500211 * Insert rows at the specified row of the screen.
rginda8ba33642011-12-14 12:31:31 -0800212 *
Joel Hockey0f933582019-08-27 18:01:51 -0700213 * @param {number} index The index to insert the rows.
214 * @param {!Array<!Element>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800215 */
216hterm.Screen.prototype.insertRows = function(index, rows) {
217 for (var i = 0; i < rows.length; i++) {
218 this.rowsArray.splice(index + i, 0, rows[i]);
219 }
220};
221
222/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500223 * Remove a row from the screen and return it.
rginda8ba33642011-12-14 12:31:31 -0800224 *
Joel Hockey0f933582019-08-27 18:01:51 -0700225 * @param {number} index The index of the row to remove.
226 * @return {!Element} The selected row.
rginda8ba33642011-12-14 12:31:31 -0800227 */
228hterm.Screen.prototype.removeRow = function(index) {
229 return this.rowsArray.splice(index, 1)[0];
230};
231
232/**
233 * Remove rows from the bottom of the screen and return them as an array.
234 *
Joel Hockey0f933582019-08-27 18:01:51 -0700235 * @param {number} index The index to start removing rows.
236 * @param {number} count The number of rows to remove.
237 * @return {!Array<!Element>} The selected rows.
rginda8ba33642011-12-14 12:31:31 -0800238 */
239hterm.Screen.prototype.removeRows = function(index, count) {
240 return this.rowsArray.splice(index, count);
241};
242
243/**
244 * Invalidate the current cursor position.
245 *
rginda87b86462011-12-14 13:48:03 -0800246 * This sets this.cursorPosition to (0, 0) and clears out some internal
rginda8ba33642011-12-14 12:31:31 -0800247 * data.
248 *
249 * Attempting to insert or overwrite text while the cursor position is invalid
250 * will raise an obscure exception.
251 */
252hterm.Screen.prototype.invalidateCursorPosition = function() {
rginda87b86462011-12-14 13:48:03 -0800253 this.cursorPosition.move(0, 0);
rginda8ba33642011-12-14 12:31:31 -0800254 this.cursorRowNode_ = null;
255 this.cursorNode_ = null;
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700256 this.cursorOffset_ = 0;
rginda8ba33642011-12-14 12:31:31 -0800257};
258
259/**
rginda8ba33642011-12-14 12:31:31 -0800260 * Clear the contents of the cursor row.
rginda8ba33642011-12-14 12:31:31 -0800261 */
262hterm.Screen.prototype.clearCursorRow = function() {
263 this.cursorRowNode_.innerHTML = '';
rgindaa09e7332012-08-17 12:49:51 -0700264 this.cursorRowNode_.removeAttribute('line-overflow');
rginda8ba33642011-12-14 12:31:31 -0800265 this.cursorOffset_ = 0;
rginda8ba33642011-12-14 12:31:31 -0800266 this.cursorPosition.column = 0;
rginda2312fff2012-01-05 16:20:52 -0800267 this.cursorPosition.overflow = false;
Robert Ginda7fd57082012-09-25 14:41:47 -0700268
269 var text;
270 if (this.textAttributes.isDefault()) {
271 text = '';
272 } else {
Mike Frysinger73e56462019-07-17 00:23:46 -0500273 text = ' '.repeat(this.columnCount_);
Robert Ginda7fd57082012-09-25 14:41:47 -0700274 }
275
Zhu Qunying30d40712017-03-14 16:27:00 -0700276 // We shouldn't honor inverse colors when clearing an area, to match
277 // xterm's back color erase behavior.
Edoardo Spadolini2fd43642014-08-23 22:59:57 +0200278 var inverse = this.textAttributes.inverse;
279 this.textAttributes.inverse = false;
280 this.textAttributes.syncColors();
281
Robert Ginda7fd57082012-09-25 14:41:47 -0700282 var node = this.textAttributes.createContainer(text);
283 this.cursorRowNode_.appendChild(node);
284 this.cursorNode_ = node;
Edoardo Spadolini2fd43642014-08-23 22:59:57 +0200285
286 this.textAttributes.inverse = inverse;
287 this.textAttributes.syncColors();
rginda8ba33642011-12-14 12:31:31 -0800288};
289
290/**
rgindaa09e7332012-08-17 12:49:51 -0700291 * Mark the current row as having overflowed to the next line.
292 *
293 * The line overflow state is used when converting a range of rows into text.
294 * It makes it possible to recombine two or more overflow terminal rows into
295 * a single line.
296 *
297 * This is distinct from the cursor being in the overflow state. Cursor
298 * overflow indicates that printing at the cursor position will commit a
299 * line overflow, unless it is preceded by a repositioning of the cursor
300 * to a non-overflow state.
301 */
302hterm.Screen.prototype.commitLineOverflow = function() {
303 this.cursorRowNode_.setAttribute('line-overflow', true);
304};
305
306/**
rginda8ba33642011-12-14 12:31:31 -0800307 * Relocate the cursor to a give row and column.
308 *
Joel Hockey0f933582019-08-27 18:01:51 -0700309 * @param {number} row The zero based row.
310 * @param {number} column The zero based column.
rginda8ba33642011-12-14 12:31:31 -0800311 */
312hterm.Screen.prototype.setCursorPosition = function(row, column) {
rginda11057d52012-04-25 12:29:56 -0700313 if (!this.rowsArray.length) {
314 console.warn('Attempt to set cursor position on empty screen.');
315 return;
316 }
317
rginda87b86462011-12-14 13:48:03 -0800318 if (row >= this.rowsArray.length) {
rgindacbbd7482012-06-13 15:06:16 -0700319 console.error('Row out of bounds: ' + row);
rginda87b86462011-12-14 13:48:03 -0800320 row = this.rowsArray.length - 1;
321 } else if (row < 0) {
rgindacbbd7482012-06-13 15:06:16 -0700322 console.error('Row out of bounds: ' + row);
rginda87b86462011-12-14 13:48:03 -0800323 row = 0;
324 }
325
326 if (column >= this.columnCount_) {
rgindacbbd7482012-06-13 15:06:16 -0700327 console.error('Column out of bounds: ' + column);
rginda87b86462011-12-14 13:48:03 -0800328 column = this.columnCount_ - 1;
329 } else if (column < 0) {
rgindacbbd7482012-06-13 15:06:16 -0700330 console.error('Column out of bounds: ' + column);
rginda87b86462011-12-14 13:48:03 -0800331 column = 0;
332 }
rginda8ba33642011-12-14 12:31:31 -0800333
rginda2312fff2012-01-05 16:20:52 -0800334 this.cursorPosition.overflow = false;
335
rginda8ba33642011-12-14 12:31:31 -0800336 var rowNode = this.rowsArray[row];
337 var node = rowNode.firstChild;
338
339 if (!node) {
340 node = rowNode.ownerDocument.createTextNode('');
341 rowNode.appendChild(node);
342 }
343
rgindaa19afe22012-01-25 15:40:22 -0800344 var currentColumn = 0;
345
rginda8ba33642011-12-14 12:31:31 -0800346 if (rowNode == this.cursorRowNode_) {
347 if (column >= this.cursorPosition.column - this.cursorOffset_) {
348 node = this.cursorNode_;
349 currentColumn = this.cursorPosition.column - this.cursorOffset_;
350 }
351 } else {
352 this.cursorRowNode_ = rowNode;
353 }
354
355 this.cursorPosition.move(row, column);
356
357 while (node) {
358 var offset = column - currentColumn;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800359 var width = hterm.TextAttributes.nodeWidth(node);
360 if (!node.nextSibling || width > offset) {
rginda8ba33642011-12-14 12:31:31 -0800361 this.cursorNode_ = node;
362 this.cursorOffset_ = offset;
363 return;
364 }
365
Ricky Liang48f05cb2013-12-31 23:35:29 +0800366 currentColumn += width;
rginda8ba33642011-12-14 12:31:31 -0800367 node = node.nextSibling;
368 }
369};
370
371/**
rginda87b86462011-12-14 13:48:03 -0800372 * Set the provided selection object to be a caret selection at the current
373 * cursor position.
Joel Hockey0f933582019-08-27 18:01:51 -0700374 *
375 * @param {!Selection} selection
rginda87b86462011-12-14 13:48:03 -0800376 */
377hterm.Screen.prototype.syncSelectionCaret = function(selection) {
Rob Spies06533ba2014-04-24 11:20:37 -0700378 try {
379 selection.collapse(this.cursorNode_, this.cursorOffset_);
380 } catch (firefoxIgnoredException) {
381 // FF can throw an exception if the range is off, rather than just not
382 // performing the collapse.
383 }
rginda87b86462011-12-14 13:48:03 -0800384};
385
386/**
rgindaa19afe22012-01-25 15:40:22 -0800387 * Split a single node into two nodes at the given offset.
rginda8ba33642011-12-14 12:31:31 -0800388 *
rgindaa19afe22012-01-25 15:40:22 -0800389 * For example:
390 * Given the DOM fragment '<div><span>Hello World</span></div>', call splitNode_
Zhu Qunying30d40712017-03-14 16:27:00 -0700391 * passing the span and an offset of 6. This would modify the fragment to
rgindaa19afe22012-01-25 15:40:22 -0800392 * become: '<div><span>Hello </span><span>World</span></div>'. If the span
393 * had any attributes they would have been copied to the new span as well.
394 *
395 * The to-be-split node must have a container, so that the new node can be
396 * placed next to it.
397 *
Joel Hockey0f933582019-08-27 18:01:51 -0700398 * @param {!Node} node The node to split.
399 * @param {number} offset The offset into the node where the split should
rgindaa19afe22012-01-25 15:40:22 -0800400 * occur.
rginda8ba33642011-12-14 12:31:31 -0800401 */
rgindaa19afe22012-01-25 15:40:22 -0800402hterm.Screen.prototype.splitNode_ = function(node, offset) {
rginda35c456b2012-02-09 17:29:05 -0800403 var afterNode = node.cloneNode(false);
rgindaa19afe22012-01-25 15:40:22 -0800404
rginda35c456b2012-02-09 17:29:05 -0800405 var textContent = node.textContent;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800406 node.textContent = hterm.TextAttributes.nodeSubstr(node, 0, offset);
407 afterNode.textContent = lib.wc.substr(textContent, offset);
rgindaa19afe22012-01-25 15:40:22 -0800408
Ricky Liang48f05cb2013-12-31 23:35:29 +0800409 if (afterNode.textContent)
410 node.parentNode.insertBefore(afterNode, node.nextSibling);
411 if (!node.textContent)
412 node.parentNode.removeChild(node);
rginda8ba33642011-12-14 12:31:31 -0800413};
414
415/**
rgindaa9abdd82012-08-06 18:05:09 -0700416 * Ensure that text is clipped and the cursor is clamped to the column count.
rgindaa19afe22012-01-25 15:40:22 -0800417 */
rgindaa9abdd82012-08-06 18:05:09 -0700418hterm.Screen.prototype.maybeClipCurrentRow = function() {
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700419 var width = hterm.TextAttributes.nodeWidth(lib.notNull(this.cursorRowNode_));
Ricky Liang48f05cb2013-12-31 23:35:29 +0800420
421 if (width <= this.columnCount_) {
rgindaa9abdd82012-08-06 18:05:09 -0700422 // Current row does not need clipping, but may need clamping.
423 if (this.cursorPosition.column >= this.columnCount_) {
424 this.setCursorPosition(this.cursorPosition.row, this.columnCount_ - 1);
425 this.cursorPosition.overflow = true;
426 }
rgindaa19afe22012-01-25 15:40:22 -0800427
rgindaa9abdd82012-08-06 18:05:09 -0700428 return;
429 }
430
431 // Save off the current column so we can maybe restore it later.
432 var currentColumn = this.cursorPosition.column;
433
434 // Move the cursor to the final column.
435 this.setCursorPosition(this.cursorPosition.row, this.columnCount_ - 1);
436
437 // Remove any text that partially overflows.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700438 width = hterm.TextAttributes.nodeWidth(lib.notNull(this.cursorNode_));
Ricky Liang48f05cb2013-12-31 23:35:29 +0800439
440 if (this.cursorOffset_ < width - 1) {
441 this.cursorNode_.textContent = hterm.TextAttributes.nodeSubstr(
442 this.cursorNode_, 0, this.cursorOffset_ + 1);
rgindaa9abdd82012-08-06 18:05:09 -0700443 }
444
445 // Remove all nodes after the cursor.
rgindaa19afe22012-01-25 15:40:22 -0800446 var rowNode = this.cursorRowNode_;
447 var node = this.cursorNode_.nextSibling;
448
449 while (node) {
rgindaa19afe22012-01-25 15:40:22 -0800450 rowNode.removeChild(node);
451 node = this.cursorNode_.nextSibling;
452 }
453
Robert Ginda7fd57082012-09-25 14:41:47 -0700454 if (currentColumn < this.columnCount_) {
rgindaa9abdd82012-08-06 18:05:09 -0700455 // If the cursor was within the screen before we started then restore its
456 // position.
rgindaa19afe22012-01-25 15:40:22 -0800457 this.setCursorPosition(this.cursorPosition.row, currentColumn);
rgindaa9abdd82012-08-06 18:05:09 -0700458 } else {
459 // Otherwise leave it at the the last column in the overflow state.
460 this.cursorPosition.overflow = true;
rgindaa19afe22012-01-25 15:40:22 -0800461 }
rgindaa19afe22012-01-25 15:40:22 -0800462};
463
464/**
465 * Insert a string at the current character position using the current
466 * text attributes.
467 *
rgindaa09e7332012-08-17 12:49:51 -0700468 * You must call maybeClipCurrentRow() after in order to clip overflowed
469 * text and clamp the cursor.
470 *
471 * It is also up to the caller to properly maintain the line overflow state
472 * using hterm.Screen..commitLineOverflow().
Joel Hockey0f933582019-08-27 18:01:51 -0700473 *
474 * @param {string} str The string to insert.
475 * @param {number=} wcwidth The cached lib.wc.strWidth value for |str|. Will be
476 * calculated on demand if need be. Passing in a cached value helps speed
477 * up processing as this is a hot codepath.
rginda8ba33642011-12-14 12:31:31 -0800478 */
Mike Frysinger6380bed2017-08-24 18:46:39 -0400479hterm.Screen.prototype.insertString = function(str, wcwidth=undefined) {
rgindaa19afe22012-01-25 15:40:22 -0800480 var cursorNode = this.cursorNode_;
481 var cursorNodeText = cursorNode.textContent;
rginda8ba33642011-12-14 12:31:31 -0800482
Robert Gindaa21dfb32013-10-31 14:17:45 -0700483 this.cursorRowNode_.removeAttribute('line-overflow');
484
Ricky Liang48f05cb2013-12-31 23:35:29 +0800485 // We may alter the width of the string by prepending some missing
486 // whitespaces, so we need to record the string width ahead of time.
Mike Frysinger6380bed2017-08-24 18:46:39 -0400487 if (wcwidth === undefined)
488 wcwidth = lib.wc.strWidth(str);
rginda8ba33642011-12-14 12:31:31 -0800489
rgindaa19afe22012-01-25 15:40:22 -0800490 // No matter what, before this function exits the cursor column will have
491 // moved this much.
Mike Frysinger6380bed2017-08-24 18:46:39 -0400492 this.cursorPosition.column += wcwidth;
rginda8ba33642011-12-14 12:31:31 -0800493
rgindaa19afe22012-01-25 15:40:22 -0800494 // Local cache of the cursor offset.
495 var offset = this.cursorOffset_;
rginda8ba33642011-12-14 12:31:31 -0800496
rgindaa19afe22012-01-25 15:40:22 -0800497 // Reverse offset is the offset measured from the end of the string.
498 // Zero implies that the cursor is at the end of the cursor node.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800499 var reverseOffset = hterm.TextAttributes.nodeWidth(cursorNode) - offset;
rgindaa19afe22012-01-25 15:40:22 -0800500
501 if (reverseOffset < 0) {
502 // A negative reverse offset means the cursor is positioned past the end
503 // of the characters on this line. We'll need to insert the missing
504 // whitespace.
Mike Frysinger73e56462019-07-17 00:23:46 -0500505 const ws = ' '.repeat(-reverseOffset);
rgindaa19afe22012-01-25 15:40:22 -0800506
Brad Town7de83302015-03-12 02:10:32 -0700507 // This whitespace should be completely unstyled. Underline, background
508 // color, and strikethrough would be visible on whitespace, so we can't use
509 // one of those spans to hold the text.
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200510 if (!(this.textAttributes.underline ||
Brad Town7de83302015-03-12 02:10:32 -0700511 this.textAttributes.strikethrough ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200512 this.textAttributes.background ||
513 this.textAttributes.wcNode ||
Mike Frysinger1e98c0f2017-08-15 01:21:31 -0400514 !this.textAttributes.asciiNode ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200515 this.textAttributes.tileData != null)) {
rgindaa19afe22012-01-25 15:40:22 -0800516 // Best case scenario, we can just pretend the spaces were part of the
517 // original string.
518 str = ws + str;
Mike Frysinger6a4f2412017-08-31 01:11:25 -0400519 } else if (cursorNode.nodeType == Node.TEXT_NODE ||
Ricky Liang48f05cb2013-12-31 23:35:29 +0800520 !(cursorNode.wcNode ||
Mike Frysinger1e98c0f2017-08-15 01:21:31 -0400521 !cursorNode.asciiNode ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200522 cursorNode.tileNode ||
Ricky Liang48f05cb2013-12-31 23:35:29 +0800523 cursorNode.style.textDecoration ||
Mike Frysinger09c54f42017-12-15 01:12:30 -0500524 cursorNode.style.textDecorationStyle ||
525 cursorNode.style.textDecorationLine ||
rgindaa19afe22012-01-25 15:40:22 -0800526 cursorNode.style.backgroundColor)) {
527 // Second best case, the current node is able to hold the whitespace.
528 cursorNode.textContent = (cursorNodeText += ws);
529 } else {
530 // Worst case, we have to create a new node to hold the whitespace.
531 var wsNode = cursorNode.ownerDocument.createTextNode(ws);
532 this.cursorRowNode_.insertBefore(wsNode, cursorNode.nextSibling);
533 this.cursorNode_ = cursorNode = wsNode;
534 this.cursorOffset_ = offset = -reverseOffset;
535 cursorNodeText = ws;
536 }
537
538 // We now know for sure that we're at the last character of the cursor node.
539 reverseOffset = 0;
rginda8ba33642011-12-14 12:31:31 -0800540 }
541
rgindaa19afe22012-01-25 15:40:22 -0800542 if (this.textAttributes.matchesContainer(cursorNode)) {
543 // The new text can be placed directly in the cursor node.
544 if (reverseOffset == 0) {
545 cursorNode.textContent = cursorNodeText + str;
546 } else if (offset == 0) {
547 cursorNode.textContent = str + cursorNodeText;
548 } else {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800549 cursorNode.textContent =
550 hterm.TextAttributes.nodeSubstr(cursorNode, 0, offset) +
551 str + hterm.TextAttributes.nodeSubstr(cursorNode, offset);
rgindaa19afe22012-01-25 15:40:22 -0800552 }
rginda8ba33642011-12-14 12:31:31 -0800553
Mike Frysinger6380bed2017-08-24 18:46:39 -0400554 this.cursorOffset_ += wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800555 return;
rginda87b86462011-12-14 13:48:03 -0800556 }
557
rgindaa19afe22012-01-25 15:40:22 -0800558 // The cursor node is the wrong style for the new text. If we're at the
559 // beginning or end of the cursor node, then the adjacent node is also a
560 // potential candidate.
rginda8ba33642011-12-14 12:31:31 -0800561
rgindaa19afe22012-01-25 15:40:22 -0800562 if (offset == 0) {
563 // At the beginning of the cursor node, the check the previous sibling.
564 var previousSibling = cursorNode.previousSibling;
565 if (previousSibling &&
566 this.textAttributes.matchesContainer(previousSibling)) {
567 previousSibling.textContent += str;
568 this.cursorNode_ = previousSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800569 this.cursorOffset_ = lib.wc.strWidth(previousSibling.textContent);
rgindaa19afe22012-01-25 15:40:22 -0800570 return;
571 }
572
573 var newNode = this.textAttributes.createContainer(str);
574 this.cursorRowNode_.insertBefore(newNode, cursorNode);
575 this.cursorNode_ = newNode;
Mike Frysinger6380bed2017-08-24 18:46:39 -0400576 this.cursorOffset_ = wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800577 return;
578 }
579
580 if (reverseOffset == 0) {
581 // At the end of the cursor node, the check the next sibling.
582 var nextSibling = cursorNode.nextSibling;
583 if (nextSibling &&
584 this.textAttributes.matchesContainer(nextSibling)) {
585 nextSibling.textContent = str + nextSibling.textContent;
586 this.cursorNode_ = nextSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800587 this.cursorOffset_ = lib.wc.strWidth(str);
rgindaa19afe22012-01-25 15:40:22 -0800588 return;
589 }
590
591 var newNode = this.textAttributes.createContainer(str);
592 this.cursorRowNode_.insertBefore(newNode, nextSibling);
593 this.cursorNode_ = newNode;
594 // We specifically need to include any missing whitespace here, since it's
595 // going in a new node.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800596 this.cursorOffset_ = hterm.TextAttributes.nodeWidth(newNode);
rgindaa19afe22012-01-25 15:40:22 -0800597 return;
598 }
599
600 // Worst case, we're somewhere in the middle of the cursor node. We'll
601 // have to split it into two nodes and insert our new container in between.
602 this.splitNode_(cursorNode, offset);
603 var newNode = this.textAttributes.createContainer(str);
604 this.cursorRowNode_.insertBefore(newNode, cursorNode.nextSibling);
605 this.cursorNode_ = newNode;
Mike Frysinger6380bed2017-08-24 18:46:39 -0400606 this.cursorOffset_ = wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800607};
608
609/**
rginda8ba33642011-12-14 12:31:31 -0800610 * Overwrite the text at the current cursor position.
611 *
rgindaa09e7332012-08-17 12:49:51 -0700612 * You must call maybeClipCurrentRow() after in order to clip overflowed
613 * text and clamp the cursor.
614 *
615 * It is also up to the caller to properly maintain the line overflow state
616 * using hterm.Screen..commitLineOverflow().
Joel Hockey0f933582019-08-27 18:01:51 -0700617 *
618 * @param {string} str The source string for overwriting existing content.
619 * @param {number=} wcwidth The cached lib.wc.strWidth value for |str|. Will be
620 * calculated on demand if need be. Passing in a cached value helps speed
621 * up processing as this is a hot codepath.
rginda8ba33642011-12-14 12:31:31 -0800622 */
Mike Frysinger6380bed2017-08-24 18:46:39 -0400623hterm.Screen.prototype.overwriteString = function(str, wcwidth=undefined) {
rginda8ba33642011-12-14 12:31:31 -0800624 var maxLength = this.columnCount_ - this.cursorPosition.column;
625 if (!maxLength)
Mike Frysinger159b7392019-03-26 11:08:32 -0700626 return;
rgindaa19afe22012-01-25 15:40:22 -0800627
Mike Frysinger6380bed2017-08-24 18:46:39 -0400628 if (wcwidth === undefined)
629 wcwidth = lib.wc.strWidth(str);
630
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700631 if (this.textAttributes.matchesContainer(lib.notNull(this.cursorNode_)) &&
632 this.cursorNode_.textContent.substr(this.cursorOffset_) ==
633 str) {
rgindaa19afe22012-01-25 15:40:22 -0800634 // This overwrite would be a no-op, just move the cursor and return.
Mike Frysinger6380bed2017-08-24 18:46:39 -0400635 this.cursorOffset_ += wcwidth;
636 this.cursorPosition.column += wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800637 return;
638 }
rginda8ba33642011-12-14 12:31:31 -0800639
Mike Frysinger6380bed2017-08-24 18:46:39 -0400640 this.deleteChars(Math.min(wcwidth, maxLength));
641 this.insertString(str, wcwidth);
rginda8ba33642011-12-14 12:31:31 -0800642};
643
644/**
645 * Forward-delete one or more characters at the current cursor position.
646 *
647 * Text to the right of the deleted characters is shifted left. Only affects
648 * characters on the same row as the cursor.
649 *
Joel Hockey0f933582019-08-27 18:01:51 -0700650 * @param {number} count The column width of characters to delete. This is
Ricky Liang48f05cb2013-12-31 23:35:29 +0800651 * clamped to the column width minus the cursor column.
Joel Hockey0f933582019-08-27 18:01:51 -0700652 * @return {number} The column width of the characters actually deleted.
rginda8ba33642011-12-14 12:31:31 -0800653 */
654hterm.Screen.prototype.deleteChars = function(count) {
655 var node = this.cursorNode_;
656 var offset = this.cursorOffset_;
657
Robert Ginda7fd57082012-09-25 14:41:47 -0700658 var currentCursorColumn = this.cursorPosition.column;
659 count = Math.min(count, this.columnCount_ - currentCursorColumn);
660 if (!count)
661 return 0;
662
663 var rv = count;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800664 var startLength, endLength;
rgindaa19afe22012-01-25 15:40:22 -0800665
rginda8ba33642011-12-14 12:31:31 -0800666 while (node && count) {
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400667 // Sanity check so we don't loop forever, but we don't also go quietly.
668 if (count < 0) {
669 console.error(`Deleting ${rv} chars went negative: ${count}`);
670 break;
671 }
672
Ricky Liang48f05cb2013-12-31 23:35:29 +0800673 startLength = hterm.TextAttributes.nodeWidth(node);
674 node.textContent = hterm.TextAttributes.nodeSubstr(node, 0, offset) +
675 hterm.TextAttributes.nodeSubstr(node, offset + count);
676 endLength = hterm.TextAttributes.nodeWidth(node);
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400677
678 // Deal with splitting wide characters. There are two ways: we could delete
679 // the first column or the second column. In both cases, we delete the wide
680 // character and replace one of the columns with a space (since the other
681 // was deleted). If there are more chars to delete, the next loop will pick
682 // up the slack.
683 if (node.wcNode && offset < startLength &&
684 ((endLength && startLength == endLength) || (!endLength && offset == 1))) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800685 // No characters were deleted when there should be. We're probably trying
686 // to delete one column width from a wide character node. We remove the
687 // wide character node here and replace it with a single space.
688 var spaceNode = this.textAttributes.createContainer(' ');
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400689 node.parentNode.insertBefore(spaceNode, offset ? node : node.nextSibling);
Ricky Liang48f05cb2013-12-31 23:35:29 +0800690 node.textContent = '';
691 endLength = 0;
692 count -= 1;
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400693 } else
694 count -= startLength - endLength;
rginda8ba33642011-12-14 12:31:31 -0800695
Ricky Liang48f05cb2013-12-31 23:35:29 +0800696 var nextNode = node.nextSibling;
697 if (endLength == 0 && node != this.cursorNode_) {
698 node.parentNode.removeChild(node);
699 }
700 node = nextNode;
rginda8ba33642011-12-14 12:31:31 -0800701 offset = 0;
702 }
Robert Ginda7fd57082012-09-25 14:41:47 -0700703
Ricky Liang48f05cb2013-12-31 23:35:29 +0800704 // Remove this.cursorNode_ if it is an empty non-text node.
Mike Frysinger6a4f2412017-08-31 01:11:25 -0400705 if (this.cursorNode_.nodeType != Node.TEXT_NODE &&
706 !this.cursorNode_.textContent) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800707 var cursorNode = this.cursorNode_;
708 if (cursorNode.previousSibling) {
709 this.cursorNode_ = cursorNode.previousSibling;
710 this.cursorOffset_ = hterm.TextAttributes.nodeWidth(
711 cursorNode.previousSibling);
712 } else if (cursorNode.nextSibling) {
713 this.cursorNode_ = cursorNode.nextSibling;
714 this.cursorOffset_ = 0;
715 } else {
716 var emptyNode = this.cursorRowNode_.ownerDocument.createTextNode('');
717 this.cursorRowNode_.appendChild(emptyNode);
718 this.cursorNode_ = emptyNode;
719 this.cursorOffset_ = 0;
720 }
721 this.cursorRowNode_.removeChild(cursorNode);
722 }
723
Robert Ginda7fd57082012-09-25 14:41:47 -0700724 return rv;
rginda8ba33642011-12-14 12:31:31 -0800725};
John Macinnesfb683832013-07-22 14:46:30 -0400726
727/**
728 * Finds first X-ROW of a line containing specified X-ROW.
729 * Used to support line overflow.
730 *
Joel Hockey0f933582019-08-27 18:01:51 -0700731 * @param {!Node} row X-ROW to begin search for first row of line.
732 * @return {!Node} The X-ROW that is at the beginning of the line.
John Macinnesfb683832013-07-22 14:46:30 -0400733 **/
734hterm.Screen.prototype.getLineStartRow_ = function(row) {
735 while (row.previousSibling &&
736 row.previousSibling.hasAttribute('line-overflow')) {
737 row = row.previousSibling;
738 }
739 return row;
740};
741
742/**
743 * Gets text of a line beginning with row.
744 * Supports line overflow.
745 *
Joel Hockey0f933582019-08-27 18:01:51 -0700746 * @param {!Node} row First X-ROW of line.
John Macinnesfb683832013-07-22 14:46:30 -0400747 * @return {string} Text content of line.
748 **/
749hterm.Screen.prototype.getLineText_ = function(row) {
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700750 let rowText = '';
751 let rowOrNull = row;
752 while (rowOrNull) {
753 rowText += rowOrNull.textContent;
754 if (rowOrNull.hasAttribute('line-overflow')) {
755 rowOrNull = rowOrNull.nextSibling;
John Macinnesfb683832013-07-22 14:46:30 -0400756 } else {
757 break;
758 }
759 }
760 return rowText;
761};
762
763/**
764 * Returns X-ROW that is ancestor of the node.
765 *
Joel Hockey0f933582019-08-27 18:01:51 -0700766 * @param {!Node} node Node to get X-ROW ancestor for.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700767 * @return {?Node} X-ROW ancestor of node, or null if not found.
John Macinnesfb683832013-07-22 14:46:30 -0400768 **/
769hterm.Screen.prototype.getXRowAncestor_ = function(node) {
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700770 let nodeOrNull = node;
771 while (nodeOrNull) {
772 if (nodeOrNull.nodeName === 'X-ROW')
John Macinnesfb683832013-07-22 14:46:30 -0400773 break;
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700774 nodeOrNull = nodeOrNull.parentNode;
John Macinnesfb683832013-07-22 14:46:30 -0400775 }
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700776 return nodeOrNull;
John Macinnesfb683832013-07-22 14:46:30 -0400777};
778
779/**
780 * Returns position within line of character at offset within node.
781 * Supports line overflow.
782 *
Joel Hockey0f933582019-08-27 18:01:51 -0700783 * @param {!Node} row X-ROW at beginning of line.
784 * @param {!Node} node Node to get position of.
785 * @param {number} offset Offset into node.
786 * @return {number} Position within line of character at offset within node.
John Macinnesfb683832013-07-22 14:46:30 -0400787 **/
788hterm.Screen.prototype.getPositionWithOverflow_ = function(row, node, offset) {
789 if (!node)
790 return -1;
791 var ancestorRow = this.getXRowAncestor_(node);
792 if (!ancestorRow)
793 return -1;
794 var position = 0;
795 while (ancestorRow != row) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800796 position += hterm.TextAttributes.nodeWidth(row);
John Macinnesfb683832013-07-22 14:46:30 -0400797 if (row.hasAttribute('line-overflow') && row.nextSibling) {
798 row = row.nextSibling;
799 } else {
800 return -1;
801 }
802 }
803 return position + this.getPositionWithinRow_(row, node, offset);
804};
805
806/**
807 * Returns position within row of character at offset within node.
808 * Does not support line overflow.
809 *
Joel Hockey0f933582019-08-27 18:01:51 -0700810 * @param {!Node} row X-ROW to get position within.
811 * @param {!Node} node Node to get position for.
812 * @param {number} offset Offset within node to get position for.
813 * @return {number} Position within row of character at offset within node.
John Macinnesfb683832013-07-22 14:46:30 -0400814 **/
815hterm.Screen.prototype.getPositionWithinRow_ = function(row, node, offset) {
816 if (node.parentNode != row) {
Mike Frysinger498192d2017-06-26 18:23:31 -0400817 // If we traversed to the top node, then there's nothing to find here.
818 if (node.parentNode == null)
819 return -1;
820
John Macinnesfb683832013-07-22 14:46:30 -0400821 return this.getPositionWithinRow_(node.parentNode, node, offset) +
822 this.getPositionWithinRow_(row, node.parentNode, 0);
823 }
824 var position = 0;
825 for (var i = 0; i < row.childNodes.length; i++) {
826 var currentNode = row.childNodes[i];
827 if (currentNode == node)
828 return position + offset;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800829 position += hterm.TextAttributes.nodeWidth(currentNode);
John Macinnesfb683832013-07-22 14:46:30 -0400830 }
831 return -1;
832};
833
834/**
835 * Returns the node and offset corresponding to position within line.
836 * Supports line overflow.
837 *
Joel Hockey0f933582019-08-27 18:01:51 -0700838 * @param {!Node} row X-ROW at beginning of line.
839 * @param {number} position Position within line to retrieve node and offset.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700840 * @return {?Array} Two element array containing node and offset respectively.
John Macinnesfb683832013-07-22 14:46:30 -0400841 **/
842hterm.Screen.prototype.getNodeAndOffsetWithOverflow_ = function(row, position) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800843 while (row && position > hterm.TextAttributes.nodeWidth(row)) {
John Macinnesfb683832013-07-22 14:46:30 -0400844 if (row.hasAttribute('line-overflow') && row.nextSibling) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800845 position -= hterm.TextAttributes.nodeWidth(row);
John Macinnesfb683832013-07-22 14:46:30 -0400846 row = row.nextSibling;
847 } else {
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700848 return [null, -1];
John Macinnesfb683832013-07-22 14:46:30 -0400849 }
850 }
851 return this.getNodeAndOffsetWithinRow_(row, position);
852};
853
854/**
855 * Returns the node and offset corresponding to position within row.
856 * Does not support line overflow.
857 *
Joel Hockey0f933582019-08-27 18:01:51 -0700858 * @param {!Node} row X-ROW to get position within.
859 * @param {number} position Position within row to retrieve node and offset.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700860 * @return {?Array} Two element array containing node and offset respectively.
John Macinnesfb683832013-07-22 14:46:30 -0400861 **/
862hterm.Screen.prototype.getNodeAndOffsetWithinRow_ = function(row, position) {
863 for (var i = 0; i < row.childNodes.length; i++) {
864 var node = row.childNodes[i];
Ricky Liang48f05cb2013-12-31 23:35:29 +0800865 var nodeTextWidth = hterm.TextAttributes.nodeWidth(node);
866 if (position <= nodeTextWidth) {
John Macinnesfb683832013-07-22 14:46:30 -0400867 if (node.nodeName === 'SPAN') {
868 /** Drill down to node contained by SPAN. **/
869 return this.getNodeAndOffsetWithinRow_(node, position);
870 } else {
871 return [node, position];
872 }
873 }
Ricky Liang48f05cb2013-12-31 23:35:29 +0800874 position -= nodeTextWidth;
John Macinnesfb683832013-07-22 14:46:30 -0400875 }
876 return null;
877};
878
879/**
880 * Returns the node and offset corresponding to position within line.
881 * Supports line overflow.
882 *
Joel Hockey0f933582019-08-27 18:01:51 -0700883 * @param {!Node} row X-ROW at beginning of line.
884 * @param {number} start Start position of range within line.
885 * @param {number} end End position of range within line.
886 * @param {!Range} range Range to modify.
John Macinnesfb683832013-07-22 14:46:30 -0400887 **/
888hterm.Screen.prototype.setRange_ = function(row, start, end, range) {
889 var startNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, start);
890 if (startNodeAndOffset == null)
891 return;
892 var endNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, end);
893 if (endNodeAndOffset == null)
894 return;
895 range.setStart(startNodeAndOffset[0], startNodeAndOffset[1]);
896 range.setEnd(endNodeAndOffset[0], endNodeAndOffset[1]);
897};
898
899/**
John Lincae9b732018-03-08 13:56:35 +0800900 * Expands selection to surrounding string with word break matches.
John Macinnesfb683832013-07-22 14:46:30 -0400901 *
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700902 * @param {?Selection} selection Selection to expand.
John Lincae9b732018-03-08 13:56:35 +0800903 * @param {string} leftMatch left word break match.
904 * @param {string} rightMatch right word break match.
905 * @param {string} insideMatch inside word break match.
906 */
907hterm.Screen.prototype.expandSelectionWithWordBreakMatches_ =
908 function(selection, leftMatch, rightMatch, insideMatch) {
John Macinnesfb683832013-07-22 14:46:30 -0400909 if (!selection)
910 return;
911
912 var range = selection.getRangeAt(0);
913 if (!range || range.toString().match(/\s/))
914 return;
915
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700916 const rowElement = this.getXRowAncestor_(lib.notNull(range.startContainer));
Raymes Khoury334625a2018-06-25 10:29:40 +1000917 if (!rowElement)
918 return;
919 const row = this.getLineStartRow_(rowElement);
John Macinnesfb683832013-07-22 14:46:30 -0400920 if (!row)
921 return;
922
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700923 var startPosition = this.getPositionWithOverflow_(
924 row, lib.notNull(range.startContainer), range.startOffset);
John Macinnesfb683832013-07-22 14:46:30 -0400925 if (startPosition == -1)
926 return;
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700927 var endPosition = this.getPositionWithOverflow_(
928 row, lib.notNull(range.endContainer), range.endOffset);
John Macinnesfb683832013-07-22 14:46:30 -0400929 if (endPosition == -1)
930 return;
931
John Macinnesfb683832013-07-22 14:46:30 -0400932 //Move start to the left.
933 var rowText = this.getLineText_(row);
Ricky Liang48f05cb2013-12-31 23:35:29 +0800934 var lineUpToRange = lib.wc.substring(rowText, 0, endPosition);
Robert Ginda5eba4562014-08-11 11:05:54 -0700935 var leftRegularExpression = new RegExp(leftMatch + insideMatch + "$");
John Macinnesfb683832013-07-22 14:46:30 -0400936 var expandedStart = lineUpToRange.search(leftRegularExpression);
937 if (expandedStart == -1 || expandedStart > startPosition)
938 return;
939
940 //Move end to the right.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800941 var lineFromRange = lib.wc.substring(rowText, startPosition,
942 lib.wc.strWidth(rowText));
Robert Ginda5eba4562014-08-11 11:05:54 -0700943 var rightRegularExpression = new RegExp("^" + insideMatch + rightMatch);
John Macinnesfb683832013-07-22 14:46:30 -0400944 var found = lineFromRange.match(rightRegularExpression);
945 if (!found)
946 return;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800947 var expandedEnd = startPosition + lib.wc.strWidth(found[0]);
John Macinnesfb683832013-07-22 14:46:30 -0400948 if (expandedEnd == -1 || expandedEnd < endPosition)
949 return;
950
951 this.setRange_(row, expandedStart, expandedEnd, range);
952 selection.addRange(range);
953};
Mike Frysingera2cacaa2017-11-29 13:51:09 -0800954
955/**
John Lincae9b732018-03-08 13:56:35 +0800956 * Expands selection to surrounding string using the user's settings.
957 *
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700958 * @param {?Selection} selection Selection to expand.
John Lincae9b732018-03-08 13:56:35 +0800959 */
960hterm.Screen.prototype.expandSelection = function(selection) {
961 this.expandSelectionWithWordBreakMatches_(
962 selection,
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700963 lib.notNull(this.wordBreakMatchLeft),
964 lib.notNull(this.wordBreakMatchRight),
965 lib.notNull(this.wordBreakMatchMiddle));
Mike Frysinger8416e0a2017-05-17 09:09:46 -0400966};
John Lincae9b732018-03-08 13:56:35 +0800967
968/**
969 * Expands selection to surrounding URL using a set of fixed match settings.
970 *
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700971 * @param {?Selection} selection Selection to expand.
John Lincae9b732018-03-08 13:56:35 +0800972 */
973hterm.Screen.prototype.expandSelectionForUrl = function(selection) {
974 this.expandSelectionWithWordBreakMatches_(
975 selection,
976 "[^\\s\\[\\](){}<>\"'\\^!@#$%&*,;:`]",
977 "[^\\s\\[\\](){}<>\"'\\^!@#$%&*,;:~.`]",
978 "[^\\s\\[\\](){}<>\"'\\^]*");
979};
980
981/**
Mike Frysingera2cacaa2017-11-29 13:51:09 -0800982 * Save the current cursor state to the corresponding screens.
983 *
Joel Hockey0f933582019-08-27 18:01:51 -0700984 * @param {!hterm.VT} vt The VT object to read graphic codeset details from.
Mike Frysingera2cacaa2017-11-29 13:51:09 -0800985 */
986hterm.Screen.prototype.saveCursorAndState = function(vt) {
987 this.cursorState_.save(vt);
988};
989
990/**
991 * Restore the saved cursor state in the corresponding screens.
992 *
Joel Hockey0f933582019-08-27 18:01:51 -0700993 * @param {!hterm.VT} vt The VT object to write graphic codeset details to.
Mike Frysingera2cacaa2017-11-29 13:51:09 -0800994 */
995hterm.Screen.prototype.restoreCursorAndState = function(vt) {
996 this.cursorState_.restore(vt);
997};
998
999/**
1000 * Track all the things related to the current "cursor".
1001 *
1002 * The set of things saved & restored here is defined by DEC:
1003 * https://vt100.net/docs/vt510-rm/DECSC.html
1004 * - Cursor position
1005 * - Character attributes set by the SGR command
1006 * - Character sets (G0, G1, G2, or G3) currently in GL and GR
1007 * - Wrap flag (autowrap or no autowrap)
1008 * - State of origin mode (DECOM)
1009 * - Selective erase attribute
1010 * - Any single shift 2 (SS2) or single shift 3 (SS3) functions sent
1011 *
1012 * These are done on a per-screen basis.
Joel Hockey0f933582019-08-27 18:01:51 -07001013 *
1014 * @param {!hterm.Screen} screen The screen this cursor is tied to.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -07001015 * @constructor
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001016 */
1017hterm.Screen.CursorState = function(screen) {
1018 this.screen_ = screen;
1019 this.cursor = null;
1020 this.textAttributes = null;
1021 this.GL = this.GR = this.G0 = this.G1 = this.G2 = this.G3 = null;
1022};
1023
1024/**
1025 * Save all the cursor state.
1026 *
Joel Hockey0f933582019-08-27 18:01:51 -07001027 * @param {!hterm.VT} vt The VT object to read graphic codeset details from.
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001028 */
1029hterm.Screen.CursorState.prototype.save = function(vt) {
1030 this.cursor = vt.terminal.saveCursor();
1031
1032 this.textAttributes = this.screen_.textAttributes.clone();
1033
1034 this.GL = vt.GL;
1035 this.GR = vt.GR;
1036
1037 this.G0 = vt.G0;
1038 this.G1 = vt.G1;
1039 this.G2 = vt.G2;
1040 this.G3 = vt.G3;
1041};
1042
1043/**
1044 * Restore the previously saved cursor state.
1045 *
Joel Hockey0f933582019-08-27 18:01:51 -07001046 * @param {!hterm.VT} vt The VT object to write graphic codeset details to.
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001047 */
1048hterm.Screen.CursorState.prototype.restore = function(vt) {
1049 vt.terminal.restoreCursor(this.cursor);
1050
1051 // Cursor restore includes char attributes (bold/etc...), but does not change
1052 // the color palette (which are a terminal setting).
1053 const tattrs = this.textAttributes.clone();
1054 tattrs.colorPalette = this.screen_.textAttributes.colorPalette;
1055 tattrs.syncColors();
1056
1057 this.screen_.textAttributes = tattrs;
1058
1059 vt.GL = this.GL;
1060 vt.GR = this.GR;
1061
1062 vt.G0 = this.G0;
1063 vt.G1 = this.G1;
1064 vt.G2 = this.G2;
1065 vt.G3 = this.G3;
1066};