blob: f4d8ab9a379df7db8ca27bf3882118a026c336af [file] [log] [blame]
rginda87b86462011-12-14 13:48:03 -08001// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
rginda8ba33642011-12-14 12:31:31 -08002// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
rgindacbbd7482012-06-13 15:06:16 -07005'use strict';
6
Ricky Liang48f05cb2013-12-31 23:35:29 +08007lib.rtdep('lib.f', 'lib.wc',
rgindacbbd7482012-06-13 15:06:16 -07008 'hterm.RowCol', 'hterm.Size', 'hterm.TextAttributes');
9
rginda8ba33642011-12-14 12:31:31 -080010/**
11 * @fileoverview This class represents a single terminal screen full of text.
12 *
13 * It maintains the current cursor position and has basic methods for text
14 * insert and overwrite, and adding or removing rows from the screen.
15 *
16 * This class has no knowledge of the scrollback buffer.
17 *
18 * The number of rows on the screen is determined only by the number of rows
19 * that the caller inserts into the screen. If a caller wants to ensure a
20 * constant number of rows on the screen, it's their responsibility to remove a
21 * row for each row inserted.
22 *
23 * The screen width, in contrast, is enforced locally.
24 *
25 *
26 * In practice...
27 * - The hterm.Terminal class holds two hterm.Screen instances. One for the
28 * primary screen and one for the alternate screen.
29 *
30 * - The html.Screen class only cares that rows are HTMLElements. In the
31 * larger context of hterm, however, the rows happen to be displayed by an
32 * hterm.ScrollPort and have to follow a few rules as a result. Each
33 * row must be rooted by the custom HTML tag 'x-row', and each must have a
34 * rowIndex property that corresponds to the index of the row in the context
35 * of the scrollback buffer. These invariants are enforced by hterm.Terminal
36 * because that is the class using the hterm.Screen in the context of an
37 * hterm.ScrollPort.
38 */
39
40/**
41 * Create a new screen instance.
42 *
43 * The screen initially has no rows and a maximum column count of 0.
44 *
45 * @param {integer} opt_columnCount The maximum number of columns for this
46 * screen. See insertString() and overwriteString() for information about
47 * what happens when too many characters are added too a row. Defaults to
48 * 0 if not provided.
49 */
50hterm.Screen = function(opt_columnCount) {
51 /**
52 * Public, read-only access to the rows in this screen.
53 */
54 this.rowsArray = [];
55
56 // The max column width for this screen.
rginda87b86462011-12-14 13:48:03 -080057 this.columnCount_ = opt_columnCount || 80;
rginda8ba33642011-12-14 12:31:31 -080058
rgindaa19afe22012-01-25 15:40:22 -080059 // The current color, bold, underline and blink attributes.
60 this.textAttributes = new hterm.TextAttributes(window.document);
61
rginda87b86462011-12-14 13:48:03 -080062 // Current zero-based cursor coordinates.
63 this.cursorPosition = new hterm.RowCol(0, 0);
rginda8ba33642011-12-14 12:31:31 -080064
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.
rginda8ba33642011-12-14 12:31:31 -080076 this.cursorOffset_ = null;
Mike Frysinger664e9992017-05-19 01:24:24 -040077
78 // Regexes for expanding word selections.
79 this.wordBreakMatchLeft = null;
80 this.wordBreakMatchRight = null;
81 this.wordBreakMatchMiddle = null;
rginda8ba33642011-12-14 12:31:31 -080082};
83
84/**
85 * Return the screen size as an hterm.Size object.
86 *
87 * @return {hterm.Size} hterm.Size object representing the current number
88 * of rows and columns in this screen.
89 */
90hterm.Screen.prototype.getSize = function() {
91 return new hterm.Size(this.columnCount_, this.rowsArray.length);
92};
93
94/**
95 * Return the current number of rows in this screen.
96 *
97 * @return {integer} The number of rows in this screen.
98 */
99hterm.Screen.prototype.getHeight = function() {
100 return this.rowsArray.length;
101};
102
103/**
104 * Return the current number of columns in this screen.
105 *
106 * @return {integer} The number of columns in this screen.
107 */
108hterm.Screen.prototype.getWidth = function() {
109 return this.columnCount_;
110};
111
112/**
113 * Set the maximum number of columns per row.
114 *
rginda8ba33642011-12-14 12:31:31 -0800115 * @param {integer} count The maximum number of columns per row.
116 */
117hterm.Screen.prototype.setColumnCount = function(count) {
rginda2312fff2012-01-05 16:20:52 -0800118 this.columnCount_ = count;
119
rgindacbbd7482012-06-13 15:06:16 -0700120 if (this.cursorPosition.column >= count)
121 this.setCursorPosition(this.cursorPosition.row, count - 1);
rginda8ba33642011-12-14 12:31:31 -0800122};
123
124/**
125 * Remove the first row from the screen and return it.
126 *
127 * @return {HTMLElement} The first row in this screen.
128 */
129hterm.Screen.prototype.shiftRow = function() {
130 return this.shiftRows(1)[0];
rginda87b86462011-12-14 13:48:03 -0800131};
rginda8ba33642011-12-14 12:31:31 -0800132
133/**
134 * Remove rows from the top of the screen and return them as an array.
135 *
136 * @param {integer} count The number of rows to remove.
137 * @return {Array.<HTMLElement>} The selected rows.
138 */
139hterm.Screen.prototype.shiftRows = function(count) {
140 return this.rowsArray.splice(0, count);
141};
142
143/**
144 * Insert a row at the top of the screen.
145 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500146 * @param {HTMLElement} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800147 */
148hterm.Screen.prototype.unshiftRow = function(row) {
149 this.rowsArray.splice(0, 0, row);
150};
151
152/**
153 * Insert rows at the top of the screen.
154 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500155 * @param {Array.<HTMLElement>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800156 */
157hterm.Screen.prototype.unshiftRows = function(rows) {
158 this.rowsArray.unshift.apply(this.rowsArray, rows);
159};
160
161/**
162 * Remove the last row from the screen and return it.
163 *
164 * @return {HTMLElement} The last row in this screen.
165 */
166hterm.Screen.prototype.popRow = function() {
167 return this.popRows(1)[0];
168};
169
170/**
171 * Remove rows from the bottom of the screen and return them as an array.
172 *
173 * @param {integer} count The number of rows to remove.
174 * @return {Array.<HTMLElement>} The selected rows.
175 */
176hterm.Screen.prototype.popRows = function(count) {
177 return this.rowsArray.splice(this.rowsArray.length - count, count);
178};
179
180/**
181 * Insert a row at the bottom of the screen.
182 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500183 * @param {HTMLElement} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800184 */
185hterm.Screen.prototype.pushRow = function(row) {
186 this.rowsArray.push(row);
187};
188
189/**
190 * Insert rows at the bottom of the screen.
191 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500192 * @param {Array.<HTMLElement>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800193 */
194hterm.Screen.prototype.pushRows = function(rows) {
195 rows.push.apply(this.rowsArray, rows);
196};
197
198/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500199 * Insert a row at the specified row of the screen.
rginda8ba33642011-12-14 12:31:31 -0800200 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500201 * @param {integer} index The index to insert the row.
202 * @param {HTMLElement} row The row to insert.
rginda8ba33642011-12-14 12:31:31 -0800203 */
204hterm.Screen.prototype.insertRow = function(index, row) {
205 this.rowsArray.splice(index, 0, row);
206};
207
208/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500209 * Insert rows at the specified row of the screen.
rginda8ba33642011-12-14 12:31:31 -0800210 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500211 * @param {integer} index The index to insert the rows.
212 * @param {Array.<HTMLElement>} rows The rows to insert.
rginda8ba33642011-12-14 12:31:31 -0800213 */
214hterm.Screen.prototype.insertRows = function(index, rows) {
215 for (var i = 0; i < rows.length; i++) {
216 this.rowsArray.splice(index + i, 0, rows[i]);
217 }
218};
219
220/**
Evan Jones2600d4f2016-12-06 09:29:36 -0500221 * Remove a row from the screen and return it.
rginda8ba33642011-12-14 12:31:31 -0800222 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500223 * @param {integer} index The index of the row to remove.
rginda8ba33642011-12-14 12:31:31 -0800224 * @return {HTMLElement} The selected row.
225 */
226hterm.Screen.prototype.removeRow = function(index) {
227 return this.rowsArray.splice(index, 1)[0];
228};
229
230/**
231 * Remove rows from the bottom of the screen and return them as an array.
232 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500233 * @param {integer} index The index to start removing rows.
rginda8ba33642011-12-14 12:31:31 -0800234 * @param {integer} count The number of rows to remove.
235 * @return {Array.<HTMLElement>} The selected rows.
236 */
237hterm.Screen.prototype.removeRows = function(index, count) {
238 return this.rowsArray.splice(index, count);
239};
240
241/**
242 * Invalidate the current cursor position.
243 *
rginda87b86462011-12-14 13:48:03 -0800244 * This sets this.cursorPosition to (0, 0) and clears out some internal
rginda8ba33642011-12-14 12:31:31 -0800245 * data.
246 *
247 * Attempting to insert or overwrite text while the cursor position is invalid
248 * will raise an obscure exception.
249 */
250hterm.Screen.prototype.invalidateCursorPosition = function() {
rginda87b86462011-12-14 13:48:03 -0800251 this.cursorPosition.move(0, 0);
rginda8ba33642011-12-14 12:31:31 -0800252 this.cursorRowNode_ = null;
253 this.cursorNode_ = null;
254 this.cursorOffset_ = null;
255};
256
257/**
rginda8ba33642011-12-14 12:31:31 -0800258 * Clear the contents of the cursor row.
rginda8ba33642011-12-14 12:31:31 -0800259 */
260hterm.Screen.prototype.clearCursorRow = function() {
261 this.cursorRowNode_.innerHTML = '';
rgindaa09e7332012-08-17 12:49:51 -0700262 this.cursorRowNode_.removeAttribute('line-overflow');
rginda8ba33642011-12-14 12:31:31 -0800263 this.cursorOffset_ = 0;
rginda8ba33642011-12-14 12:31:31 -0800264 this.cursorPosition.column = 0;
rginda2312fff2012-01-05 16:20:52 -0800265 this.cursorPosition.overflow = false;
Robert Ginda7fd57082012-09-25 14:41:47 -0700266
267 var text;
268 if (this.textAttributes.isDefault()) {
269 text = '';
270 } else {
271 text = lib.f.getWhitespace(this.columnCount_);
272 }
273
Zhu Qunying30d40712017-03-14 16:27:00 -0700274 // We shouldn't honor inverse colors when clearing an area, to match
275 // xterm's back color erase behavior.
Edoardo Spadolini2fd43642014-08-23 22:59:57 +0200276 var inverse = this.textAttributes.inverse;
277 this.textAttributes.inverse = false;
278 this.textAttributes.syncColors();
279
Robert Ginda7fd57082012-09-25 14:41:47 -0700280 var node = this.textAttributes.createContainer(text);
281 this.cursorRowNode_.appendChild(node);
282 this.cursorNode_ = node;
Edoardo Spadolini2fd43642014-08-23 22:59:57 +0200283
284 this.textAttributes.inverse = inverse;
285 this.textAttributes.syncColors();
rginda8ba33642011-12-14 12:31:31 -0800286};
287
288/**
rgindaa09e7332012-08-17 12:49:51 -0700289 * Mark the current row as having overflowed to the next line.
290 *
291 * The line overflow state is used when converting a range of rows into text.
292 * It makes it possible to recombine two or more overflow terminal rows into
293 * a single line.
294 *
295 * This is distinct from the cursor being in the overflow state. Cursor
296 * overflow indicates that printing at the cursor position will commit a
297 * line overflow, unless it is preceded by a repositioning of the cursor
298 * to a non-overflow state.
299 */
300hterm.Screen.prototype.commitLineOverflow = function() {
301 this.cursorRowNode_.setAttribute('line-overflow', true);
302};
303
304/**
rginda8ba33642011-12-14 12:31:31 -0800305 * Relocate the cursor to a give row and column.
306 *
307 * @param {integer} row The zero based row.
308 * @param {integer} column The zero based column.
309 */
310hterm.Screen.prototype.setCursorPosition = function(row, column) {
rginda11057d52012-04-25 12:29:56 -0700311 if (!this.rowsArray.length) {
312 console.warn('Attempt to set cursor position on empty screen.');
313 return;
314 }
315
rginda87b86462011-12-14 13:48:03 -0800316 if (row >= this.rowsArray.length) {
rgindacbbd7482012-06-13 15:06:16 -0700317 console.error('Row out of bounds: ' + row);
rginda87b86462011-12-14 13:48:03 -0800318 row = this.rowsArray.length - 1;
319 } else if (row < 0) {
rgindacbbd7482012-06-13 15:06:16 -0700320 console.error('Row out of bounds: ' + row);
rginda87b86462011-12-14 13:48:03 -0800321 row = 0;
322 }
323
324 if (column >= this.columnCount_) {
rgindacbbd7482012-06-13 15:06:16 -0700325 console.error('Column out of bounds: ' + column);
rginda87b86462011-12-14 13:48:03 -0800326 column = this.columnCount_ - 1;
327 } else if (column < 0) {
rgindacbbd7482012-06-13 15:06:16 -0700328 console.error('Column out of bounds: ' + column);
rginda87b86462011-12-14 13:48:03 -0800329 column = 0;
330 }
rginda8ba33642011-12-14 12:31:31 -0800331
rginda2312fff2012-01-05 16:20:52 -0800332 this.cursorPosition.overflow = false;
333
rginda8ba33642011-12-14 12:31:31 -0800334 var rowNode = this.rowsArray[row];
335 var node = rowNode.firstChild;
336
337 if (!node) {
338 node = rowNode.ownerDocument.createTextNode('');
339 rowNode.appendChild(node);
340 }
341
rgindaa19afe22012-01-25 15:40:22 -0800342 var currentColumn = 0;
343
rginda8ba33642011-12-14 12:31:31 -0800344 if (rowNode == this.cursorRowNode_) {
345 if (column >= this.cursorPosition.column - this.cursorOffset_) {
346 node = this.cursorNode_;
347 currentColumn = this.cursorPosition.column - this.cursorOffset_;
348 }
349 } else {
350 this.cursorRowNode_ = rowNode;
351 }
352
353 this.cursorPosition.move(row, column);
354
355 while (node) {
356 var offset = column - currentColumn;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800357 var width = hterm.TextAttributes.nodeWidth(node);
358 if (!node.nextSibling || width > offset) {
rginda8ba33642011-12-14 12:31:31 -0800359 this.cursorNode_ = node;
360 this.cursorOffset_ = offset;
361 return;
362 }
363
Ricky Liang48f05cb2013-12-31 23:35:29 +0800364 currentColumn += width;
rginda8ba33642011-12-14 12:31:31 -0800365 node = node.nextSibling;
366 }
367};
368
369/**
rginda87b86462011-12-14 13:48:03 -0800370 * Set the provided selection object to be a caret selection at the current
371 * cursor position.
372 */
373hterm.Screen.prototype.syncSelectionCaret = function(selection) {
Rob Spies06533ba2014-04-24 11:20:37 -0700374 try {
375 selection.collapse(this.cursorNode_, this.cursorOffset_);
376 } catch (firefoxIgnoredException) {
377 // FF can throw an exception if the range is off, rather than just not
378 // performing the collapse.
379 }
rginda87b86462011-12-14 13:48:03 -0800380};
381
382/**
rgindaa19afe22012-01-25 15:40:22 -0800383 * Split a single node into two nodes at the given offset.
rginda8ba33642011-12-14 12:31:31 -0800384 *
rgindaa19afe22012-01-25 15:40:22 -0800385 * For example:
386 * Given the DOM fragment '<div><span>Hello World</span></div>', call splitNode_
Zhu Qunying30d40712017-03-14 16:27:00 -0700387 * passing the span and an offset of 6. This would modify the fragment to
rgindaa19afe22012-01-25 15:40:22 -0800388 * become: '<div><span>Hello </span><span>World</span></div>'. If the span
389 * had any attributes they would have been copied to the new span as well.
390 *
391 * The to-be-split node must have a container, so that the new node can be
392 * placed next to it.
393 *
394 * @param {HTMLNode} node The node to split.
395 * @param {integer} offset The offset into the node where the split should
396 * occur.
rginda8ba33642011-12-14 12:31:31 -0800397 */
rgindaa19afe22012-01-25 15:40:22 -0800398hterm.Screen.prototype.splitNode_ = function(node, offset) {
rginda35c456b2012-02-09 17:29:05 -0800399 var afterNode = node.cloneNode(false);
rgindaa19afe22012-01-25 15:40:22 -0800400
rginda35c456b2012-02-09 17:29:05 -0800401 var textContent = node.textContent;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800402 node.textContent = hterm.TextAttributes.nodeSubstr(node, 0, offset);
403 afterNode.textContent = lib.wc.substr(textContent, offset);
rgindaa19afe22012-01-25 15:40:22 -0800404
Ricky Liang48f05cb2013-12-31 23:35:29 +0800405 if (afterNode.textContent)
406 node.parentNode.insertBefore(afterNode, node.nextSibling);
407 if (!node.textContent)
408 node.parentNode.removeChild(node);
rginda8ba33642011-12-14 12:31:31 -0800409};
410
411/**
rgindaa9abdd82012-08-06 18:05:09 -0700412 * Ensure that text is clipped and the cursor is clamped to the column count.
rgindaa19afe22012-01-25 15:40:22 -0800413 */
rgindaa9abdd82012-08-06 18:05:09 -0700414hterm.Screen.prototype.maybeClipCurrentRow = function() {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800415 var width = hterm.TextAttributes.nodeWidth(this.cursorRowNode_);
416
417 if (width <= this.columnCount_) {
rgindaa9abdd82012-08-06 18:05:09 -0700418 // Current row does not need clipping, but may need clamping.
419 if (this.cursorPosition.column >= this.columnCount_) {
420 this.setCursorPosition(this.cursorPosition.row, this.columnCount_ - 1);
421 this.cursorPosition.overflow = true;
422 }
rgindaa19afe22012-01-25 15:40:22 -0800423
rgindaa9abdd82012-08-06 18:05:09 -0700424 return;
425 }
426
427 // Save off the current column so we can maybe restore it later.
428 var currentColumn = this.cursorPosition.column;
429
430 // Move the cursor to the final column.
431 this.setCursorPosition(this.cursorPosition.row, this.columnCount_ - 1);
432
433 // Remove any text that partially overflows.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800434 width = hterm.TextAttributes.nodeWidth(this.cursorNode_);
435
436 if (this.cursorOffset_ < width - 1) {
437 this.cursorNode_.textContent = hterm.TextAttributes.nodeSubstr(
438 this.cursorNode_, 0, this.cursorOffset_ + 1);
rgindaa9abdd82012-08-06 18:05:09 -0700439 }
440
441 // Remove all nodes after the cursor.
rgindaa19afe22012-01-25 15:40:22 -0800442 var rowNode = this.cursorRowNode_;
443 var node = this.cursorNode_.nextSibling;
444
445 while (node) {
rgindaa19afe22012-01-25 15:40:22 -0800446 rowNode.removeChild(node);
447 node = this.cursorNode_.nextSibling;
448 }
449
Robert Ginda7fd57082012-09-25 14:41:47 -0700450 if (currentColumn < this.columnCount_) {
rgindaa9abdd82012-08-06 18:05:09 -0700451 // If the cursor was within the screen before we started then restore its
452 // position.
rgindaa19afe22012-01-25 15:40:22 -0800453 this.setCursorPosition(this.cursorPosition.row, currentColumn);
rgindaa9abdd82012-08-06 18:05:09 -0700454 } else {
455 // Otherwise leave it at the the last column in the overflow state.
456 this.cursorPosition.overflow = true;
rgindaa19afe22012-01-25 15:40:22 -0800457 }
rgindaa19afe22012-01-25 15:40:22 -0800458};
459
460/**
461 * Insert a string at the current character position using the current
462 * text attributes.
463 *
rgindaa09e7332012-08-17 12:49:51 -0700464 * You must call maybeClipCurrentRow() after in order to clip overflowed
465 * text and clamp the cursor.
466 *
467 * It is also up to the caller to properly maintain the line overflow state
468 * using hterm.Screen..commitLineOverflow().
rginda8ba33642011-12-14 12:31:31 -0800469 */
Mike Frysinger6380bed2017-08-24 18:46:39 -0400470hterm.Screen.prototype.insertString = function(str, wcwidth=undefined) {
rgindaa19afe22012-01-25 15:40:22 -0800471 var cursorNode = this.cursorNode_;
472 var cursorNodeText = cursorNode.textContent;
rginda8ba33642011-12-14 12:31:31 -0800473
Robert Gindaa21dfb32013-10-31 14:17:45 -0700474 this.cursorRowNode_.removeAttribute('line-overflow');
475
Ricky Liang48f05cb2013-12-31 23:35:29 +0800476 // We may alter the width of the string by prepending some missing
477 // whitespaces, so we need to record the string width ahead of time.
Mike Frysinger6380bed2017-08-24 18:46:39 -0400478 if (wcwidth === undefined)
479 wcwidth = lib.wc.strWidth(str);
rginda8ba33642011-12-14 12:31:31 -0800480
rgindaa19afe22012-01-25 15:40:22 -0800481 // No matter what, before this function exits the cursor column will have
482 // moved this much.
Mike Frysinger6380bed2017-08-24 18:46:39 -0400483 this.cursorPosition.column += wcwidth;
rginda8ba33642011-12-14 12:31:31 -0800484
rgindaa19afe22012-01-25 15:40:22 -0800485 // Local cache of the cursor offset.
486 var offset = this.cursorOffset_;
rginda8ba33642011-12-14 12:31:31 -0800487
rgindaa19afe22012-01-25 15:40:22 -0800488 // Reverse offset is the offset measured from the end of the string.
489 // Zero implies that the cursor is at the end of the cursor node.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800490 var reverseOffset = hterm.TextAttributes.nodeWidth(cursorNode) - offset;
rgindaa19afe22012-01-25 15:40:22 -0800491
492 if (reverseOffset < 0) {
493 // A negative reverse offset means the cursor is positioned past the end
494 // of the characters on this line. We'll need to insert the missing
495 // whitespace.
rgindacbbd7482012-06-13 15:06:16 -0700496 var ws = lib.f.getWhitespace(-reverseOffset);
rgindaa19afe22012-01-25 15:40:22 -0800497
Brad Town7de83302015-03-12 02:10:32 -0700498 // This whitespace should be completely unstyled. Underline, background
499 // color, and strikethrough would be visible on whitespace, so we can't use
500 // one of those spans to hold the text.
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200501 if (!(this.textAttributes.underline ||
Brad Town7de83302015-03-12 02:10:32 -0700502 this.textAttributes.strikethrough ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200503 this.textAttributes.background ||
504 this.textAttributes.wcNode ||
Mike Frysinger1e98c0f2017-08-15 01:21:31 -0400505 !this.textAttributes.asciiNode ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200506 this.textAttributes.tileData != null)) {
rgindaa19afe22012-01-25 15:40:22 -0800507 // Best case scenario, we can just pretend the spaces were part of the
508 // original string.
509 str = ws + str;
Mike Frysinger6a4f2412017-08-31 01:11:25 -0400510 } else if (cursorNode.nodeType == Node.TEXT_NODE ||
Ricky Liang48f05cb2013-12-31 23:35:29 +0800511 !(cursorNode.wcNode ||
Mike Frysinger1e98c0f2017-08-15 01:21:31 -0400512 !cursorNode.asciiNode ||
Edoardo Spadolini198828a2014-08-08 00:22:51 +0200513 cursorNode.tileNode ||
Ricky Liang48f05cb2013-12-31 23:35:29 +0800514 cursorNode.style.textDecoration ||
rgindaa19afe22012-01-25 15:40:22 -0800515 cursorNode.style.backgroundColor)) {
516 // Second best case, the current node is able to hold the whitespace.
517 cursorNode.textContent = (cursorNodeText += ws);
518 } else {
519 // Worst case, we have to create a new node to hold the whitespace.
520 var wsNode = cursorNode.ownerDocument.createTextNode(ws);
521 this.cursorRowNode_.insertBefore(wsNode, cursorNode.nextSibling);
522 this.cursorNode_ = cursorNode = wsNode;
523 this.cursorOffset_ = offset = -reverseOffset;
524 cursorNodeText = ws;
525 }
526
527 // We now know for sure that we're at the last character of the cursor node.
528 reverseOffset = 0;
rginda8ba33642011-12-14 12:31:31 -0800529 }
530
rgindaa19afe22012-01-25 15:40:22 -0800531 if (this.textAttributes.matchesContainer(cursorNode)) {
532 // The new text can be placed directly in the cursor node.
533 if (reverseOffset == 0) {
534 cursorNode.textContent = cursorNodeText + str;
535 } else if (offset == 0) {
536 cursorNode.textContent = str + cursorNodeText;
537 } else {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800538 cursorNode.textContent =
539 hterm.TextAttributes.nodeSubstr(cursorNode, 0, offset) +
540 str + hterm.TextAttributes.nodeSubstr(cursorNode, offset);
rgindaa19afe22012-01-25 15:40:22 -0800541 }
rginda8ba33642011-12-14 12:31:31 -0800542
Mike Frysinger6380bed2017-08-24 18:46:39 -0400543 this.cursorOffset_ += wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800544 return;
rginda87b86462011-12-14 13:48:03 -0800545 }
546
rgindaa19afe22012-01-25 15:40:22 -0800547 // The cursor node is the wrong style for the new text. If we're at the
548 // beginning or end of the cursor node, then the adjacent node is also a
549 // potential candidate.
rginda8ba33642011-12-14 12:31:31 -0800550
rgindaa19afe22012-01-25 15:40:22 -0800551 if (offset == 0) {
552 // At the beginning of the cursor node, the check the previous sibling.
553 var previousSibling = cursorNode.previousSibling;
554 if (previousSibling &&
555 this.textAttributes.matchesContainer(previousSibling)) {
556 previousSibling.textContent += str;
557 this.cursorNode_ = previousSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800558 this.cursorOffset_ = lib.wc.strWidth(previousSibling.textContent);
rgindaa19afe22012-01-25 15:40:22 -0800559 return;
560 }
561
562 var newNode = this.textAttributes.createContainer(str);
563 this.cursorRowNode_.insertBefore(newNode, cursorNode);
564 this.cursorNode_ = newNode;
Mike Frysinger6380bed2017-08-24 18:46:39 -0400565 this.cursorOffset_ = wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800566 return;
567 }
568
569 if (reverseOffset == 0) {
570 // At the end of the cursor node, the check the next sibling.
571 var nextSibling = cursorNode.nextSibling;
572 if (nextSibling &&
573 this.textAttributes.matchesContainer(nextSibling)) {
574 nextSibling.textContent = str + nextSibling.textContent;
575 this.cursorNode_ = nextSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800576 this.cursorOffset_ = lib.wc.strWidth(str);
rgindaa19afe22012-01-25 15:40:22 -0800577 return;
578 }
579
580 var newNode = this.textAttributes.createContainer(str);
581 this.cursorRowNode_.insertBefore(newNode, nextSibling);
582 this.cursorNode_ = newNode;
583 // We specifically need to include any missing whitespace here, since it's
584 // going in a new node.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800585 this.cursorOffset_ = hterm.TextAttributes.nodeWidth(newNode);
rgindaa19afe22012-01-25 15:40:22 -0800586 return;
587 }
588
589 // Worst case, we're somewhere in the middle of the cursor node. We'll
590 // have to split it into two nodes and insert our new container in between.
591 this.splitNode_(cursorNode, offset);
592 var newNode = this.textAttributes.createContainer(str);
593 this.cursorRowNode_.insertBefore(newNode, cursorNode.nextSibling);
594 this.cursorNode_ = newNode;
Mike Frysinger6380bed2017-08-24 18:46:39 -0400595 this.cursorOffset_ = wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800596};
597
598/**
rginda8ba33642011-12-14 12:31:31 -0800599 * Overwrite the text at the current cursor position.
600 *
rgindaa09e7332012-08-17 12:49:51 -0700601 * You must call maybeClipCurrentRow() after in order to clip overflowed
602 * text and clamp the cursor.
603 *
604 * It is also up to the caller to properly maintain the line overflow state
605 * using hterm.Screen..commitLineOverflow().
rginda8ba33642011-12-14 12:31:31 -0800606 */
Mike Frysinger6380bed2017-08-24 18:46:39 -0400607hterm.Screen.prototype.overwriteString = function(str, wcwidth=undefined) {
rginda8ba33642011-12-14 12:31:31 -0800608 var maxLength = this.columnCount_ - this.cursorPosition.column;
609 if (!maxLength)
rgindaa19afe22012-01-25 15:40:22 -0800610 return [str];
611
Mike Frysinger6380bed2017-08-24 18:46:39 -0400612 if (wcwidth === undefined)
613 wcwidth = lib.wc.strWidth(str);
614
Ricky Liang48f05cb2013-12-31 23:35:29 +0800615 if (this.textAttributes.matchesContainer(this.cursorNode_) &&
616 this.cursorNode_.textContent.substr(this.cursorOffset_) == str) {
rgindaa19afe22012-01-25 15:40:22 -0800617 // This overwrite would be a no-op, just move the cursor and return.
Mike Frysinger6380bed2017-08-24 18:46:39 -0400618 this.cursorOffset_ += wcwidth;
619 this.cursorPosition.column += wcwidth;
rgindaa19afe22012-01-25 15:40:22 -0800620 return;
621 }
rginda8ba33642011-12-14 12:31:31 -0800622
Mike Frysinger6380bed2017-08-24 18:46:39 -0400623 this.deleteChars(Math.min(wcwidth, maxLength));
624 this.insertString(str, wcwidth);
rginda8ba33642011-12-14 12:31:31 -0800625};
626
627/**
628 * Forward-delete one or more characters at the current cursor position.
629 *
630 * Text to the right of the deleted characters is shifted left. Only affects
631 * characters on the same row as the cursor.
632 *
Ricky Liang48f05cb2013-12-31 23:35:29 +0800633 * @param {integer} count The column width of characters to delete. This is
634 * clamped to the column width minus the cursor column.
635 * @return {integer} The column width of the characters actually deleted.
rginda8ba33642011-12-14 12:31:31 -0800636 */
637hterm.Screen.prototype.deleteChars = function(count) {
638 var node = this.cursorNode_;
639 var offset = this.cursorOffset_;
640
Robert Ginda7fd57082012-09-25 14:41:47 -0700641 var currentCursorColumn = this.cursorPosition.column;
642 count = Math.min(count, this.columnCount_ - currentCursorColumn);
643 if (!count)
644 return 0;
645
646 var rv = count;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800647 var startLength, endLength;
rgindaa19afe22012-01-25 15:40:22 -0800648
rginda8ba33642011-12-14 12:31:31 -0800649 while (node && count) {
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400650 // Sanity check so we don't loop forever, but we don't also go quietly.
651 if (count < 0) {
652 console.error(`Deleting ${rv} chars went negative: ${count}`);
653 break;
654 }
655
Ricky Liang48f05cb2013-12-31 23:35:29 +0800656 startLength = hterm.TextAttributes.nodeWidth(node);
657 node.textContent = hterm.TextAttributes.nodeSubstr(node, 0, offset) +
658 hterm.TextAttributes.nodeSubstr(node, offset + count);
659 endLength = hterm.TextAttributes.nodeWidth(node);
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400660
661 // Deal with splitting wide characters. There are two ways: we could delete
662 // the first column or the second column. In both cases, we delete the wide
663 // character and replace one of the columns with a space (since the other
664 // was deleted). If there are more chars to delete, the next loop will pick
665 // up the slack.
666 if (node.wcNode && offset < startLength &&
667 ((endLength && startLength == endLength) || (!endLength && offset == 1))) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800668 // No characters were deleted when there should be. We're probably trying
669 // to delete one column width from a wide character node. We remove the
670 // wide character node here and replace it with a single space.
671 var spaceNode = this.textAttributes.createContainer(' ');
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400672 node.parentNode.insertBefore(spaceNode, offset ? node : node.nextSibling);
Ricky Liang48f05cb2013-12-31 23:35:29 +0800673 node.textContent = '';
674 endLength = 0;
675 count -= 1;
Mike Frysinger859bcbd2017-08-28 23:48:43 -0400676 } else
677 count -= startLength - endLength;
rginda8ba33642011-12-14 12:31:31 -0800678
Ricky Liang48f05cb2013-12-31 23:35:29 +0800679 var nextNode = node.nextSibling;
680 if (endLength == 0 && node != this.cursorNode_) {
681 node.parentNode.removeChild(node);
682 }
683 node = nextNode;
rginda8ba33642011-12-14 12:31:31 -0800684 offset = 0;
685 }
Robert Ginda7fd57082012-09-25 14:41:47 -0700686
Ricky Liang48f05cb2013-12-31 23:35:29 +0800687 // Remove this.cursorNode_ if it is an empty non-text node.
Mike Frysinger6a4f2412017-08-31 01:11:25 -0400688 if (this.cursorNode_.nodeType != Node.TEXT_NODE &&
689 !this.cursorNode_.textContent) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800690 var cursorNode = this.cursorNode_;
691 if (cursorNode.previousSibling) {
692 this.cursorNode_ = cursorNode.previousSibling;
693 this.cursorOffset_ = hterm.TextAttributes.nodeWidth(
694 cursorNode.previousSibling);
695 } else if (cursorNode.nextSibling) {
696 this.cursorNode_ = cursorNode.nextSibling;
697 this.cursorOffset_ = 0;
698 } else {
699 var emptyNode = this.cursorRowNode_.ownerDocument.createTextNode('');
700 this.cursorRowNode_.appendChild(emptyNode);
701 this.cursorNode_ = emptyNode;
702 this.cursorOffset_ = 0;
703 }
704 this.cursorRowNode_.removeChild(cursorNode);
705 }
706
Robert Ginda7fd57082012-09-25 14:41:47 -0700707 return rv;
rginda8ba33642011-12-14 12:31:31 -0800708};
John Macinnesfb683832013-07-22 14:46:30 -0400709
710/**
711 * Finds first X-ROW of a line containing specified X-ROW.
712 * Used to support line overflow.
713 *
714 * @param {Node} row X-ROW to begin search for first row of line.
715 * @return {Node} The X-ROW that is at the beginning of the line.
716 **/
717hterm.Screen.prototype.getLineStartRow_ = function(row) {
718 while (row.previousSibling &&
719 row.previousSibling.hasAttribute('line-overflow')) {
720 row = row.previousSibling;
721 }
722 return row;
723};
724
725/**
726 * Gets text of a line beginning with row.
727 * Supports line overflow.
728 *
729 * @param {Node} row First X-ROW of line.
730 * @return {string} Text content of line.
731 **/
732hterm.Screen.prototype.getLineText_ = function(row) {
733 var rowText = "";
734 while (row) {
735 rowText += row.textContent;
736 if (row.hasAttribute('line-overflow')) {
737 row = row.nextSibling;
738 } else {
739 break;
740 }
741 }
742 return rowText;
743};
744
745/**
746 * Returns X-ROW that is ancestor of the node.
747 *
748 * @param {Node} node Node to get X-ROW ancestor for.
749 * @return {Node} X-ROW ancestor of node, or null if not found.
750 **/
751hterm.Screen.prototype.getXRowAncestor_ = function(node) {
752 while (node) {
753 if (node.nodeName === 'X-ROW')
754 break;
755 node = node.parentNode;
756 }
757 return node;
758};
759
760/**
761 * Returns position within line of character at offset within node.
762 * Supports line overflow.
763 *
764 * @param {Node} row X-ROW at beginning of line.
765 * @param {Node} node Node to get position of.
766 * @param {integer} offset Offset into node.
767 *
768 * @return {integer} Position within line of character at offset within node.
769 **/
770hterm.Screen.prototype.getPositionWithOverflow_ = function(row, node, offset) {
771 if (!node)
772 return -1;
773 var ancestorRow = this.getXRowAncestor_(node);
774 if (!ancestorRow)
775 return -1;
776 var position = 0;
777 while (ancestorRow != row) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800778 position += hterm.TextAttributes.nodeWidth(row);
John Macinnesfb683832013-07-22 14:46:30 -0400779 if (row.hasAttribute('line-overflow') && row.nextSibling) {
780 row = row.nextSibling;
781 } else {
782 return -1;
783 }
784 }
785 return position + this.getPositionWithinRow_(row, node, offset);
786};
787
788/**
789 * Returns position within row of character at offset within node.
790 * Does not support line overflow.
791 *
792 * @param {Node} row X-ROW to get position within.
793 * @param {Node} node Node to get position for.
794 * @param {integer} offset Offset within node to get position for.
795 * @return {integer} Position within row of character at offset within node.
796 **/
797hterm.Screen.prototype.getPositionWithinRow_ = function(row, node, offset) {
798 if (node.parentNode != row) {
Mike Frysinger498192d2017-06-26 18:23:31 -0400799 // If we traversed to the top node, then there's nothing to find here.
800 if (node.parentNode == null)
801 return -1;
802
John Macinnesfb683832013-07-22 14:46:30 -0400803 return this.getPositionWithinRow_(node.parentNode, node, offset) +
804 this.getPositionWithinRow_(row, node.parentNode, 0);
805 }
806 var position = 0;
807 for (var i = 0; i < row.childNodes.length; i++) {
808 var currentNode = row.childNodes[i];
809 if (currentNode == node)
810 return position + offset;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800811 position += hterm.TextAttributes.nodeWidth(currentNode);
John Macinnesfb683832013-07-22 14:46:30 -0400812 }
813 return -1;
814};
815
816/**
817 * Returns the node and offset corresponding to position within line.
818 * Supports line overflow.
819 *
820 * @param {Node} row X-ROW at beginning of line.
821 * @param {integer} position Position within line to retrieve node and offset.
822 * @return {Array} Two element array containing node and offset respectively.
823 **/
824hterm.Screen.prototype.getNodeAndOffsetWithOverflow_ = function(row, position) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800825 while (row && position > hterm.TextAttributes.nodeWidth(row)) {
John Macinnesfb683832013-07-22 14:46:30 -0400826 if (row.hasAttribute('line-overflow') && row.nextSibling) {
Ricky Liang48f05cb2013-12-31 23:35:29 +0800827 position -= hterm.TextAttributes.nodeWidth(row);
John Macinnesfb683832013-07-22 14:46:30 -0400828 row = row.nextSibling;
829 } else {
830 return -1;
831 }
832 }
833 return this.getNodeAndOffsetWithinRow_(row, position);
834};
835
836/**
837 * Returns the node and offset corresponding to position within row.
838 * Does not support line overflow.
839 *
840 * @param {Node} row X-ROW to get position within.
841 * @param {integer} position Position within row to retrieve node and offset.
842 * @return {Array} Two element array containing node and offset respectively.
843 **/
844hterm.Screen.prototype.getNodeAndOffsetWithinRow_ = function(row, position) {
845 for (var i = 0; i < row.childNodes.length; i++) {
846 var node = row.childNodes[i];
Ricky Liang48f05cb2013-12-31 23:35:29 +0800847 var nodeTextWidth = hterm.TextAttributes.nodeWidth(node);
848 if (position <= nodeTextWidth) {
John Macinnesfb683832013-07-22 14:46:30 -0400849 if (node.nodeName === 'SPAN') {
850 /** Drill down to node contained by SPAN. **/
851 return this.getNodeAndOffsetWithinRow_(node, position);
852 } else {
853 return [node, position];
854 }
855 }
Ricky Liang48f05cb2013-12-31 23:35:29 +0800856 position -= nodeTextWidth;
John Macinnesfb683832013-07-22 14:46:30 -0400857 }
858 return null;
859};
860
861/**
862 * Returns the node and offset corresponding to position within line.
863 * Supports line overflow.
864 *
865 * @param {Node} row X-ROW at beginning of line.
866 * @param {integer} start Start position of range within line.
867 * @param {integer} end End position of range within line.
868 * @param {Range} range Range to modify.
869 **/
870hterm.Screen.prototype.setRange_ = function(row, start, end, range) {
871 var startNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, start);
872 if (startNodeAndOffset == null)
873 return;
874 var endNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, end);
875 if (endNodeAndOffset == null)
876 return;
877 range.setStart(startNodeAndOffset[0], startNodeAndOffset[1]);
878 range.setEnd(endNodeAndOffset[0], endNodeAndOffset[1]);
879};
880
881/**
882 * Expands selection to surround URLs.
883 *
John Macinnesfb683832013-07-22 14:46:30 -0400884 * @param {Selection} selection Selection to expand.
885 **/
886hterm.Screen.prototype.expandSelection = function(selection) {
887 if (!selection)
888 return;
889
890 var range = selection.getRangeAt(0);
891 if (!range || range.toString().match(/\s/))
892 return;
893
894 var row = this.getLineStartRow_(this.getXRowAncestor_(range.startContainer));
895 if (!row)
896 return;
897
898 var startPosition = this.getPositionWithOverflow_(row,
899 range.startContainer,
900 range.startOffset);
901 if (startPosition == -1)
902 return;
903 var endPosition = this.getPositionWithOverflow_(row,
904 range.endContainer,
905 range.endOffset);
906 if (endPosition == -1)
907 return;
908
Mike Frysinger664e9992017-05-19 01:24:24 -0400909 // Use the user configurable match settings.
910 var leftMatch = this.wordBreakMatchLeft;
911 var rightMatch = this.wordBreakMatchRight;
912 var insideMatch = this.wordBreakMatchMiddle;
John Macinnesfb683832013-07-22 14:46:30 -0400913
914 //Move start to the left.
915 var rowText = this.getLineText_(row);
Ricky Liang48f05cb2013-12-31 23:35:29 +0800916 var lineUpToRange = lib.wc.substring(rowText, 0, endPosition);
Robert Ginda5eba4562014-08-11 11:05:54 -0700917 var leftRegularExpression = new RegExp(leftMatch + insideMatch + "$");
John Macinnesfb683832013-07-22 14:46:30 -0400918 var expandedStart = lineUpToRange.search(leftRegularExpression);
919 if (expandedStart == -1 || expandedStart > startPosition)
920 return;
921
922 //Move end to the right.
Ricky Liang48f05cb2013-12-31 23:35:29 +0800923 var lineFromRange = lib.wc.substring(rowText, startPosition,
924 lib.wc.strWidth(rowText));
Robert Ginda5eba4562014-08-11 11:05:54 -0700925 var rightRegularExpression = new RegExp("^" + insideMatch + rightMatch);
John Macinnesfb683832013-07-22 14:46:30 -0400926 var found = lineFromRange.match(rightRegularExpression);
927 if (!found)
928 return;
Ricky Liang48f05cb2013-12-31 23:35:29 +0800929 var expandedEnd = startPosition + lib.wc.strWidth(found[0]);
John Macinnesfb683832013-07-22 14:46:30 -0400930 if (expandedEnd == -1 || expandedEnd < endPosition)
931 return;
932
933 this.setRange_(row, expandedStart, expandedEnd, range);
934 selection.addRange(range);
935};
Mike Frysingera2cacaa2017-11-29 13:51:09 -0800936
937/**
938 * Save the current cursor state to the corresponding screens.
939 *
940 * @param {hterm.VT} vt The VT object to read graphic codeset details from.
941 */
942hterm.Screen.prototype.saveCursorAndState = function(vt) {
943 this.cursorState_.save(vt);
944};
945
946/**
947 * Restore the saved cursor state in the corresponding screens.
948 *
949 * @param {hterm.VT} vt The VT object to write graphic codeset details to.
950 */
951hterm.Screen.prototype.restoreCursorAndState = function(vt) {
952 this.cursorState_.restore(vt);
953};
954
955/**
956 * Track all the things related to the current "cursor".
957 *
958 * The set of things saved & restored here is defined by DEC:
959 * https://vt100.net/docs/vt510-rm/DECSC.html
960 * - Cursor position
961 * - Character attributes set by the SGR command
962 * - Character sets (G0, G1, G2, or G3) currently in GL and GR
963 * - Wrap flag (autowrap or no autowrap)
964 * - State of origin mode (DECOM)
965 * - Selective erase attribute
966 * - Any single shift 2 (SS2) or single shift 3 (SS3) functions sent
967 *
968 * These are done on a per-screen basis.
969 */
970hterm.Screen.CursorState = function(screen) {
971 this.screen_ = screen;
972 this.cursor = null;
973 this.textAttributes = null;
974 this.GL = this.GR = this.G0 = this.G1 = this.G2 = this.G3 = null;
975};
976
977/**
978 * Save all the cursor state.
979 *
980 * @param {hterm.VT} vt The VT object to read graphic codeset details from.
981 */
982hterm.Screen.CursorState.prototype.save = function(vt) {
983 this.cursor = vt.terminal.saveCursor();
984
985 this.textAttributes = this.screen_.textAttributes.clone();
986
987 this.GL = vt.GL;
988 this.GR = vt.GR;
989
990 this.G0 = vt.G0;
991 this.G1 = vt.G1;
992 this.G2 = vt.G2;
993 this.G3 = vt.G3;
994};
995
996/**
997 * Restore the previously saved cursor state.
998 *
999 * @param {hterm.VT} vt The VT object to write graphic codeset details to.
1000 */
1001hterm.Screen.CursorState.prototype.restore = function(vt) {
1002 vt.terminal.restoreCursor(this.cursor);
1003
1004 // Cursor restore includes char attributes (bold/etc...), but does not change
1005 // the color palette (which are a terminal setting).
1006 const tattrs = this.textAttributes.clone();
1007 tattrs.colorPalette = this.screen_.textAttributes.colorPalette;
1008 tattrs.syncColors();
1009
1010 this.screen_.textAttributes = tattrs;
1011
1012 vt.GL = this.GL;
1013 vt.GR = this.GR;
1014
1015 vt.G0 = this.G0;
1016 vt.G1 = this.G1;
1017 vt.G2 = this.G2;
1018 vt.G3 = this.G3;
1019};