blob: 62c4a69625aaead7046e5568b0e9b65a14cba748 [file] [log] [blame]
rginda87b86462011-12-14 13:48:03 -08001// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
rginda8ba33642011-12-14 12:31:31 -08002// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
rgindacbbd7482012-06-13 15:06:16 -07005'use strict';
6
rginda8ba33642011-12-14 12:31:31 -08007/**
8 * @fileoverview This class represents a single terminal screen full of text.
9 *
10 * It maintains the current cursor position and has basic methods for text
11 * insert and overwrite, and adding or removing rows from the screen.
12 *
13 * This class has no knowledge of the scrollback buffer.
14 *
15 * The number of rows on the screen is determined only by the number of rows
16 * that the caller inserts into the screen. If a caller wants to ensure a
17 * constant number of rows on the screen, it's their responsibility to remove a
18 * row for each row inserted.
19 *
20 * The screen width, in contrast, is enforced locally.
21 *
22 *
23 * In practice...
24 * - The hterm.Terminal class holds two hterm.Screen instances. One for the
25 * primary screen and one for the alternate screen.
26 *
Joel Hockey0f933582019-08-27 18:01:51 -070027 * - The html.Screen class only cares that rows are HTML Elements. In the
rginda8ba33642011-12-14 12:31:31 -080028 * larger context of hterm, however, the rows happen to be displayed by an
29 * hterm.ScrollPort and have to follow a few rules as a result. Each
30 * row must be rooted by the custom HTML tag 'x-row', and each must have a
31 * rowIndex property that corresponds to the index of the row in the context
32 * of the scrollback buffer. These invariants are enforced by hterm.Terminal
33 * because that is the class using the hterm.Screen in the context of an
34 * hterm.ScrollPort.
35 */
36
37/**
38 * Create a new screen instance.
39 *
40 * The screen initially has no rows and a maximum column count of 0.
41 *
Joel Hockey0f933582019-08-27 18:01:51 -070042 * @param {number=} columnCount The maximum number of columns for this
rginda8ba33642011-12-14 12:31:31 -080043 * screen. See insertString() and overwriteString() for information about
44 * what happens when too many characters are added too a row. Defaults to
45 * 0 if not provided.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -070046 * @constructor
rginda8ba33642011-12-14 12:31:31 -080047 */
Mike Frysinger60a156d2019-06-13 10:15:45 -040048hterm.Screen = function(columnCount=0) {
rginda8ba33642011-12-14 12:31:31 -080049 /**
50 * Public, read-only access to the rows in this screen.
Mike Frysinger23b5b832019-10-01 17:05:29 -040051 *
Joel Hockeyadd2f7e2019-09-20 16:37:35 -070052 * @type {!Array<!Element>}
rginda8ba33642011-12-14 12:31:31 -080053 */
54 this.rowsArray = [];
55
56 // The max column width for this screen.
Mike Frysinger60a156d2019-06-13 10:15:45 -040057 this.columnCount_ = columnCount;
rginda8ba33642011-12-14 12:31:31 -080058
rgindaa19afe22012-01-25 15:40:22 -080059 // The current color, bold, underline and blink attributes.
60 this.textAttributes = new hterm.TextAttributes(window.document);
61
rginda87b86462011-12-14 13:48:03 -080062 // Current zero-based cursor coordinates.
63 this.cursorPosition = new hterm.RowCol(0, 0);
rginda8ba33642011-12-14 12:31:31 -080064
Mike Frysingera2cacaa2017-11-29 13:51:09 -080065 // Saved state used by DECSC and related settings. This is only for saving
66 // and restoring specific state, not for the current/active state.
67 this.cursorState_ = new hterm.Screen.CursorState(this);
68
rginda8ba33642011-12-14 12:31:31 -080069 // The node containing the row that the cursor is positioned on.
70 this.cursorRowNode_ = null;
71
72 // The node containing the span of text that the cursor is positioned on.
73 this.cursorNode_ = null;
74
Ricky Liang48f05cb2013-12-31 23:35:29 +080075 // The offset in column width into cursorNode_ where the cursor is positioned.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -070076 this.cursorOffset_ = 0;
Mike Frysinger664e9992017-05-19 01:24:24 -040077
78 // Regexes for expanding word selections.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -070079 /** @type {?string} */
Mike Frysinger664e9992017-05-19 01:24:24 -040080 this.wordBreakMatchLeft = null;
Joel Hockeyadd2f7e2019-09-20 16:37:35 -070081 /** @type {?string} */
Mike Frysinger664e9992017-05-19 01:24:24 -040082 this.wordBreakMatchRight = null;
Joel Hockeyadd2f7e2019-09-20 16:37:35 -070083 /** @type {?string} */
Mike Frysinger664e9992017-05-19 01:24:24 -040084 this.wordBreakMatchMiddle = null;
rginda8ba33642011-12-14 12:31:31 -080085};
86
87/**
88 * Return the screen size as an hterm.Size object.
89 *
Joel Hockey0f933582019-08-27 18:01:51 -070090 * @return {!hterm.Size} hterm.Size object representing the current number
rginda8ba33642011-12-14 12:31:31 -080091 * of rows and columns in this screen.
92 */
93hterm.Screen.prototype.getSize = function() {
94 return new hterm.Size(this.columnCount_, this.rowsArray.length);
95};
96
97/**
98 * Return the current number of rows in this screen.
99 *
Joel Hockey0f933582019-08-27 18:01:51 -0700100 * @return {number} The number of rows in this screen.
rginda8ba33642011-12-14 12:31:31 -0800101 */
102hterm.Screen.prototype.getHeight = function() {
103 return this.rowsArray.length;
104};
105
106/**
107 * Return the current number of columns in this screen.
108 *
Joel Hockey0f933582019-08-27 18:01:51 -0700109 * @return {number} The number of columns in this screen.
rginda8ba33642011-12-14 12:31:31 -0800110 */
111hterm.Screen.prototype.getWidth = function() {
112 return this.columnCount_;
113};
114
115/**
116 * Set the maximum number of columns per row.
117 *
Joel Hockey0f933582019-08-27 18:01:51 -0700118 * @param {number} count The maximum number of columns per row.
rginda8ba33642011-12-14 12:31:31 -0800119 */
120hterm.Screen.prototype.setColumnCount = function(count) {
rginda2312fff2012-01-05 16:20:52 -0800121 this.columnCount_ = count;
122
rgindacbbd7482012-06-13 15:06:16 -0700123 if (this.cursorPosition.column >= count)
124 this.setCursorPosition(this.cursorPosition.row, count - 1);
rginda8ba33642011-12-14 12:31:31 -0800125};
126
127/**
128 * Remove the first row from the screen and return it.
129 *
Joel Hockey0f933582019-08-27 18:01:51 -0700130 * @return {!Element} The first row in this screen.
rginda8ba33642011-12-14 12:31:31 -0800131 */
132hterm.Screen.prototype.shiftRow = function() {
133 return this.shiftRows(1)[0];
rginda87b86462011-12-14 13:48:03 -0800134};
rginda8ba33642011-12-14 12:31:31 -0800135
136/**
137 * Remove rows from the top of the screen and return them as an array.
138 *
Joel Hockey0f933582019-08-27 18:01:51 -0700139 * @param {number} count The number of rows to remove.
140 * @return {!Array<!Element>} The selected rows.
rginda8ba33642011-12-14 12:31:31 -0800141 */
142hterm.Screen.prototype.shiftRows = function(count) {
143 return this.rowsArray.splice(0, count);
144};
145
146/**
147 * Insert a row at the top of the screen.
148 *
Joel Hockey0f933582019-08-27 18:01:51 -0700149 * @param {!Element} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800150 */
151hterm.Screen.prototype.unshiftRow = function(row) {
152 this.rowsArray.splice(0, 0, row);
153};
154
155/**
156 * Insert rows at the top of the screen.
157 *
Joel Hockey0f933582019-08-27 18:01:51 -0700158 * @param {!Array<!Element>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800159 */
160hterm.Screen.prototype.unshiftRows = function(rows) {
161 this.rowsArray.unshift.apply(this.rowsArray, rows);
162};
163
164/**
165 * Remove the last row from the screen and return it.
166 *
Joel Hockey0f933582019-08-27 18:01:51 -0700167 * @return {!Element} The last row in this screen.
rginda8ba33642011-12-14 12:31:31 -0800168 */
169hterm.Screen.prototype.popRow = function() {
170 return this.popRows(1)[0];
171};
172
173/**
174 * Remove rows from the bottom of the screen and return them as an array.
175 *
Joel Hockey0f933582019-08-27 18:01:51 -0700176 * @param {number} count The number of rows to remove.
177 * @return {!Array<!Element>} The selected rows.
rginda8ba33642011-12-14 12:31:31 -0800178 */
179hterm.Screen.prototype.popRows = function(count) {
180 return this.rowsArray.splice(this.rowsArray.length - count, count);
181};
182
183/**
184 * Insert a row at the bottom of the screen.
185 *
Joel Hockey0f933582019-08-27 18:01:51 -0700186 * @param {!Element} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800187 */
188hterm.Screen.prototype.pushRow = function(row) {
189 this.rowsArray.push(row);
190};
191
192/**
193 * Insert rows at the bottom of the screen.
194 *
Joel Hockey0f933582019-08-27 18:01:51 -0700195 * @param {!Array<!Element>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800196 */
197hterm.Screen.prototype.pushRows = function(rows) {
198 rows.push.apply(this.rowsArray, rows);
199};
200
201/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500202 * Insert a row at the specified row of the screen.
rginda8ba33642011-12-14 12:31:31 -0800203 *
Joel Hockey0f933582019-08-27 18:01:51 -0700204 * @param {number} index The index to insert the row.
205 * @param {!Element} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800206 */
207hterm.Screen.prototype.insertRow = function(index, row) {
208 this.rowsArray.splice(index, 0, row);
209};
210
211/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500212 * Insert rows at the specified row of the screen.
rginda8ba33642011-12-14 12:31:31 -0800213 *
Joel Hockey0f933582019-08-27 18:01:51 -0700214 * @param {number} index The index to insert the rows.
215 * @param {!Array<!Element>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800216 */
217hterm.Screen.prototype.insertRows = function(index, rows) {
218 for (var i = 0; i < rows.length; i++) {
219 this.rowsArray.splice(index + i, 0, rows[i]);
220 }
221};
222
223/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500224 * Remove a row from the screen and return it.
rginda8ba33642011-12-14 12:31:31 -0800225 *
Joel Hockey0f933582019-08-27 18:01:51 -0700226 * @param {number} index The index of the row to remove.
227 * @return {!Element} The selected row.
rginda8ba33642011-12-14 12:31:31 -0800228 */
229hterm.Screen.prototype.removeRow = function(index) {
230 return this.rowsArray.splice(index, 1)[0];
231};
232
233/**
234 * Remove rows from the bottom of the screen and return them as an array.
235 *
Joel Hockey0f933582019-08-27 18:01:51 -0700236 * @param {number} index The index to start removing rows.
237 * @param {number} count The number of rows to remove.
238 * @return {!Array<!Element>} The selected rows.
rginda8ba33642011-12-14 12:31:31 -0800239 */
240hterm.Screen.prototype.removeRows = function(index, count) {
241 return this.rowsArray.splice(index, count);
242};
243
244/**
245 * Invalidate the current cursor position.
246 *
rginda87b86462011-12-14 13:48:03 -0800247 * This sets this.cursorPosition to (0, 0) and clears out some internal
rginda8ba33642011-12-14 12:31:31 -0800248 * data.
249 *
250 * Attempting to insert or overwrite text while the cursor position is invalid
251 * will raise an obscure exception.
252 */
253hterm.Screen.prototype.invalidateCursorPosition = function() {
rginda87b86462011-12-14 13:48:03 -0800254 this.cursorPosition.move(0, 0);
rginda8ba33642011-12-14 12:31:31 -0800255 this.cursorRowNode_ = null;
256 this.cursorNode_ = null;
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700257 this.cursorOffset_ = 0;
rginda8ba33642011-12-14 12:31:31 -0800258};
259
260/**
rginda8ba33642011-12-14 12:31:31 -0800261 * Clear the contents of the cursor row.
rginda8ba33642011-12-14 12:31:31 -0800262 */
263hterm.Screen.prototype.clearCursorRow = function() {
264 this.cursorRowNode_.innerHTML = '';
rgindaa09e7332012-08-17 12:49:51 -0700265 this.cursorRowNode_.removeAttribute('line-overflow');
rginda8ba33642011-12-14 12:31:31 -0800266 this.cursorOffset_ = 0;
rginda8ba33642011-12-14 12:31:31 -0800267 this.cursorPosition.column = 0;
rginda2312fff2012-01-05 16:20:52 -0800268 this.cursorPosition.overflow = false;
Robert Ginda7fd57082012-09-25 14:41:47 -0700269
270 var text;
271 if (this.textAttributes.isDefault()) {
272 text = '';
273 } else {
Mike Frysinger73e56462019-07-17 00:23:46 -0500274 text = ' '.repeat(this.columnCount_);
Robert Ginda7fd57082012-09-25 14:41:47 -0700275 }
276
Zhu Qunying30d40712017-03-14 16:27:00 -0700277 // We shouldn't honor inverse colors when clearing an area, to match
278 // xterm's back color erase behavior.
Edoardo Spadolini2fd43642014-08-23 22:59:57 +0200279 var inverse = this.textAttributes.inverse;
280 this.textAttributes.inverse = false;
281 this.textAttributes.syncColors();
282
Robert Ginda7fd57082012-09-25 14:41:47 -0700283 var node = this.textAttributes.createContainer(text);
284 this.cursorRowNode_.appendChild(node);
285 this.cursorNode_ = node;
Edoardo Spadolini2fd43642014-08-23 22:59:57 +0200286
287 this.textAttributes.inverse = inverse;
288 this.textAttributes.syncColors();
rginda8ba33642011-12-14 12:31:31 -0800289};
290
291/**
rgindaa09e7332012-08-17 12:49:51 -0700292 * Mark the current row as having overflowed to the next line.
293 *
294 * The line overflow state is used when converting a range of rows into text.
295 * It makes it possible to recombine two or more overflow terminal rows into
296 * a single line.
297 *
298 * This is distinct from the cursor being in the overflow state. Cursor
299 * overflow indicates that printing at the cursor position will commit a
300 * line overflow, unless it is preceded by a repositioning of the cursor
301 * to a non-overflow state.
302 */
303hterm.Screen.prototype.commitLineOverflow = function() {
304 this.cursorRowNode_.setAttribute('line-overflow', true);
305};
306
307/**
rginda8ba33642011-12-14 12:31:31 -0800308 * Relocate the cursor to a give row and column.
309 *
Joel Hockey0f933582019-08-27 18:01:51 -0700310 * @param {number} row The zero based row.
311 * @param {number} column The zero based column.
rginda8ba33642011-12-14 12:31:31 -0800312 */
313hterm.Screen.prototype.setCursorPosition = function(row, column) {
rginda11057d52012-04-25 12:29:56 -0700314 if (!this.rowsArray.length) {
315 console.warn('Attempt to set cursor position on empty screen.');
316 return;
317 }
318
rginda87b86462011-12-14 13:48:03 -0800319 if (row >= this.rowsArray.length) {
rgindacbbd7482012-06-13 15:06:16 -0700320 console.error('Row out of bounds: ' + row);
rginda87b86462011-12-14 13:48:03 -0800321 row = this.rowsArray.length - 1;
322 } else if (row < 0) {
rgindacbbd7482012-06-13 15:06:16 -0700323 console.error('Row out of bounds: ' + row);
rginda87b86462011-12-14 13:48:03 -0800324 row = 0;
325 }
326
327 if (column >= this.columnCount_) {
rgindacbbd7482012-06-13 15:06:16 -0700328 console.error('Column out of bounds: ' + column);
rginda87b86462011-12-14 13:48:03 -0800329 column = this.columnCount_ - 1;
330 } else if (column < 0) {
rgindacbbd7482012-06-13 15:06:16 -0700331 console.error('Column out of bounds: ' + column);
rginda87b86462011-12-14 13:48:03 -0800332 column = 0;
333 }
rginda8ba33642011-12-14 12:31:31 -0800334
rginda2312fff2012-01-05 16:20:52 -0800335 this.cursorPosition.overflow = false;
336
rginda8ba33642011-12-14 12:31:31 -0800337 var rowNode = this.rowsArray[row];
338 var node = rowNode.firstChild;
339
340 if (!node) {
341 node = rowNode.ownerDocument.createTextNode('');
342 rowNode.appendChild(node);
343 }
344
rgindaa19afe22012-01-25 15:40:22 -0800345 var currentColumn = 0;
346
rginda8ba33642011-12-14 12:31:31 -0800347 if (rowNode == this.cursorRowNode_) {
348 if (column >= this.cursorPosition.column - this.cursorOffset_) {
349 node = this.cursorNode_;
350 currentColumn = this.cursorPosition.column - this.cursorOffset_;
351 }
352 } else {
353 this.cursorRowNode_ = rowNode;
354 }
355
356 this.cursorPosition.move(row, column);
357
358 while (node) {
359 var offset = column - currentColumn;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800360 var width = hterm.TextAttributes.nodeWidth(node);
361 if (!node.nextSibling || width > offset) {
rginda8ba33642011-12-14 12:31:31 -0800362 this.cursorNode_ = node;
363 this.cursorOffset_ = offset;
364 return;
365 }
366
Ricky Liang48f05cb2013-12-31 23:35:29 +0800367 currentColumn += width;
rginda8ba33642011-12-14 12:31:31 -0800368 node = node.nextSibling;
369 }
370};
371
372/**
rginda87b86462011-12-14 13:48:03 -0800373 * Set the provided selection object to be a caret selection at the current
374 * cursor position.
Joel Hockey0f933582019-08-27 18:01:51 -0700375 *
376 * @param {!Selection} selection
rginda87b86462011-12-14 13:48:03 -0800377 */
378hterm.Screen.prototype.syncSelectionCaret = function(selection) {
Rob Spies06533ba2014-04-24 11:20:37 -0700379 try {
380 selection.collapse(this.cursorNode_, this.cursorOffset_);
381 } catch (firefoxIgnoredException) {
382 // FF can throw an exception if the range is off, rather than just not
383 // performing the collapse.
384 }
rginda87b86462011-12-14 13:48:03 -0800385};
386
387/**
rgindaa19afe22012-01-25 15:40:22 -0800388 * Split a single node into two nodes at the given offset.
rginda8ba33642011-12-14 12:31:31 -0800389 *
rgindaa19afe22012-01-25 15:40:22 -0800390 * For example:
391 * Given the DOM fragment '<div><span>Hello World</span></div>', call splitNode_
Zhu Qunying30d40712017-03-14 16:27:00 -0700392 * passing the span and an offset of 6. This would modify the fragment to
rgindaa19afe22012-01-25 15:40:22 -0800393 * become: '<div><span>Hello </span><span>World</span></div>'. If the span
394 * had any attributes they would have been copied to the new span as well.
395 *
396 * The to-be-split node must have a container, so that the new node can be
397 * placed next to it.
398 *
Joel Hockey0f933582019-08-27 18:01:51 -0700399 * @param {!Node} node The node to split.
400 * @param {number} offset The offset into the node where the split should
rgindaa19afe22012-01-25 15:40:22 -0800401 * occur.
rginda8ba33642011-12-14 12:31:31 -0800402 */
rgindaa19afe22012-01-25 15:40:22 -0800403hterm.Screen.prototype.splitNode_ = function(node, offset) {
rginda35c456b2012-02-09 17:29:05 -0800404 var afterNode = node.cloneNode(false);
rgindaa19afe22012-01-25 15:40:22 -0800405
rginda35c456b2012-02-09 17:29:05 -0800406 var textContent = node.textContent;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800407 node.textContent = hterm.TextAttributes.nodeSubstr(node, 0, offset);
408 afterNode.textContent = lib.wc.substr(textContent, offset);
rgindaa19afe22012-01-25 15:40:22 -0800409
Ricky Liang48f05cb2013-12-31 23:35:29 +0800410 if (afterNode.textContent)
411 node.parentNode.insertBefore(afterNode, node.nextSibling);
412 if (!node.textContent)
413 node.parentNode.removeChild(node);
rginda8ba33642011-12-14 12:31:31 -0800414};
415
416/**
rgindaa9abdd82012-08-06 18:05:09 -0700417 * Ensure that text is clipped and the cursor is clamped to the column count.
rgindaa19afe22012-01-25 15:40:22 -0800418 */
rgindaa9abdd82012-08-06 18:05:09 -0700419hterm.Screen.prototype.maybeClipCurrentRow = function() {
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700420 var width = hterm.TextAttributes.nodeWidth(lib.notNull(this.cursorRowNode_));
Ricky Liang48f05cb2013-12-31 23:35:29 +0800421
422 if (width <= this.columnCount_) {
rgindaa9abdd82012-08-06 18:05:09 -0700423 // Current row does not need clipping, but may need clamping.
424 if (this.cursorPosition.column >= this.columnCount_) {
425 this.setCursorPosition(this.cursorPosition.row, this.columnCount_ - 1);
426 this.cursorPosition.overflow = true;
427 }
rgindaa19afe22012-01-25 15:40:22 -0800428
rgindaa9abdd82012-08-06 18:05:09 -0700429 return;
430 }
431
432 // Save off the current column so we can maybe restore it later.
433 var currentColumn = this.cursorPosition.column;
434
435 // Move the cursor to the final column.
436 this.setCursorPosition(this.cursorPosition.row, this.columnCount_ - 1);
437
438 // Remove any text that partially overflows.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700439 width = hterm.TextAttributes.nodeWidth(lib.notNull(this.cursorNode_));
Ricky Liang48f05cb2013-12-31 23:35:29 +0800440
441 if (this.cursorOffset_ < width - 1) {
442 this.cursorNode_.textContent = hterm.TextAttributes.nodeSubstr(
443 this.cursorNode_, 0, this.cursorOffset_ + 1);
rgindaa9abdd82012-08-06 18:05:09 -0700444 }
445
446 // Remove all nodes after the cursor.
rgindaa19afe22012-01-25 15:40:22 -0800447 var rowNode = this.cursorRowNode_;
448 var node = this.cursorNode_.nextSibling;
449
450 while (node) {
rgindaa19afe22012-01-25 15:40:22 -0800451 rowNode.removeChild(node);
452 node = this.cursorNode_.nextSibling;
453 }
454
Robert Ginda7fd57082012-09-25 14:41:47 -0700455 if (currentColumn < this.columnCount_) {
rgindaa9abdd82012-08-06 18:05:09 -0700456 // If the cursor was within the screen before we started then restore its
457 // position.
rgindaa19afe22012-01-25 15:40:22 -0800458 this.setCursorPosition(this.cursorPosition.row, currentColumn);
rgindaa9abdd82012-08-06 18:05:09 -0700459 } else {
460 // Otherwise leave it at the the last column in the overflow state.
461 this.cursorPosition.overflow = true;
rgindaa19afe22012-01-25 15:40:22 -0800462 }
rgindaa19afe22012-01-25 15:40:22 -0800463};
464
465/**
466 * Insert a string at the current character position using the current
467 * text attributes.
468 *
rgindaa09e7332012-08-17 12:49:51 -0700469 * You must call maybeClipCurrentRow() after in order to clip overflowed
470 * text and clamp the cursor.
471 *
472 * It is also up to the caller to properly maintain the line overflow state
473 * using hterm.Screen..commitLineOverflow().
Joel Hockey0f933582019-08-27 18:01:51 -0700474 *
475 * @param {string} str The string to insert.
476 * @param {number=} wcwidth The cached lib.wc.strWidth value for |str|. Will be
477 * calculated on demand if need be. Passing in a cached value helps speed
478 * up processing as this is a hot codepath.
rginda8ba33642011-12-14 12:31:31 -0800479 */
Mike Frysinger6380bed2017-08-24 18:46:39 -0400480hterm.Screen.prototype.insertString = function(str, wcwidth=undefined) {
rgindaa19afe22012-01-25 15:40:22 -0800481 var cursorNode = this.cursorNode_;
482 var cursorNodeText = cursorNode.textContent;
rginda8ba33642011-12-14 12:31:31 -0800483
Robert Gindaa21dfb32013-10-31 14:17:45 -0700484 this.cursorRowNode_.removeAttribute('line-overflow');
485
Ricky Liang48f05cb2013-12-31 23:35:29 +0800486 // We may alter the width of the string by prepending some missing
487 // whitespaces, so we need to record the string width ahead of time.
Mike Frysinger6380bed2017-08-24 18:46:39 -0400488 if (wcwidth === undefined)
489 wcwidth = lib.wc.strWidth(str);
rginda8ba33642011-12-14 12:31:31 -0800490
rgindaa19afe22012-01-25 15:40:22 -0800491 // No matter what, before this function exits the cursor column will have
492 // moved this much.
Mike Frysinger6380bed2017-08-24 18:46:39 -0400493 this.cursorPosition.column += wcwidth;
rginda8ba33642011-12-14 12:31:31 -0800494
rgindaa19afe22012-01-25 15:40:22 -0800495 // Local cache of the cursor offset.
496 var offset = this.cursorOffset_;
rginda8ba33642011-12-14 12:31:31 -0800497
rgindaa19afe22012-01-25 15:40:22 -0800498 // Reverse offset is the offset measured from the end of the string.
499 // Zero implies that the cursor is at the end of the cursor node.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800500 var reverseOffset = hterm.TextAttributes.nodeWidth(cursorNode) - offset;
rgindaa19afe22012-01-25 15:40:22 -0800501
502 if (reverseOffset < 0) {
503 // A negative reverse offset means the cursor is positioned past the end
504 // of the characters on this line. We'll need to insert the missing
505 // whitespace.
Mike Frysinger73e56462019-07-17 00:23:46 -0500506 const ws = ' '.repeat(-reverseOffset);
rgindaa19afe22012-01-25 15:40:22 -0800507
Brad Town7de83302015-03-12 02:10:32 -0700508 // This whitespace should be completely unstyled. Underline, background
509 // color, and strikethrough would be visible on whitespace, so we can't use
510 // one of those spans to hold the text.
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200511 if (!(this.textAttributes.underline ||
Brad Town7de83302015-03-12 02:10:32 -0700512 this.textAttributes.strikethrough ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200513 this.textAttributes.background ||
514 this.textAttributes.wcNode ||
Mike Frysinger1e98c0f2017-08-15 01:21:31 -0400515 !this.textAttributes.asciiNode ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200516 this.textAttributes.tileData != null)) {
rgindaa19afe22012-01-25 15:40:22 -0800517 // Best case scenario, we can just pretend the spaces were part of the
518 // original string.
519 str = ws + str;
Mike Frysinger6a4f2412017-08-31 01:11:25 -0400520 } else if (cursorNode.nodeType == Node.TEXT_NODE ||
Ricky Liang48f05cb2013-12-31 23:35:29 +0800521 !(cursorNode.wcNode ||
Mike Frysinger1e98c0f2017-08-15 01:21:31 -0400522 !cursorNode.asciiNode ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200523 cursorNode.tileNode ||
Ricky Liang48f05cb2013-12-31 23:35:29 +0800524 cursorNode.style.textDecoration ||
Mike Frysinger09c54f42017-12-15 01:12:30 -0500525 cursorNode.style.textDecorationStyle ||
526 cursorNode.style.textDecorationLine ||
rgindaa19afe22012-01-25 15:40:22 -0800527 cursorNode.style.backgroundColor)) {
528 // Second best case, the current node is able to hold the whitespace.
529 cursorNode.textContent = (cursorNodeText += ws);
530 } else {
531 // Worst case, we have to create a new node to hold the whitespace.
532 var wsNode = cursorNode.ownerDocument.createTextNode(ws);
533 this.cursorRowNode_.insertBefore(wsNode, cursorNode.nextSibling);
534 this.cursorNode_ = cursorNode = wsNode;
535 this.cursorOffset_ = offset = -reverseOffset;
536 cursorNodeText = ws;
537 }
538
539 // We now know for sure that we're at the last character of the cursor node.
540 reverseOffset = 0;
rginda8ba33642011-12-14 12:31:31 -0800541 }
542
rgindaa19afe22012-01-25 15:40:22 -0800543 if (this.textAttributes.matchesContainer(cursorNode)) {
544 // The new text can be placed directly in the cursor node.
545 if (reverseOffset == 0) {
546 cursorNode.textContent = cursorNodeText + str;
547 } else if (offset == 0) {
548 cursorNode.textContent = str + cursorNodeText;
549 } else {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800550 cursorNode.textContent =
551 hterm.TextAttributes.nodeSubstr(cursorNode, 0, offset) +
552 str + hterm.TextAttributes.nodeSubstr(cursorNode, offset);
rgindaa19afe22012-01-25 15:40:22 -0800553 }
rginda8ba33642011-12-14 12:31:31 -0800554
Mike Frysinger6380bed2017-08-24 18:46:39 -0400555 this.cursorOffset_ += wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800556 return;
rginda87b86462011-12-14 13:48:03 -0800557 }
558
rgindaa19afe22012-01-25 15:40:22 -0800559 // The cursor node is the wrong style for the new text. If we're at the
560 // beginning or end of the cursor node, then the adjacent node is also a
561 // potential candidate.
rginda8ba33642011-12-14 12:31:31 -0800562
rgindaa19afe22012-01-25 15:40:22 -0800563 if (offset == 0) {
564 // At the beginning of the cursor node, the check the previous sibling.
565 var previousSibling = cursorNode.previousSibling;
566 if (previousSibling &&
567 this.textAttributes.matchesContainer(previousSibling)) {
568 previousSibling.textContent += str;
569 this.cursorNode_ = previousSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800570 this.cursorOffset_ = lib.wc.strWidth(previousSibling.textContent);
rgindaa19afe22012-01-25 15:40:22 -0800571 return;
572 }
573
574 var newNode = this.textAttributes.createContainer(str);
575 this.cursorRowNode_.insertBefore(newNode, cursorNode);
576 this.cursorNode_ = newNode;
Mike Frysinger6380bed2017-08-24 18:46:39 -0400577 this.cursorOffset_ = wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800578 return;
579 }
580
581 if (reverseOffset == 0) {
582 // At the end of the cursor node, the check the next sibling.
583 var nextSibling = cursorNode.nextSibling;
584 if (nextSibling &&
585 this.textAttributes.matchesContainer(nextSibling)) {
586 nextSibling.textContent = str + nextSibling.textContent;
587 this.cursorNode_ = nextSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800588 this.cursorOffset_ = lib.wc.strWidth(str);
rgindaa19afe22012-01-25 15:40:22 -0800589 return;
590 }
591
592 var newNode = this.textAttributes.createContainer(str);
593 this.cursorRowNode_.insertBefore(newNode, nextSibling);
594 this.cursorNode_ = newNode;
595 // We specifically need to include any missing whitespace here, since it's
596 // going in a new node.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800597 this.cursorOffset_ = hterm.TextAttributes.nodeWidth(newNode);
rgindaa19afe22012-01-25 15:40:22 -0800598 return;
599 }
600
601 // Worst case, we're somewhere in the middle of the cursor node. We'll
602 // have to split it into two nodes and insert our new container in between.
603 this.splitNode_(cursorNode, offset);
604 var newNode = this.textAttributes.createContainer(str);
605 this.cursorRowNode_.insertBefore(newNode, cursorNode.nextSibling);
606 this.cursorNode_ = newNode;
Mike Frysinger6380bed2017-08-24 18:46:39 -0400607 this.cursorOffset_ = wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800608};
609
610/**
rginda8ba33642011-12-14 12:31:31 -0800611 * Overwrite the text at the current cursor position.
612 *
rgindaa09e7332012-08-17 12:49:51 -0700613 * You must call maybeClipCurrentRow() after in order to clip overflowed
614 * text and clamp the cursor.
615 *
616 * It is also up to the caller to properly maintain the line overflow state
617 * using hterm.Screen..commitLineOverflow().
Joel Hockey0f933582019-08-27 18:01:51 -0700618 *
619 * @param {string} str The source string for overwriting existing content.
620 * @param {number=} wcwidth The cached lib.wc.strWidth value for |str|. Will be
621 * calculated on demand if need be. Passing in a cached value helps speed
622 * up processing as this is a hot codepath.
rginda8ba33642011-12-14 12:31:31 -0800623 */
Mike Frysinger6380bed2017-08-24 18:46:39 -0400624hterm.Screen.prototype.overwriteString = function(str, wcwidth=undefined) {
rginda8ba33642011-12-14 12:31:31 -0800625 var maxLength = this.columnCount_ - this.cursorPosition.column;
626 if (!maxLength)
Mike Frysinger159b7392019-03-26 11:08:32 -0700627 return;
rgindaa19afe22012-01-25 15:40:22 -0800628
Mike Frysinger6380bed2017-08-24 18:46:39 -0400629 if (wcwidth === undefined)
630 wcwidth = lib.wc.strWidth(str);
631
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700632 if (this.textAttributes.matchesContainer(lib.notNull(this.cursorNode_)) &&
633 this.cursorNode_.textContent.substr(this.cursorOffset_) ==
634 str) {
rgindaa19afe22012-01-25 15:40:22 -0800635 // This overwrite would be a no-op, just move the cursor and return.
Mike Frysinger6380bed2017-08-24 18:46:39 -0400636 this.cursorOffset_ += wcwidth;
637 this.cursorPosition.column += wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800638 return;
639 }
rginda8ba33642011-12-14 12:31:31 -0800640
Mike Frysinger6380bed2017-08-24 18:46:39 -0400641 this.deleteChars(Math.min(wcwidth, maxLength));
642 this.insertString(str, wcwidth);
rginda8ba33642011-12-14 12:31:31 -0800643};
644
645/**
646 * Forward-delete one or more characters at the current cursor position.
647 *
648 * Text to the right of the deleted characters is shifted left. Only affects
649 * characters on the same row as the cursor.
650 *
Joel Hockey0f933582019-08-27 18:01:51 -0700651 * @param {number} count The column width of characters to delete. This is
Ricky Liang48f05cb2013-12-31 23:35:29 +0800652 * clamped to the column width minus the cursor column.
Joel Hockey0f933582019-08-27 18:01:51 -0700653 * @return {number} The column width of the characters actually deleted.
rginda8ba33642011-12-14 12:31:31 -0800654 */
655hterm.Screen.prototype.deleteChars = function(count) {
656 var node = this.cursorNode_;
657 var offset = this.cursorOffset_;
658
Robert Ginda7fd57082012-09-25 14:41:47 -0700659 var currentCursorColumn = this.cursorPosition.column;
660 count = Math.min(count, this.columnCount_ - currentCursorColumn);
661 if (!count)
662 return 0;
663
664 var rv = count;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800665 var startLength, endLength;
rgindaa19afe22012-01-25 15:40:22 -0800666
rginda8ba33642011-12-14 12:31:31 -0800667 while (node && count) {
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400668 // Sanity check so we don't loop forever, but we don't also go quietly.
669 if (count < 0) {
670 console.error(`Deleting ${rv} chars went negative: ${count}`);
671 break;
672 }
673
Ricky Liang48f05cb2013-12-31 23:35:29 +0800674 startLength = hterm.TextAttributes.nodeWidth(node);
675 node.textContent = hterm.TextAttributes.nodeSubstr(node, 0, offset) +
676 hterm.TextAttributes.nodeSubstr(node, offset + count);
677 endLength = hterm.TextAttributes.nodeWidth(node);
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400678
679 // Deal with splitting wide characters. There are two ways: we could delete
680 // the first column or the second column. In both cases, we delete the wide
681 // character and replace one of the columns with a space (since the other
682 // was deleted). If there are more chars to delete, the next loop will pick
683 // up the slack.
684 if (node.wcNode && offset < startLength &&
Joel Hockeyd36efd62019-09-30 14:16:20 -0700685 ((endLength && startLength == endLength) ||
686 (!endLength && offset == 1))) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800687 // No characters were deleted when there should be. We're probably trying
688 // to delete one column width from a wide character node. We remove the
689 // wide character node here and replace it with a single space.
690 var spaceNode = this.textAttributes.createContainer(' ');
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400691 node.parentNode.insertBefore(spaceNode, offset ? node : node.nextSibling);
Ricky Liang48f05cb2013-12-31 23:35:29 +0800692 node.textContent = '';
693 endLength = 0;
694 count -= 1;
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400695 } else
696 count -= startLength - endLength;
rginda8ba33642011-12-14 12:31:31 -0800697
Ricky Liang48f05cb2013-12-31 23:35:29 +0800698 var nextNode = node.nextSibling;
699 if (endLength == 0 && node != this.cursorNode_) {
700 node.parentNode.removeChild(node);
701 }
702 node = nextNode;
rginda8ba33642011-12-14 12:31:31 -0800703 offset = 0;
704 }
Robert Ginda7fd57082012-09-25 14:41:47 -0700705
Ricky Liang48f05cb2013-12-31 23:35:29 +0800706 // Remove this.cursorNode_ if it is an empty non-text node.
Mike Frysinger6a4f2412017-08-31 01:11:25 -0400707 if (this.cursorNode_.nodeType != Node.TEXT_NODE &&
708 !this.cursorNode_.textContent) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800709 var cursorNode = this.cursorNode_;
710 if (cursorNode.previousSibling) {
711 this.cursorNode_ = cursorNode.previousSibling;
712 this.cursorOffset_ = hterm.TextAttributes.nodeWidth(
713 cursorNode.previousSibling);
714 } else if (cursorNode.nextSibling) {
715 this.cursorNode_ = cursorNode.nextSibling;
716 this.cursorOffset_ = 0;
717 } else {
718 var emptyNode = this.cursorRowNode_.ownerDocument.createTextNode('');
719 this.cursorRowNode_.appendChild(emptyNode);
720 this.cursorNode_ = emptyNode;
721 this.cursorOffset_ = 0;
722 }
723 this.cursorRowNode_.removeChild(cursorNode);
724 }
725
Robert Ginda7fd57082012-09-25 14:41:47 -0700726 return rv;
rginda8ba33642011-12-14 12:31:31 -0800727};
John Macinnesfb683832013-07-22 14:46:30 -0400728
729/**
730 * Finds first X-ROW of a line containing specified X-ROW.
731 * Used to support line overflow.
732 *
Joel Hockey0f933582019-08-27 18:01:51 -0700733 * @param {!Node} row X-ROW to begin search for first row of line.
734 * @return {!Node} The X-ROW that is at the beginning of the line.
John Macinnesfb683832013-07-22 14:46:30 -0400735 **/
736hterm.Screen.prototype.getLineStartRow_ = function(row) {
737 while (row.previousSibling &&
738 row.previousSibling.hasAttribute('line-overflow')) {
739 row = row.previousSibling;
740 }
741 return row;
742};
743
744/**
745 * Gets text of a line beginning with row.
746 * Supports line overflow.
747 *
Joel Hockey0f933582019-08-27 18:01:51 -0700748 * @param {!Node} row First X-ROW of line.
John Macinnesfb683832013-07-22 14:46:30 -0400749 * @return {string} Text content of line.
750 **/
751hterm.Screen.prototype.getLineText_ = function(row) {
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700752 let rowText = '';
753 let rowOrNull = row;
754 while (rowOrNull) {
755 rowText += rowOrNull.textContent;
756 if (rowOrNull.hasAttribute('line-overflow')) {
757 rowOrNull = rowOrNull.nextSibling;
John Macinnesfb683832013-07-22 14:46:30 -0400758 } else {
759 break;
760 }
761 }
762 return rowText;
763};
764
765/**
766 * Returns X-ROW that is ancestor of the node.
767 *
Joel Hockey0f933582019-08-27 18:01:51 -0700768 * @param {!Node} node Node to get X-ROW ancestor for.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700769 * @return {?Node} X-ROW ancestor of node, or null if not found.
John Macinnesfb683832013-07-22 14:46:30 -0400770 **/
771hterm.Screen.prototype.getXRowAncestor_ = function(node) {
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700772 let nodeOrNull = node;
773 while (nodeOrNull) {
774 if (nodeOrNull.nodeName === 'X-ROW')
John Macinnesfb683832013-07-22 14:46:30 -0400775 break;
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700776 nodeOrNull = nodeOrNull.parentNode;
John Macinnesfb683832013-07-22 14:46:30 -0400777 }
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700778 return nodeOrNull;
John Macinnesfb683832013-07-22 14:46:30 -0400779};
780
781/**
782 * Returns position within line of character at offset within node.
783 * Supports line overflow.
784 *
Joel Hockey0f933582019-08-27 18:01:51 -0700785 * @param {!Node} row X-ROW at beginning of line.
786 * @param {!Node} node Node to get position of.
787 * @param {number} offset Offset into node.
788 * @return {number} Position within line of character at offset within node.
John Macinnesfb683832013-07-22 14:46:30 -0400789 **/
790hterm.Screen.prototype.getPositionWithOverflow_ = function(row, node, offset) {
791 if (!node)
792 return -1;
793 var ancestorRow = this.getXRowAncestor_(node);
794 if (!ancestorRow)
795 return -1;
796 var position = 0;
797 while (ancestorRow != row) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800798 position += hterm.TextAttributes.nodeWidth(row);
John Macinnesfb683832013-07-22 14:46:30 -0400799 if (row.hasAttribute('line-overflow') && row.nextSibling) {
800 row = row.nextSibling;
801 } else {
802 return -1;
803 }
804 }
805 return position + this.getPositionWithinRow_(row, node, offset);
806};
807
808/**
809 * Returns position within row of character at offset within node.
810 * Does not support line overflow.
811 *
Joel Hockey0f933582019-08-27 18:01:51 -0700812 * @param {!Node} row X-ROW to get position within.
813 * @param {!Node} node Node to get position for.
814 * @param {number} offset Offset within node to get position for.
815 * @return {number} Position within row of character at offset within node.
John Macinnesfb683832013-07-22 14:46:30 -0400816 **/
817hterm.Screen.prototype.getPositionWithinRow_ = function(row, node, offset) {
818 if (node.parentNode != row) {
Mike Frysinger498192d2017-06-26 18:23:31 -0400819 // If we traversed to the top node, then there's nothing to find here.
820 if (node.parentNode == null)
821 return -1;
822
John Macinnesfb683832013-07-22 14:46:30 -0400823 return this.getPositionWithinRow_(node.parentNode, node, offset) +
824 this.getPositionWithinRow_(row, node.parentNode, 0);
825 }
826 var position = 0;
827 for (var i = 0; i < row.childNodes.length; i++) {
828 var currentNode = row.childNodes[i];
829 if (currentNode == node)
830 return position + offset;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800831 position += hterm.TextAttributes.nodeWidth(currentNode);
John Macinnesfb683832013-07-22 14:46:30 -0400832 }
833 return -1;
834};
835
836/**
837 * Returns the node and offset corresponding to position within line.
838 * Supports line overflow.
839 *
Joel Hockey0f933582019-08-27 18:01:51 -0700840 * @param {!Node} row X-ROW at beginning of line.
841 * @param {number} position Position within line to retrieve node and offset.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700842 * @return {?Array} Two element array containing node and offset respectively.
John Macinnesfb683832013-07-22 14:46:30 -0400843 **/
844hterm.Screen.prototype.getNodeAndOffsetWithOverflow_ = function(row, position) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800845 while (row && position > hterm.TextAttributes.nodeWidth(row)) {
John Macinnesfb683832013-07-22 14:46:30 -0400846 if (row.hasAttribute('line-overflow') && row.nextSibling) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800847 position -= hterm.TextAttributes.nodeWidth(row);
John Macinnesfb683832013-07-22 14:46:30 -0400848 row = row.nextSibling;
849 } else {
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700850 return [null, -1];
John Macinnesfb683832013-07-22 14:46:30 -0400851 }
852 }
853 return this.getNodeAndOffsetWithinRow_(row, position);
854};
855
856/**
857 * Returns the node and offset corresponding to position within row.
858 * Does not support line overflow.
859 *
Joel Hockey0f933582019-08-27 18:01:51 -0700860 * @param {!Node} row X-ROW to get position within.
861 * @param {number} position Position within row to retrieve node and offset.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700862 * @return {?Array} Two element array containing node and offset respectively.
John Macinnesfb683832013-07-22 14:46:30 -0400863 **/
864hterm.Screen.prototype.getNodeAndOffsetWithinRow_ = function(row, position) {
865 for (var i = 0; i < row.childNodes.length; i++) {
866 var node = row.childNodes[i];
Ricky Liang48f05cb2013-12-31 23:35:29 +0800867 var nodeTextWidth = hterm.TextAttributes.nodeWidth(node);
868 if (position <= nodeTextWidth) {
John Macinnesfb683832013-07-22 14:46:30 -0400869 if (node.nodeName === 'SPAN') {
870 /** Drill down to node contained by SPAN. **/
871 return this.getNodeAndOffsetWithinRow_(node, position);
872 } else {
873 return [node, position];
874 }
875 }
Ricky Liang48f05cb2013-12-31 23:35:29 +0800876 position -= nodeTextWidth;
John Macinnesfb683832013-07-22 14:46:30 -0400877 }
878 return null;
879};
880
881/**
882 * Returns the node and offset corresponding to position within line.
883 * Supports line overflow.
884 *
Joel Hockey0f933582019-08-27 18:01:51 -0700885 * @param {!Node} row X-ROW at beginning of line.
886 * @param {number} start Start position of range within line.
887 * @param {number} end End position of range within line.
888 * @param {!Range} range Range to modify.
John Macinnesfb683832013-07-22 14:46:30 -0400889 **/
890hterm.Screen.prototype.setRange_ = function(row, start, end, range) {
891 var startNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, start);
892 if (startNodeAndOffset == null)
893 return;
894 var endNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, end);
895 if (endNodeAndOffset == null)
896 return;
897 range.setStart(startNodeAndOffset[0], startNodeAndOffset[1]);
898 range.setEnd(endNodeAndOffset[0], endNodeAndOffset[1]);
899};
900
901/**
John Lincae9b732018-03-08 13:56:35 +0800902 * Expands selection to surrounding string with word break matches.
John Macinnesfb683832013-07-22 14:46:30 -0400903 *
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700904 * @param {?Selection} selection Selection to expand.
John Lincae9b732018-03-08 13:56:35 +0800905 * @param {string} leftMatch left word break match.
906 * @param {string} rightMatch right word break match.
907 * @param {string} insideMatch inside word break match.
908 */
909hterm.Screen.prototype.expandSelectionWithWordBreakMatches_ =
910 function(selection, leftMatch, rightMatch, insideMatch) {
John Macinnesfb683832013-07-22 14:46:30 -0400911 if (!selection)
912 return;
913
914 var range = selection.getRangeAt(0);
915 if (!range || range.toString().match(/\s/))
916 return;
917
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700918 const rowElement = this.getXRowAncestor_(lib.notNull(range.startContainer));
Raymes Khoury334625a2018-06-25 10:29:40 +1000919 if (!rowElement)
920 return;
921 const row = this.getLineStartRow_(rowElement);
John Macinnesfb683832013-07-22 14:46:30 -0400922 if (!row)
923 return;
924
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700925 var startPosition = this.getPositionWithOverflow_(
926 row, lib.notNull(range.startContainer), range.startOffset);
John Macinnesfb683832013-07-22 14:46:30 -0400927 if (startPosition == -1)
928 return;
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700929 var endPosition = this.getPositionWithOverflow_(
930 row, lib.notNull(range.endContainer), range.endOffset);
John Macinnesfb683832013-07-22 14:46:30 -0400931 if (endPosition == -1)
932 return;
933
John Macinnesfb683832013-07-22 14:46:30 -0400934 //Move start to the left.
935 var rowText = this.getLineText_(row);
Ricky Liang48f05cb2013-12-31 23:35:29 +0800936 var lineUpToRange = lib.wc.substring(rowText, 0, endPosition);
Mike Frysingerd6e0d432019-12-02 04:54:41 -0500937 var leftRegularExpression = new RegExp(leftMatch + insideMatch + '$');
John Macinnesfb683832013-07-22 14:46:30 -0400938 var expandedStart = lineUpToRange.search(leftRegularExpression);
939 if (expandedStart == -1 || expandedStart > startPosition)
940 return;
941
942 //Move end to the right.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800943 var lineFromRange = lib.wc.substring(rowText, startPosition,
944 lib.wc.strWidth(rowText));
Mike Frysingerd6e0d432019-12-02 04:54:41 -0500945 var rightRegularExpression = new RegExp('^' + insideMatch + rightMatch);
John Macinnesfb683832013-07-22 14:46:30 -0400946 var found = lineFromRange.match(rightRegularExpression);
947 if (!found)
948 return;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800949 var expandedEnd = startPosition + lib.wc.strWidth(found[0]);
John Macinnesfb683832013-07-22 14:46:30 -0400950 if (expandedEnd == -1 || expandedEnd < endPosition)
951 return;
952
953 this.setRange_(row, expandedStart, expandedEnd, range);
954 selection.addRange(range);
955};
Mike Frysingera2cacaa2017-11-29 13:51:09 -0800956
957/**
John Lincae9b732018-03-08 13:56:35 +0800958 * Expands selection to surrounding string using the user's settings.
959 *
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700960 * @param {?Selection} selection Selection to expand.
John Lincae9b732018-03-08 13:56:35 +0800961 */
962hterm.Screen.prototype.expandSelection = function(selection) {
963 this.expandSelectionWithWordBreakMatches_(
964 selection,
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700965 lib.notNull(this.wordBreakMatchLeft),
966 lib.notNull(this.wordBreakMatchRight),
967 lib.notNull(this.wordBreakMatchMiddle));
Mike Frysinger8416e0a2017-05-17 09:09:46 -0400968};
John Lincae9b732018-03-08 13:56:35 +0800969
970/**
971 * Expands selection to surrounding URL using a set of fixed match settings.
972 *
Joel Hockeyadd2f7e2019-09-20 16:37:35 -0700973 * @param {?Selection} selection Selection to expand.
John Lincae9b732018-03-08 13:56:35 +0800974 */
975hterm.Screen.prototype.expandSelectionForUrl = function(selection) {
976 this.expandSelectionWithWordBreakMatches_(
977 selection,
Mike Frysinger1a1a1802020-01-29 21:38:55 -0500978 '[^\\s[\\](){}<>"\'^!@#$%&*,;:`\u{2018}\u{201c}\u{2039}\u{ab}]',
979 '[^\\s[\\](){}<>"\'^!@#$%&*,;:~.`\u{2019}\u{201d}\u{203a}\u{bb}]',
Mike Frysinger9e11e492020-01-06 14:29:57 +0545980 '[^\\s[\\](){}<>"\'^]*');
John Lincae9b732018-03-08 13:56:35 +0800981};
982
983/**
Mike Frysingera2cacaa2017-11-29 13:51:09 -0800984 * Save the current cursor state to the corresponding screens.
985 *
Joel Hockey0f933582019-08-27 18:01:51 -0700986 * @param {!hterm.VT} vt The VT object to read graphic codeset details from.
Mike Frysingera2cacaa2017-11-29 13:51:09 -0800987 */
988hterm.Screen.prototype.saveCursorAndState = function(vt) {
989 this.cursorState_.save(vt);
990};
991
992/**
993 * Restore the saved cursor state in the corresponding screens.
994 *
Joel Hockey0f933582019-08-27 18:01:51 -0700995 * @param {!hterm.VT} vt The VT object to write graphic codeset details to.
Mike Frysingera2cacaa2017-11-29 13:51:09 -0800996 */
997hterm.Screen.prototype.restoreCursorAndState = function(vt) {
998 this.cursorState_.restore(vt);
999};
1000
1001/**
1002 * Track all the things related to the current "cursor".
1003 *
1004 * The set of things saved & restored here is defined by DEC:
1005 * https://vt100.net/docs/vt510-rm/DECSC.html
1006 * - Cursor position
1007 * - Character attributes set by the SGR command
1008 * - Character sets (G0, G1, G2, or G3) currently in GL and GR
1009 * - Wrap flag (autowrap or no autowrap)
1010 * - State of origin mode (DECOM)
1011 * - Selective erase attribute
1012 * - Any single shift 2 (SS2) or single shift 3 (SS3) functions sent
1013 *
1014 * These are done on a per-screen basis.
Joel Hockey0f933582019-08-27 18:01:51 -07001015 *
1016 * @param {!hterm.Screen} screen The screen this cursor is tied to.
Joel Hockeyadd2f7e2019-09-20 16:37:35 -07001017 * @constructor
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001018 */
1019hterm.Screen.CursorState = function(screen) {
1020 this.screen_ = screen;
1021 this.cursor = null;
1022 this.textAttributes = null;
1023 this.GL = this.GR = this.G0 = this.G1 = this.G2 = this.G3 = null;
1024};
1025
1026/**
1027 * Save all the cursor state.
1028 *
Joel Hockey0f933582019-08-27 18:01:51 -07001029 * @param {!hterm.VT} vt The VT object to read graphic codeset details from.
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001030 */
1031hterm.Screen.CursorState.prototype.save = function(vt) {
1032 this.cursor = vt.terminal.saveCursor();
1033
1034 this.textAttributes = this.screen_.textAttributes.clone();
1035
1036 this.GL = vt.GL;
1037 this.GR = vt.GR;
1038
1039 this.G0 = vt.G0;
1040 this.G1 = vt.G1;
1041 this.G2 = vt.G2;
1042 this.G3 = vt.G3;
1043};
1044
1045/**
1046 * Restore the previously saved cursor state.
1047 *
Joel Hockey0f933582019-08-27 18:01:51 -07001048 * @param {!hterm.VT} vt The VT object to write graphic codeset details to.
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001049 */
1050hterm.Screen.CursorState.prototype.restore = function(vt) {
1051 vt.terminal.restoreCursor(this.cursor);
1052
1053 // Cursor restore includes char attributes (bold/etc...), but does not change
1054 // the color palette (which are a terminal setting).
1055 const tattrs = this.textAttributes.clone();
1056 tattrs.colorPalette = this.screen_.textAttributes.colorPalette;
1057 tattrs.syncColors();
1058
1059 this.screen_.textAttributes = tattrs;
1060
1061 vt.GL = this.GL;
1062 vt.GR = this.GR;
1063
1064 vt.G0 = this.G0;
1065 vt.G1 = this.G1;
1066 vt.G2 = this.G2;
1067 vt.G3 = this.G3;
1068};