blob: c7396d2d879d230351c2762e1a3c5cfa2c4b0bb4 [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.
46 */
Mike Frysinger60a156d2019-06-13 10:15:45 -040047hterm.Screen = function(columnCount=0) {
rginda8ba33642011-12-14 12:31:31 -080048 /**
49 * Public, read-only access to the rows in this screen.
50 */
51 this.rowsArray = [];
52
53 // The max column width for this screen.
Mike Frysinger60a156d2019-06-13 10:15:45 -040054 this.columnCount_ = columnCount;
rginda8ba33642011-12-14 12:31:31 -080055
rgindaa19afe22012-01-25 15:40:22 -080056 // The current color, bold, underline and blink attributes.
57 this.textAttributes = new hterm.TextAttributes(window.document);
58
rginda87b86462011-12-14 13:48:03 -080059 // Current zero-based cursor coordinates.
60 this.cursorPosition = new hterm.RowCol(0, 0);
rginda8ba33642011-12-14 12:31:31 -080061
Mike Frysingera2cacaa2017-11-29 13:51:09 -080062 // Saved state used by DECSC and related settings. This is only for saving
63 // and restoring specific state, not for the current/active state.
64 this.cursorState_ = new hterm.Screen.CursorState(this);
65
rginda8ba33642011-12-14 12:31:31 -080066 // The node containing the row that the cursor is positioned on.
67 this.cursorRowNode_ = null;
68
69 // The node containing the span of text that the cursor is positioned on.
70 this.cursorNode_ = null;
71
Ricky Liang48f05cb2013-12-31 23:35:29 +080072 // The offset in column width into cursorNode_ where the cursor is positioned.
rginda8ba33642011-12-14 12:31:31 -080073 this.cursorOffset_ = null;
Mike Frysinger664e9992017-05-19 01:24:24 -040074
75 // Regexes for expanding word selections.
76 this.wordBreakMatchLeft = null;
77 this.wordBreakMatchRight = null;
78 this.wordBreakMatchMiddle = null;
rginda8ba33642011-12-14 12:31:31 -080079};
80
81/**
82 * Return the screen size as an hterm.Size object.
83 *
Joel Hockey0f933582019-08-27 18:01:51 -070084 * @return {!hterm.Size} hterm.Size object representing the current number
rginda8ba33642011-12-14 12:31:31 -080085 * of rows and columns in this screen.
86 */
87hterm.Screen.prototype.getSize = function() {
88 return new hterm.Size(this.columnCount_, this.rowsArray.length);
89};
90
91/**
92 * Return the current number of rows in this screen.
93 *
Joel Hockey0f933582019-08-27 18:01:51 -070094 * @return {number} The number of rows in this screen.
rginda8ba33642011-12-14 12:31:31 -080095 */
96hterm.Screen.prototype.getHeight = function() {
97 return this.rowsArray.length;
98};
99
100/**
101 * Return the current number of columns in this screen.
102 *
Joel Hockey0f933582019-08-27 18:01:51 -0700103 * @return {number} The number of columns in this screen.
rginda8ba33642011-12-14 12:31:31 -0800104 */
105hterm.Screen.prototype.getWidth = function() {
106 return this.columnCount_;
107};
108
109/**
110 * Set the maximum number of columns per row.
111 *
Joel Hockey0f933582019-08-27 18:01:51 -0700112 * @param {number} count The maximum number of columns per row.
rginda8ba33642011-12-14 12:31:31 -0800113 */
114hterm.Screen.prototype.setColumnCount = function(count) {
rginda2312fff2012-01-05 16:20:52 -0800115 this.columnCount_ = count;
116
rgindacbbd7482012-06-13 15:06:16 -0700117 if (this.cursorPosition.column >= count)
118 this.setCursorPosition(this.cursorPosition.row, count - 1);
rginda8ba33642011-12-14 12:31:31 -0800119};
120
121/**
122 * Remove the first row from the screen and return it.
123 *
Joel Hockey0f933582019-08-27 18:01:51 -0700124 * @return {!Element} The first row in this screen.
rginda8ba33642011-12-14 12:31:31 -0800125 */
126hterm.Screen.prototype.shiftRow = function() {
127 return this.shiftRows(1)[0];
rginda87b86462011-12-14 13:48:03 -0800128};
rginda8ba33642011-12-14 12:31:31 -0800129
130/**
131 * Remove rows from the top of the screen and return them as an array.
132 *
Joel Hockey0f933582019-08-27 18:01:51 -0700133 * @param {number} count The number of rows to remove.
134 * @return {!Array<!Element>} The selected rows.
rginda8ba33642011-12-14 12:31:31 -0800135 */
136hterm.Screen.prototype.shiftRows = function(count) {
137 return this.rowsArray.splice(0, count);
138};
139
140/**
141 * Insert a row at the top of the screen.
142 *
Joel Hockey0f933582019-08-27 18:01:51 -0700143 * @param {!Element} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800144 */
145hterm.Screen.prototype.unshiftRow = function(row) {
146 this.rowsArray.splice(0, 0, row);
147};
148
149/**
150 * Insert rows at the top of the screen.
151 *
Joel Hockey0f933582019-08-27 18:01:51 -0700152 * @param {!Array<!Element>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800153 */
154hterm.Screen.prototype.unshiftRows = function(rows) {
155 this.rowsArray.unshift.apply(this.rowsArray, rows);
156};
157
158/**
159 * Remove the last row from the screen and return it.
160 *
Joel Hockey0f933582019-08-27 18:01:51 -0700161 * @return {!Element} The last row in this screen.
rginda8ba33642011-12-14 12:31:31 -0800162 */
163hterm.Screen.prototype.popRow = function() {
164 return this.popRows(1)[0];
165};
166
167/**
168 * Remove rows from the bottom of the screen and return them as an array.
169 *
Joel Hockey0f933582019-08-27 18:01:51 -0700170 * @param {number} count The number of rows to remove.
171 * @return {!Array<!Element>} The selected rows.
rginda8ba33642011-12-14 12:31:31 -0800172 */
173hterm.Screen.prototype.popRows = function(count) {
174 return this.rowsArray.splice(this.rowsArray.length - count, count);
175};
176
177/**
178 * Insert a row at the bottom of the screen.
179 *
Joel Hockey0f933582019-08-27 18:01:51 -0700180 * @param {!Element} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800181 */
182hterm.Screen.prototype.pushRow = function(row) {
183 this.rowsArray.push(row);
184};
185
186/**
187 * Insert rows at the bottom of the screen.
188 *
Joel Hockey0f933582019-08-27 18:01:51 -0700189 * @param {!Array<!Element>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800190 */
191hterm.Screen.prototype.pushRows = function(rows) {
192 rows.push.apply(this.rowsArray, rows);
193};
194
195/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500196 * Insert a row at the specified row of the screen.
rginda8ba33642011-12-14 12:31:31 -0800197 *
Joel Hockey0f933582019-08-27 18:01:51 -0700198 * @param {number} index The index to insert the row.
199 * @param {!Element} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800200 */
201hterm.Screen.prototype.insertRow = function(index, row) {
202 this.rowsArray.splice(index, 0, row);
203};
204
205/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500206 * Insert rows at the specified row of the screen.
rginda8ba33642011-12-14 12:31:31 -0800207 *
Joel Hockey0f933582019-08-27 18:01:51 -0700208 * @param {number} index The index to insert the rows.
209 * @param {!Array<!Element>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800210 */
211hterm.Screen.prototype.insertRows = function(index, rows) {
212 for (var i = 0; i < rows.length; i++) {
213 this.rowsArray.splice(index + i, 0, rows[i]);
214 }
215};
216
217/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500218 * Remove a row from the screen and return it.
rginda8ba33642011-12-14 12:31:31 -0800219 *
Joel Hockey0f933582019-08-27 18:01:51 -0700220 * @param {number} index The index of the row to remove.
221 * @return {!Element} The selected row.
rginda8ba33642011-12-14 12:31:31 -0800222 */
223hterm.Screen.prototype.removeRow = function(index) {
224 return this.rowsArray.splice(index, 1)[0];
225};
226
227/**
228 * Remove rows from the bottom of the screen and return them as an array.
229 *
Joel Hockey0f933582019-08-27 18:01:51 -0700230 * @param {number} index The index to start removing rows.
231 * @param {number} count The number of rows to remove.
232 * @return {!Array<!Element>} The selected rows.
rginda8ba33642011-12-14 12:31:31 -0800233 */
234hterm.Screen.prototype.removeRows = function(index, count) {
235 return this.rowsArray.splice(index, count);
236};
237
238/**
239 * Invalidate the current cursor position.
240 *
rginda87b86462011-12-14 13:48:03 -0800241 * This sets this.cursorPosition to (0, 0) and clears out some internal
rginda8ba33642011-12-14 12:31:31 -0800242 * data.
243 *
244 * Attempting to insert or overwrite text while the cursor position is invalid
245 * will raise an obscure exception.
246 */
247hterm.Screen.prototype.invalidateCursorPosition = function() {
rginda87b86462011-12-14 13:48:03 -0800248 this.cursorPosition.move(0, 0);
rginda8ba33642011-12-14 12:31:31 -0800249 this.cursorRowNode_ = null;
250 this.cursorNode_ = null;
251 this.cursorOffset_ = null;
252};
253
254/**
rginda8ba33642011-12-14 12:31:31 -0800255 * Clear the contents of the cursor row.
rginda8ba33642011-12-14 12:31:31 -0800256 */
257hterm.Screen.prototype.clearCursorRow = function() {
258 this.cursorRowNode_.innerHTML = '';
rgindaa09e7332012-08-17 12:49:51 -0700259 this.cursorRowNode_.removeAttribute('line-overflow');
rginda8ba33642011-12-14 12:31:31 -0800260 this.cursorOffset_ = 0;
rginda8ba33642011-12-14 12:31:31 -0800261 this.cursorPosition.column = 0;
rginda2312fff2012-01-05 16:20:52 -0800262 this.cursorPosition.overflow = false;
Robert Ginda7fd57082012-09-25 14:41:47 -0700263
264 var text;
265 if (this.textAttributes.isDefault()) {
266 text = '';
267 } else {
Mike Frysinger73e56462019-07-17 00:23:46 -0500268 text = ' '.repeat(this.columnCount_);
Robert Ginda7fd57082012-09-25 14:41:47 -0700269 }
270
Zhu Qunying30d40712017-03-14 16:27:00 -0700271 // We shouldn't honor inverse colors when clearing an area, to match
272 // xterm's back color erase behavior.
Edoardo Spadolini2fd43642014-08-23 22:59:57 +0200273 var inverse = this.textAttributes.inverse;
274 this.textAttributes.inverse = false;
275 this.textAttributes.syncColors();
276
Robert Ginda7fd57082012-09-25 14:41:47 -0700277 var node = this.textAttributes.createContainer(text);
278 this.cursorRowNode_.appendChild(node);
279 this.cursorNode_ = node;
Edoardo Spadolini2fd43642014-08-23 22:59:57 +0200280
281 this.textAttributes.inverse = inverse;
282 this.textAttributes.syncColors();
rginda8ba33642011-12-14 12:31:31 -0800283};
284
285/**
rgindaa09e7332012-08-17 12:49:51 -0700286 * Mark the current row as having overflowed to the next line.
287 *
288 * The line overflow state is used when converting a range of rows into text.
289 * It makes it possible to recombine two or more overflow terminal rows into
290 * a single line.
291 *
292 * This is distinct from the cursor being in the overflow state. Cursor
293 * overflow indicates that printing at the cursor position will commit a
294 * line overflow, unless it is preceded by a repositioning of the cursor
295 * to a non-overflow state.
296 */
297hterm.Screen.prototype.commitLineOverflow = function() {
298 this.cursorRowNode_.setAttribute('line-overflow', true);
299};
300
301/**
rginda8ba33642011-12-14 12:31:31 -0800302 * Relocate the cursor to a give row and column.
303 *
Joel Hockey0f933582019-08-27 18:01:51 -0700304 * @param {number} row The zero based row.
305 * @param {number} column The zero based column.
rginda8ba33642011-12-14 12:31:31 -0800306 */
307hterm.Screen.prototype.setCursorPosition = function(row, column) {
rginda11057d52012-04-25 12:29:56 -0700308 if (!this.rowsArray.length) {
309 console.warn('Attempt to set cursor position on empty screen.');
310 return;
311 }
312
rginda87b86462011-12-14 13:48:03 -0800313 if (row >= this.rowsArray.length) {
rgindacbbd7482012-06-13 15:06:16 -0700314 console.error('Row out of bounds: ' + row);
rginda87b86462011-12-14 13:48:03 -0800315 row = this.rowsArray.length - 1;
316 } else if (row < 0) {
rgindacbbd7482012-06-13 15:06:16 -0700317 console.error('Row out of bounds: ' + row);
rginda87b86462011-12-14 13:48:03 -0800318 row = 0;
319 }
320
321 if (column >= this.columnCount_) {
rgindacbbd7482012-06-13 15:06:16 -0700322 console.error('Column out of bounds: ' + column);
rginda87b86462011-12-14 13:48:03 -0800323 column = this.columnCount_ - 1;
324 } else if (column < 0) {
rgindacbbd7482012-06-13 15:06:16 -0700325 console.error('Column out of bounds: ' + column);
rginda87b86462011-12-14 13:48:03 -0800326 column = 0;
327 }
rginda8ba33642011-12-14 12:31:31 -0800328
rginda2312fff2012-01-05 16:20:52 -0800329 this.cursorPosition.overflow = false;
330
rginda8ba33642011-12-14 12:31:31 -0800331 var rowNode = this.rowsArray[row];
332 var node = rowNode.firstChild;
333
334 if (!node) {
335 node = rowNode.ownerDocument.createTextNode('');
336 rowNode.appendChild(node);
337 }
338
rgindaa19afe22012-01-25 15:40:22 -0800339 var currentColumn = 0;
340
rginda8ba33642011-12-14 12:31:31 -0800341 if (rowNode == this.cursorRowNode_) {
342 if (column >= this.cursorPosition.column - this.cursorOffset_) {
343 node = this.cursorNode_;
344 currentColumn = this.cursorPosition.column - this.cursorOffset_;
345 }
346 } else {
347 this.cursorRowNode_ = rowNode;
348 }
349
350 this.cursorPosition.move(row, column);
351
352 while (node) {
353 var offset = column - currentColumn;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800354 var width = hterm.TextAttributes.nodeWidth(node);
355 if (!node.nextSibling || width > offset) {
rginda8ba33642011-12-14 12:31:31 -0800356 this.cursorNode_ = node;
357 this.cursorOffset_ = offset;
358 return;
359 }
360
Ricky Liang48f05cb2013-12-31 23:35:29 +0800361 currentColumn += width;
rginda8ba33642011-12-14 12:31:31 -0800362 node = node.nextSibling;
363 }
364};
365
366/**
rginda87b86462011-12-14 13:48:03 -0800367 * Set the provided selection object to be a caret selection at the current
368 * cursor position.
Joel Hockey0f933582019-08-27 18:01:51 -0700369 *
370 * @param {!Selection} selection
rginda87b86462011-12-14 13:48:03 -0800371 */
372hterm.Screen.prototype.syncSelectionCaret = function(selection) {
Rob Spies06533ba2014-04-24 11:20:37 -0700373 try {
374 selection.collapse(this.cursorNode_, this.cursorOffset_);
375 } catch (firefoxIgnoredException) {
376 // FF can throw an exception if the range is off, rather than just not
377 // performing the collapse.
378 }
rginda87b86462011-12-14 13:48:03 -0800379};
380
381/**
rgindaa19afe22012-01-25 15:40:22 -0800382 * Split a single node into two nodes at the given offset.
rginda8ba33642011-12-14 12:31:31 -0800383 *
rgindaa19afe22012-01-25 15:40:22 -0800384 * For example:
385 * Given the DOM fragment '<div><span>Hello World</span></div>', call splitNode_
Zhu Qunying30d40712017-03-14 16:27:00 -0700386 * passing the span and an offset of 6. This would modify the fragment to
rgindaa19afe22012-01-25 15:40:22 -0800387 * become: '<div><span>Hello </span><span>World</span></div>'. If the span
388 * had any attributes they would have been copied to the new span as well.
389 *
390 * The to-be-split node must have a container, so that the new node can be
391 * placed next to it.
392 *
Joel Hockey0f933582019-08-27 18:01:51 -0700393 * @param {!Node} node The node to split.
394 * @param {number} offset The offset into the node where the split should
rgindaa19afe22012-01-25 15:40:22 -0800395 * occur.
rginda8ba33642011-12-14 12:31:31 -0800396 */
rgindaa19afe22012-01-25 15:40:22 -0800397hterm.Screen.prototype.splitNode_ = function(node, offset) {
rginda35c456b2012-02-09 17:29:05 -0800398 var afterNode = node.cloneNode(false);
rgindaa19afe22012-01-25 15:40:22 -0800399
rginda35c456b2012-02-09 17:29:05 -0800400 var textContent = node.textContent;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800401 node.textContent = hterm.TextAttributes.nodeSubstr(node, 0, offset);
402 afterNode.textContent = lib.wc.substr(textContent, offset);
rgindaa19afe22012-01-25 15:40:22 -0800403
Ricky Liang48f05cb2013-12-31 23:35:29 +0800404 if (afterNode.textContent)
405 node.parentNode.insertBefore(afterNode, node.nextSibling);
406 if (!node.textContent)
407 node.parentNode.removeChild(node);
rginda8ba33642011-12-14 12:31:31 -0800408};
409
410/**
rgindaa9abdd82012-08-06 18:05:09 -0700411 * Ensure that text is clipped and the cursor is clamped to the column count.
rgindaa19afe22012-01-25 15:40:22 -0800412 */
rgindaa9abdd82012-08-06 18:05:09 -0700413hterm.Screen.prototype.maybeClipCurrentRow = function() {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800414 var width = hterm.TextAttributes.nodeWidth(this.cursorRowNode_);
415
416 if (width <= this.columnCount_) {
rgindaa9abdd82012-08-06 18:05:09 -0700417 // Current row does not need clipping, but may need clamping.
418 if (this.cursorPosition.column >= this.columnCount_) {
419 this.setCursorPosition(this.cursorPosition.row, this.columnCount_ - 1);
420 this.cursorPosition.overflow = true;
421 }
rgindaa19afe22012-01-25 15:40:22 -0800422
rgindaa9abdd82012-08-06 18:05:09 -0700423 return;
424 }
425
426 // Save off the current column so we can maybe restore it later.
427 var currentColumn = this.cursorPosition.column;
428
429 // Move the cursor to the final column.
430 this.setCursorPosition(this.cursorPosition.row, this.columnCount_ - 1);
431
432 // Remove any text that partially overflows.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800433 width = hterm.TextAttributes.nodeWidth(this.cursorNode_);
434
435 if (this.cursorOffset_ < width - 1) {
436 this.cursorNode_.textContent = hterm.TextAttributes.nodeSubstr(
437 this.cursorNode_, 0, this.cursorOffset_ + 1);
rgindaa9abdd82012-08-06 18:05:09 -0700438 }
439
440 // Remove all nodes after the cursor.
rgindaa19afe22012-01-25 15:40:22 -0800441 var rowNode = this.cursorRowNode_;
442 var node = this.cursorNode_.nextSibling;
443
444 while (node) {
rgindaa19afe22012-01-25 15:40:22 -0800445 rowNode.removeChild(node);
446 node = this.cursorNode_.nextSibling;
447 }
448
Robert Ginda7fd57082012-09-25 14:41:47 -0700449 if (currentColumn < this.columnCount_) {
rgindaa9abdd82012-08-06 18:05:09 -0700450 // If the cursor was within the screen before we started then restore its
451 // position.
rgindaa19afe22012-01-25 15:40:22 -0800452 this.setCursorPosition(this.cursorPosition.row, currentColumn);
rgindaa9abdd82012-08-06 18:05:09 -0700453 } else {
454 // Otherwise leave it at the the last column in the overflow state.
455 this.cursorPosition.overflow = true;
rgindaa19afe22012-01-25 15:40:22 -0800456 }
rgindaa19afe22012-01-25 15:40:22 -0800457};
458
459/**
460 * Insert a string at the current character position using the current
461 * text attributes.
462 *
rgindaa09e7332012-08-17 12:49:51 -0700463 * You must call maybeClipCurrentRow() after in order to clip overflowed
464 * text and clamp the cursor.
465 *
466 * It is also up to the caller to properly maintain the line overflow state
467 * using hterm.Screen..commitLineOverflow().
Joel Hockey0f933582019-08-27 18:01:51 -0700468 *
469 * @param {string} str The string to insert.
470 * @param {number=} wcwidth The cached lib.wc.strWidth value for |str|. Will be
471 * calculated on demand if need be. Passing in a cached value helps speed
472 * up processing as this is a hot codepath.
rginda8ba33642011-12-14 12:31:31 -0800473 */
Mike Frysinger6380bed2017-08-24 18:46:39 -0400474hterm.Screen.prototype.insertString = function(str, wcwidth=undefined) {
rgindaa19afe22012-01-25 15:40:22 -0800475 var cursorNode = this.cursorNode_;
476 var cursorNodeText = cursorNode.textContent;
rginda8ba33642011-12-14 12:31:31 -0800477
Robert Gindaa21dfb32013-10-31 14:17:45 -0700478 this.cursorRowNode_.removeAttribute('line-overflow');
479
Ricky Liang48f05cb2013-12-31 23:35:29 +0800480 // We may alter the width of the string by prepending some missing
481 // whitespaces, so we need to record the string width ahead of time.
Mike Frysinger6380bed2017-08-24 18:46:39 -0400482 if (wcwidth === undefined)
483 wcwidth = lib.wc.strWidth(str);
rginda8ba33642011-12-14 12:31:31 -0800484
rgindaa19afe22012-01-25 15:40:22 -0800485 // No matter what, before this function exits the cursor column will have
486 // moved this much.
Mike Frysinger6380bed2017-08-24 18:46:39 -0400487 this.cursorPosition.column += wcwidth;
rginda8ba33642011-12-14 12:31:31 -0800488
rgindaa19afe22012-01-25 15:40:22 -0800489 // Local cache of the cursor offset.
490 var offset = this.cursorOffset_;
rginda8ba33642011-12-14 12:31:31 -0800491
rgindaa19afe22012-01-25 15:40:22 -0800492 // Reverse offset is the offset measured from the end of the string.
493 // Zero implies that the cursor is at the end of the cursor node.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800494 var reverseOffset = hterm.TextAttributes.nodeWidth(cursorNode) - offset;
rgindaa19afe22012-01-25 15:40:22 -0800495
496 if (reverseOffset < 0) {
497 // A negative reverse offset means the cursor is positioned past the end
498 // of the characters on this line. We'll need to insert the missing
499 // whitespace.
Mike Frysinger73e56462019-07-17 00:23:46 -0500500 const ws = ' '.repeat(-reverseOffset);
rgindaa19afe22012-01-25 15:40:22 -0800501
Brad Town7de83302015-03-12 02:10:32 -0700502 // This whitespace should be completely unstyled. Underline, background
503 // color, and strikethrough would be visible on whitespace, so we can't use
504 // one of those spans to hold the text.
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200505 if (!(this.textAttributes.underline ||
Brad Town7de83302015-03-12 02:10:32 -0700506 this.textAttributes.strikethrough ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200507 this.textAttributes.background ||
508 this.textAttributes.wcNode ||
Mike Frysinger1e98c0f2017-08-15 01:21:31 -0400509 !this.textAttributes.asciiNode ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200510 this.textAttributes.tileData != null)) {
rgindaa19afe22012-01-25 15:40:22 -0800511 // Best case scenario, we can just pretend the spaces were part of the
512 // original string.
513 str = ws + str;
Mike Frysinger6a4f2412017-08-31 01:11:25 -0400514 } else if (cursorNode.nodeType == Node.TEXT_NODE ||
Ricky Liang48f05cb2013-12-31 23:35:29 +0800515 !(cursorNode.wcNode ||
Mike Frysinger1e98c0f2017-08-15 01:21:31 -0400516 !cursorNode.asciiNode ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200517 cursorNode.tileNode ||
Ricky Liang48f05cb2013-12-31 23:35:29 +0800518 cursorNode.style.textDecoration ||
Mike Frysinger09c54f42017-12-15 01:12:30 -0500519 cursorNode.style.textDecorationStyle ||
520 cursorNode.style.textDecorationLine ||
rgindaa19afe22012-01-25 15:40:22 -0800521 cursorNode.style.backgroundColor)) {
522 // Second best case, the current node is able to hold the whitespace.
523 cursorNode.textContent = (cursorNodeText += ws);
524 } else {
525 // Worst case, we have to create a new node to hold the whitespace.
526 var wsNode = cursorNode.ownerDocument.createTextNode(ws);
527 this.cursorRowNode_.insertBefore(wsNode, cursorNode.nextSibling);
528 this.cursorNode_ = cursorNode = wsNode;
529 this.cursorOffset_ = offset = -reverseOffset;
530 cursorNodeText = ws;
531 }
532
533 // We now know for sure that we're at the last character of the cursor node.
534 reverseOffset = 0;
rginda8ba33642011-12-14 12:31:31 -0800535 }
536
rgindaa19afe22012-01-25 15:40:22 -0800537 if (this.textAttributes.matchesContainer(cursorNode)) {
538 // The new text can be placed directly in the cursor node.
539 if (reverseOffset == 0) {
540 cursorNode.textContent = cursorNodeText + str;
541 } else if (offset == 0) {
542 cursorNode.textContent = str + cursorNodeText;
543 } else {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800544 cursorNode.textContent =
545 hterm.TextAttributes.nodeSubstr(cursorNode, 0, offset) +
546 str + hterm.TextAttributes.nodeSubstr(cursorNode, offset);
rgindaa19afe22012-01-25 15:40:22 -0800547 }
rginda8ba33642011-12-14 12:31:31 -0800548
Mike Frysinger6380bed2017-08-24 18:46:39 -0400549 this.cursorOffset_ += wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800550 return;
rginda87b86462011-12-14 13:48:03 -0800551 }
552
rgindaa19afe22012-01-25 15:40:22 -0800553 // The cursor node is the wrong style for the new text. If we're at the
554 // beginning or end of the cursor node, then the adjacent node is also a
555 // potential candidate.
rginda8ba33642011-12-14 12:31:31 -0800556
rgindaa19afe22012-01-25 15:40:22 -0800557 if (offset == 0) {
558 // At the beginning of the cursor node, the check the previous sibling.
559 var previousSibling = cursorNode.previousSibling;
560 if (previousSibling &&
561 this.textAttributes.matchesContainer(previousSibling)) {
562 previousSibling.textContent += str;
563 this.cursorNode_ = previousSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800564 this.cursorOffset_ = lib.wc.strWidth(previousSibling.textContent);
rgindaa19afe22012-01-25 15:40:22 -0800565 return;
566 }
567
568 var newNode = this.textAttributes.createContainer(str);
569 this.cursorRowNode_.insertBefore(newNode, cursorNode);
570 this.cursorNode_ = newNode;
Mike Frysinger6380bed2017-08-24 18:46:39 -0400571 this.cursorOffset_ = wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800572 return;
573 }
574
575 if (reverseOffset == 0) {
576 // At the end of the cursor node, the check the next sibling.
577 var nextSibling = cursorNode.nextSibling;
578 if (nextSibling &&
579 this.textAttributes.matchesContainer(nextSibling)) {
580 nextSibling.textContent = str + nextSibling.textContent;
581 this.cursorNode_ = nextSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800582 this.cursorOffset_ = lib.wc.strWidth(str);
rgindaa19afe22012-01-25 15:40:22 -0800583 return;
584 }
585
586 var newNode = this.textAttributes.createContainer(str);
587 this.cursorRowNode_.insertBefore(newNode, nextSibling);
588 this.cursorNode_ = newNode;
589 // We specifically need to include any missing whitespace here, since it's
590 // going in a new node.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800591 this.cursorOffset_ = hterm.TextAttributes.nodeWidth(newNode);
rgindaa19afe22012-01-25 15:40:22 -0800592 return;
593 }
594
595 // Worst case, we're somewhere in the middle of the cursor node. We'll
596 // have to split it into two nodes and insert our new container in between.
597 this.splitNode_(cursorNode, offset);
598 var newNode = this.textAttributes.createContainer(str);
599 this.cursorRowNode_.insertBefore(newNode, cursorNode.nextSibling);
600 this.cursorNode_ = newNode;
Mike Frysinger6380bed2017-08-24 18:46:39 -0400601 this.cursorOffset_ = wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800602};
603
604/**
rginda8ba33642011-12-14 12:31:31 -0800605 * Overwrite the text at the current cursor position.
606 *
rgindaa09e7332012-08-17 12:49:51 -0700607 * You must call maybeClipCurrentRow() after in order to clip overflowed
608 * text and clamp the cursor.
609 *
610 * It is also up to the caller to properly maintain the line overflow state
611 * using hterm.Screen..commitLineOverflow().
Joel Hockey0f933582019-08-27 18:01:51 -0700612 *
613 * @param {string} str The source string for overwriting existing content.
614 * @param {number=} wcwidth The cached lib.wc.strWidth value for |str|. Will be
615 * calculated on demand if need be. Passing in a cached value helps speed
616 * up processing as this is a hot codepath.
rginda8ba33642011-12-14 12:31:31 -0800617 */
Mike Frysinger6380bed2017-08-24 18:46:39 -0400618hterm.Screen.prototype.overwriteString = function(str, wcwidth=undefined) {
rginda8ba33642011-12-14 12:31:31 -0800619 var maxLength = this.columnCount_ - this.cursorPosition.column;
620 if (!maxLength)
Mike Frysinger159b7392019-03-26 11:08:32 -0700621 return;
rgindaa19afe22012-01-25 15:40:22 -0800622
Mike Frysinger6380bed2017-08-24 18:46:39 -0400623 if (wcwidth === undefined)
624 wcwidth = lib.wc.strWidth(str);
625
Ricky Liang48f05cb2013-12-31 23:35:29 +0800626 if (this.textAttributes.matchesContainer(this.cursorNode_) &&
627 this.cursorNode_.textContent.substr(this.cursorOffset_) == str) {
rgindaa19afe22012-01-25 15:40:22 -0800628 // This overwrite would be a no-op, just move the cursor and return.
Mike Frysinger6380bed2017-08-24 18:46:39 -0400629 this.cursorOffset_ += wcwidth;
630 this.cursorPosition.column += wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800631 return;
632 }
rginda8ba33642011-12-14 12:31:31 -0800633
Mike Frysinger6380bed2017-08-24 18:46:39 -0400634 this.deleteChars(Math.min(wcwidth, maxLength));
635 this.insertString(str, wcwidth);
rginda8ba33642011-12-14 12:31:31 -0800636};
637
638/**
639 * Forward-delete one or more characters at the current cursor position.
640 *
641 * Text to the right of the deleted characters is shifted left. Only affects
642 * characters on the same row as the cursor.
643 *
Joel Hockey0f933582019-08-27 18:01:51 -0700644 * @param {number} count The column width of characters to delete. This is
Ricky Liang48f05cb2013-12-31 23:35:29 +0800645 * clamped to the column width minus the cursor column.
Joel Hockey0f933582019-08-27 18:01:51 -0700646 * @return {number} The column width of the characters actually deleted.
rginda8ba33642011-12-14 12:31:31 -0800647 */
648hterm.Screen.prototype.deleteChars = function(count) {
649 var node = this.cursorNode_;
650 var offset = this.cursorOffset_;
651
Robert Ginda7fd57082012-09-25 14:41:47 -0700652 var currentCursorColumn = this.cursorPosition.column;
653 count = Math.min(count, this.columnCount_ - currentCursorColumn);
654 if (!count)
655 return 0;
656
657 var rv = count;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800658 var startLength, endLength;
rgindaa19afe22012-01-25 15:40:22 -0800659
rginda8ba33642011-12-14 12:31:31 -0800660 while (node && count) {
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400661 // Sanity check so we don't loop forever, but we don't also go quietly.
662 if (count < 0) {
663 console.error(`Deleting ${rv} chars went negative: ${count}`);
664 break;
665 }
666
Ricky Liang48f05cb2013-12-31 23:35:29 +0800667 startLength = hterm.TextAttributes.nodeWidth(node);
668 node.textContent = hterm.TextAttributes.nodeSubstr(node, 0, offset) +
669 hterm.TextAttributes.nodeSubstr(node, offset + count);
670 endLength = hterm.TextAttributes.nodeWidth(node);
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400671
672 // Deal with splitting wide characters. There are two ways: we could delete
673 // the first column or the second column. In both cases, we delete the wide
674 // character and replace one of the columns with a space (since the other
675 // was deleted). If there are more chars to delete, the next loop will pick
676 // up the slack.
677 if (node.wcNode && offset < startLength &&
678 ((endLength && startLength == endLength) || (!endLength && offset == 1))) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800679 // No characters were deleted when there should be. We're probably trying
680 // to delete one column width from a wide character node. We remove the
681 // wide character node here and replace it with a single space.
682 var spaceNode = this.textAttributes.createContainer(' ');
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400683 node.parentNode.insertBefore(spaceNode, offset ? node : node.nextSibling);
Ricky Liang48f05cb2013-12-31 23:35:29 +0800684 node.textContent = '';
685 endLength = 0;
686 count -= 1;
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400687 } else
688 count -= startLength - endLength;
rginda8ba33642011-12-14 12:31:31 -0800689
Ricky Liang48f05cb2013-12-31 23:35:29 +0800690 var nextNode = node.nextSibling;
691 if (endLength == 0 && node != this.cursorNode_) {
692 node.parentNode.removeChild(node);
693 }
694 node = nextNode;
rginda8ba33642011-12-14 12:31:31 -0800695 offset = 0;
696 }
Robert Ginda7fd57082012-09-25 14:41:47 -0700697
Ricky Liang48f05cb2013-12-31 23:35:29 +0800698 // Remove this.cursorNode_ if it is an empty non-text node.
Mike Frysinger6a4f2412017-08-31 01:11:25 -0400699 if (this.cursorNode_.nodeType != Node.TEXT_NODE &&
700 !this.cursorNode_.textContent) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800701 var cursorNode = this.cursorNode_;
702 if (cursorNode.previousSibling) {
703 this.cursorNode_ = cursorNode.previousSibling;
704 this.cursorOffset_ = hterm.TextAttributes.nodeWidth(
705 cursorNode.previousSibling);
706 } else if (cursorNode.nextSibling) {
707 this.cursorNode_ = cursorNode.nextSibling;
708 this.cursorOffset_ = 0;
709 } else {
710 var emptyNode = this.cursorRowNode_.ownerDocument.createTextNode('');
711 this.cursorRowNode_.appendChild(emptyNode);
712 this.cursorNode_ = emptyNode;
713 this.cursorOffset_ = 0;
714 }
715 this.cursorRowNode_.removeChild(cursorNode);
716 }
717
Robert Ginda7fd57082012-09-25 14:41:47 -0700718 return rv;
rginda8ba33642011-12-14 12:31:31 -0800719};
John Macinnesfb683832013-07-22 14:46:30 -0400720
721/**
722 * Finds first X-ROW of a line containing specified X-ROW.
723 * Used to support line overflow.
724 *
Joel Hockey0f933582019-08-27 18:01:51 -0700725 * @param {!Node} row X-ROW to begin search for first row of line.
726 * @return {!Node} The X-ROW that is at the beginning of the line.
John Macinnesfb683832013-07-22 14:46:30 -0400727 **/
728hterm.Screen.prototype.getLineStartRow_ = function(row) {
729 while (row.previousSibling &&
730 row.previousSibling.hasAttribute('line-overflow')) {
731 row = row.previousSibling;
732 }
733 return row;
734};
735
736/**
737 * Gets text of a line beginning with row.
738 * Supports line overflow.
739 *
Joel Hockey0f933582019-08-27 18:01:51 -0700740 * @param {!Node} row First X-ROW of line.
John Macinnesfb683832013-07-22 14:46:30 -0400741 * @return {string} Text content of line.
742 **/
743hterm.Screen.prototype.getLineText_ = function(row) {
744 var rowText = "";
745 while (row) {
746 rowText += row.textContent;
747 if (row.hasAttribute('line-overflow')) {
748 row = row.nextSibling;
749 } else {
750 break;
751 }
752 }
753 return rowText;
754};
755
756/**
757 * Returns X-ROW that is ancestor of the node.
758 *
Joel Hockey0f933582019-08-27 18:01:51 -0700759 * @param {!Node} node Node to get X-ROW ancestor for.
760 * @return {!Node} X-ROW ancestor of node, or null if not found.
John Macinnesfb683832013-07-22 14:46:30 -0400761 **/
762hterm.Screen.prototype.getXRowAncestor_ = function(node) {
763 while (node) {
764 if (node.nodeName === 'X-ROW')
765 break;
766 node = node.parentNode;
767 }
768 return node;
769};
770
771/**
772 * Returns position within line of character at offset within node.
773 * Supports line overflow.
774 *
Joel Hockey0f933582019-08-27 18:01:51 -0700775 * @param {!Node} row X-ROW at beginning of line.
776 * @param {!Node} node Node to get position of.
777 * @param {number} offset Offset into node.
778 * @return {number} Position within line of character at offset within node.
John Macinnesfb683832013-07-22 14:46:30 -0400779 **/
780hterm.Screen.prototype.getPositionWithOverflow_ = function(row, node, offset) {
781 if (!node)
782 return -1;
783 var ancestorRow = this.getXRowAncestor_(node);
784 if (!ancestorRow)
785 return -1;
786 var position = 0;
787 while (ancestorRow != row) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800788 position += hterm.TextAttributes.nodeWidth(row);
John Macinnesfb683832013-07-22 14:46:30 -0400789 if (row.hasAttribute('line-overflow') && row.nextSibling) {
790 row = row.nextSibling;
791 } else {
792 return -1;
793 }
794 }
795 return position + this.getPositionWithinRow_(row, node, offset);
796};
797
798/**
799 * Returns position within row of character at offset within node.
800 * Does not support line overflow.
801 *
Joel Hockey0f933582019-08-27 18:01:51 -0700802 * @param {!Node} row X-ROW to get position within.
803 * @param {!Node} node Node to get position for.
804 * @param {number} offset Offset within node to get position for.
805 * @return {number} Position within row of character at offset within node.
John Macinnesfb683832013-07-22 14:46:30 -0400806 **/
807hterm.Screen.prototype.getPositionWithinRow_ = function(row, node, offset) {
808 if (node.parentNode != row) {
Mike Frysinger498192d2017-06-26 18:23:31 -0400809 // If we traversed to the top node, then there's nothing to find here.
810 if (node.parentNode == null)
811 return -1;
812
John Macinnesfb683832013-07-22 14:46:30 -0400813 return this.getPositionWithinRow_(node.parentNode, node, offset) +
814 this.getPositionWithinRow_(row, node.parentNode, 0);
815 }
816 var position = 0;
817 for (var i = 0; i < row.childNodes.length; i++) {
818 var currentNode = row.childNodes[i];
819 if (currentNode == node)
820 return position + offset;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800821 position += hterm.TextAttributes.nodeWidth(currentNode);
John Macinnesfb683832013-07-22 14:46:30 -0400822 }
823 return -1;
824};
825
826/**
827 * Returns the node and offset corresponding to position within line.
828 * Supports line overflow.
829 *
Joel Hockey0f933582019-08-27 18:01:51 -0700830 * @param {!Node} row X-ROW at beginning of line.
831 * @param {number} position Position within line to retrieve node and offset.
832 * @return {!Array} Two element array containing node and offset respectively.
John Macinnesfb683832013-07-22 14:46:30 -0400833 **/
834hterm.Screen.prototype.getNodeAndOffsetWithOverflow_ = function(row, position) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800835 while (row && position > hterm.TextAttributes.nodeWidth(row)) {
John Macinnesfb683832013-07-22 14:46:30 -0400836 if (row.hasAttribute('line-overflow') && row.nextSibling) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800837 position -= hterm.TextAttributes.nodeWidth(row);
John Macinnesfb683832013-07-22 14:46:30 -0400838 row = row.nextSibling;
839 } else {
840 return -1;
841 }
842 }
843 return this.getNodeAndOffsetWithinRow_(row, position);
844};
845
846/**
847 * Returns the node and offset corresponding to position within row.
848 * Does not support line overflow.
849 *
Joel Hockey0f933582019-08-27 18:01:51 -0700850 * @param {!Node} row X-ROW to get position within.
851 * @param {number} position Position within row to retrieve node and offset.
852 * @return {!Array} Two element array containing node and offset respectively.
John Macinnesfb683832013-07-22 14:46:30 -0400853 **/
854hterm.Screen.prototype.getNodeAndOffsetWithinRow_ = function(row, position) {
855 for (var i = 0; i < row.childNodes.length; i++) {
856 var node = row.childNodes[i];
Ricky Liang48f05cb2013-12-31 23:35:29 +0800857 var nodeTextWidth = hterm.TextAttributes.nodeWidth(node);
858 if (position <= nodeTextWidth) {
John Macinnesfb683832013-07-22 14:46:30 -0400859 if (node.nodeName === 'SPAN') {
860 /** Drill down to node contained by SPAN. **/
861 return this.getNodeAndOffsetWithinRow_(node, position);
862 } else {
863 return [node, position];
864 }
865 }
Ricky Liang48f05cb2013-12-31 23:35:29 +0800866 position -= nodeTextWidth;
John Macinnesfb683832013-07-22 14:46:30 -0400867 }
868 return null;
869};
870
871/**
872 * Returns the node and offset corresponding to position within line.
873 * Supports line overflow.
874 *
Joel Hockey0f933582019-08-27 18:01:51 -0700875 * @param {!Node} row X-ROW at beginning of line.
876 * @param {number} start Start position of range within line.
877 * @param {number} end End position of range within line.
878 * @param {!Range} range Range to modify.
John Macinnesfb683832013-07-22 14:46:30 -0400879 **/
880hterm.Screen.prototype.setRange_ = function(row, start, end, range) {
881 var startNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, start);
882 if (startNodeAndOffset == null)
883 return;
884 var endNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, end);
885 if (endNodeAndOffset == null)
886 return;
887 range.setStart(startNodeAndOffset[0], startNodeAndOffset[1]);
888 range.setEnd(endNodeAndOffset[0], endNodeAndOffset[1]);
889};
890
891/**
John Lincae9b732018-03-08 13:56:35 +0800892 * Expands selection to surrounding string with word break matches.
John Macinnesfb683832013-07-22 14:46:30 -0400893 *
Joel Hockey0f933582019-08-27 18:01:51 -0700894 * @param {!Selection} selection Selection to expand.
John Lincae9b732018-03-08 13:56:35 +0800895 * @param {string} leftMatch left word break match.
896 * @param {string} rightMatch right word break match.
897 * @param {string} insideMatch inside word break match.
898 */
899hterm.Screen.prototype.expandSelectionWithWordBreakMatches_ =
900 function(selection, leftMatch, rightMatch, insideMatch) {
John Macinnesfb683832013-07-22 14:46:30 -0400901 if (!selection)
902 return;
903
904 var range = selection.getRangeAt(0);
905 if (!range || range.toString().match(/\s/))
906 return;
907
Raymes Khoury334625a2018-06-25 10:29:40 +1000908 const rowElement = this.getXRowAncestor_(range.startContainer);
909 if (!rowElement)
910 return;
911 const row = this.getLineStartRow_(rowElement);
John Macinnesfb683832013-07-22 14:46:30 -0400912 if (!row)
913 return;
914
915 var startPosition = this.getPositionWithOverflow_(row,
916 range.startContainer,
917 range.startOffset);
918 if (startPosition == -1)
919 return;
920 var endPosition = this.getPositionWithOverflow_(row,
921 range.endContainer,
922 range.endOffset);
923 if (endPosition == -1)
924 return;
925
John Macinnesfb683832013-07-22 14:46:30 -0400926 //Move start to the left.
927 var rowText = this.getLineText_(row);
Ricky Liang48f05cb2013-12-31 23:35:29 +0800928 var lineUpToRange = lib.wc.substring(rowText, 0, endPosition);
Robert Ginda5eba4562014-08-11 11:05:54 -0700929 var leftRegularExpression = new RegExp(leftMatch + insideMatch + "$");
John Macinnesfb683832013-07-22 14:46:30 -0400930 var expandedStart = lineUpToRange.search(leftRegularExpression);
931 if (expandedStart == -1 || expandedStart > startPosition)
932 return;
933
934 //Move end to the right.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800935 var lineFromRange = lib.wc.substring(rowText, startPosition,
936 lib.wc.strWidth(rowText));
Robert Ginda5eba4562014-08-11 11:05:54 -0700937 var rightRegularExpression = new RegExp("^" + insideMatch + rightMatch);
John Macinnesfb683832013-07-22 14:46:30 -0400938 var found = lineFromRange.match(rightRegularExpression);
939 if (!found)
940 return;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800941 var expandedEnd = startPosition + lib.wc.strWidth(found[0]);
John Macinnesfb683832013-07-22 14:46:30 -0400942 if (expandedEnd == -1 || expandedEnd < endPosition)
943 return;
944
945 this.setRange_(row, expandedStart, expandedEnd, range);
946 selection.addRange(range);
947};
Mike Frysingera2cacaa2017-11-29 13:51:09 -0800948
949/**
John Lincae9b732018-03-08 13:56:35 +0800950 * Expands selection to surrounding string using the user's settings.
951 *
Joel Hockey0f933582019-08-27 18:01:51 -0700952 * @param {!Selection} selection Selection to expand.
John Lincae9b732018-03-08 13:56:35 +0800953 */
954hterm.Screen.prototype.expandSelection = function(selection) {
955 this.expandSelectionWithWordBreakMatches_(
956 selection,
957 this.wordBreakMatchLeft,
958 this.wordBreakMatchRight,
959 this.wordBreakMatchMiddle);
Mike Frysinger8416e0a2017-05-17 09:09:46 -0400960};
John Lincae9b732018-03-08 13:56:35 +0800961
962/**
963 * Expands selection to surrounding URL using a set of fixed match settings.
964 *
Joel Hockey0f933582019-08-27 18:01:51 -0700965 * @param {!Selection} selection Selection to expand.
John Lincae9b732018-03-08 13:56:35 +0800966 */
967hterm.Screen.prototype.expandSelectionForUrl = function(selection) {
968 this.expandSelectionWithWordBreakMatches_(
969 selection,
970 "[^\\s\\[\\](){}<>\"'\\^!@#$%&*,;:`]",
971 "[^\\s\\[\\](){}<>\"'\\^!@#$%&*,;:~.`]",
972 "[^\\s\\[\\](){}<>\"'\\^]*");
973};
974
975/**
Mike Frysingera2cacaa2017-11-29 13:51:09 -0800976 * Save the current cursor state to the corresponding screens.
977 *
Joel Hockey0f933582019-08-27 18:01:51 -0700978 * @param {!hterm.VT} vt The VT object to read graphic codeset details from.
Mike Frysingera2cacaa2017-11-29 13:51:09 -0800979 */
980hterm.Screen.prototype.saveCursorAndState = function(vt) {
981 this.cursorState_.save(vt);
982};
983
984/**
985 * Restore the saved cursor state in the corresponding screens.
986 *
Joel Hockey0f933582019-08-27 18:01:51 -0700987 * @param {!hterm.VT} vt The VT object to write graphic codeset details to.
Mike Frysingera2cacaa2017-11-29 13:51:09 -0800988 */
989hterm.Screen.prototype.restoreCursorAndState = function(vt) {
990 this.cursorState_.restore(vt);
991};
992
993/**
994 * Track all the things related to the current "cursor".
995 *
996 * The set of things saved & restored here is defined by DEC:
997 * https://vt100.net/docs/vt510-rm/DECSC.html
998 * - Cursor position
999 * - Character attributes set by the SGR command
1000 * - Character sets (G0, G1, G2, or G3) currently in GL and GR
1001 * - Wrap flag (autowrap or no autowrap)
1002 * - State of origin mode (DECOM)
1003 * - Selective erase attribute
1004 * - Any single shift 2 (SS2) or single shift 3 (SS3) functions sent
1005 *
1006 * These are done on a per-screen basis.
Joel Hockey0f933582019-08-27 18:01:51 -07001007 *
1008 * @param {!hterm.Screen} screen The screen this cursor is tied to.
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001009 */
1010hterm.Screen.CursorState = function(screen) {
1011 this.screen_ = screen;
1012 this.cursor = null;
1013 this.textAttributes = null;
1014 this.GL = this.GR = this.G0 = this.G1 = this.G2 = this.G3 = null;
1015};
1016
1017/**
1018 * Save all the cursor state.
1019 *
Joel Hockey0f933582019-08-27 18:01:51 -07001020 * @param {!hterm.VT} vt The VT object to read graphic codeset details from.
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001021 */
1022hterm.Screen.CursorState.prototype.save = function(vt) {
1023 this.cursor = vt.terminal.saveCursor();
1024
1025 this.textAttributes = this.screen_.textAttributes.clone();
1026
1027 this.GL = vt.GL;
1028 this.GR = vt.GR;
1029
1030 this.G0 = vt.G0;
1031 this.G1 = vt.G1;
1032 this.G2 = vt.G2;
1033 this.G3 = vt.G3;
1034};
1035
1036/**
1037 * Restore the previously saved cursor state.
1038 *
Joel Hockey0f933582019-08-27 18:01:51 -07001039 * @param {!hterm.VT} vt The VT object to write graphic codeset details to.
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001040 */
1041hterm.Screen.CursorState.prototype.restore = function(vt) {
1042 vt.terminal.restoreCursor(this.cursor);
1043
1044 // Cursor restore includes char attributes (bold/etc...), but does not change
1045 // the color palette (which are a terminal setting).
1046 const tattrs = this.textAttributes.clone();
1047 tattrs.colorPalette = this.screen_.textAttributes.colorPalette;
1048 tattrs.syncColors();
1049
1050 this.screen_.textAttributes = tattrs;
1051
1052 vt.GL = this.GL;
1053 vt.GR = this.GR;
1054
1055 vt.G0 = this.G0;
1056 vt.G1 = this.G1;
1057 vt.G2 = this.G2;
1058 vt.G3 = this.G3;
1059};