Chromium Code Reviews| Index: chrome/browser/resources/hterm/js/screen.js |
| diff --git a/chrome/browser/resources/hterm/js/screen.js b/chrome/browser/resources/hterm/js/screen.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..d513586e454abf70bdcdec7583bc892873fb122d |
| --- /dev/null |
| +++ b/chrome/browser/resources/hterm/js/screen.js |
| @@ -0,0 +1,429 @@ |
| +// Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +/** |
| + * @fileoverview This class represents a single terminal screen full of text. |
| + * |
| + * It maintains the current cursor position and has basic methods for text |
| + * insert and overwrite, and adding or removing rows from the screen. |
| + * |
| + * This class has no knowledge of the scrollback buffer. |
| + * |
| + * The number of rows on the screen is determined only by the number of rows |
| + * that the caller inserts into the screen. If a caller wants to ensure a |
| + * constant number of rows on the screen, it's their responsibility to remove a |
| + * row for each row inserted. |
| + * |
| + * The screen width, in contrast, is enforced locally. |
| + * |
| + * |
| + * In practice... |
| + * - The hterm.Terminal class holds two hterm.Screen instances. One for the |
| + * primary screen and one for the alternate screen. |
| + * |
| + * - The html.Screen class only cares that rows are HTMLElements. In the |
| + * larger context of hterm, however, the rows happen to be displayed by an |
| + * hterm.ScrollPort and have to follow a few rules as a result. Each |
| + * row must be rooted by the custom HTML tag 'x-row', and each must have a |
| + * rowIndex property that corresponds to the index of the row in the context |
| + * of the scrollback buffer. These invariants are enforced by hterm.Terminal |
| + * because that is the class using the hterm.Screen in the context of an |
| + * hterm.ScrollPort. |
| + */ |
| + |
| +/** |
| + * Create a new screen instance. |
| + * |
| + * The screen initially has no rows and a maximum column count of 0. |
| + * |
| + * @param {integer} opt_columnCount The maximum number of columns for this |
| + * screen. See insertString() and overwriteString() for information about |
| + * what happens when too many characters are added too a row. Defaults to |
| + * 0 if not provided. |
| + */ |
| +hterm.Screen = function(opt_columnCount) { |
| + /** |
| + * Public, read-only access to the rows in this screen. |
| + */ |
| + this.rowsArray = []; |
| + |
| + // The max column width for this screen. |
| + this.columnCount_ = opt_columnCount || 0; |
| + |
| + // Current zero-based cursor coordinates. (-1, -1) implies that the cursor |
| + // is uninitialized. |
| + this.cursorPosition = new hterm.RowCol(-1, -1); |
| + |
| + // The node containing the row that the cursor is positioned on. |
| + this.cursorRowNode_ = null; |
| + |
| + // The node containing the span of text that the cursor is positioned on. |
| + this.cursorNode_ = null; |
| + |
| + // The offset into cursorNode_ where the cursor is positioned. |
| + this.cursorOffset_ = null; |
| +}; |
| + |
| +/** |
| + * Return the screen size as an hterm.Size object. |
| + * |
| + * @return {hterm.Size} hterm.Size object representing the current number |
| + * of rows and columns in this screen. |
| + */ |
| +hterm.Screen.prototype.getSize = function() { |
| + return new hterm.Size(this.columnCount_, this.rowsArray.length); |
| +}; |
| + |
| +/** |
| + * Return the current number of rows in this screen. |
| + * |
| + * @return {integer} The number of rows in this screen. |
| + */ |
| +hterm.Screen.prototype.getHeight = function() { |
| + return this.rowsArray.length; |
| +}; |
| + |
| +/** |
| + * Return the current number of columns in this screen. |
| + * |
| + * @return {integer} The number of columns in this screen. |
| + */ |
| +hterm.Screen.prototype.getWidth = function() { |
| + return this.columnCount_; |
| +}; |
| + |
| +/** |
| + * Set the maximum number of columns per row. |
| + * |
| + * TODO(rginda): This should probably clip existing rows if the count is |
| + * decreased. |
| + * |
| + * @param {integer} count The maximum number of columns per row. |
| + */ |
| +hterm.Screen.prototype.setColumnCount = function(count) { |
| + this.columnCount_ = count; |
| +}; |
| + |
| +/** |
| + * Remove the first row from the screen and return it. |
| + * |
| + * @return {HTMLElement} The first row in this screen. |
| + */ |
| +hterm.Screen.prototype.shiftRow = function() { |
| + return this.shiftRows(1)[0]; |
| +} |
| + |
| +/** |
| + * Remove rows from the top of the screen and return them as an array. |
| + * |
| + * @param {integer} count The number of rows to remove. |
| + * @return {Array.<HTMLElement>} The selected rows. |
| + */ |
| +hterm.Screen.prototype.shiftRows = function(count) { |
| + return this.rowsArray.splice(0, count); |
| +}; |
| + |
| +/** |
| + * Insert a row at the top of the screen. |
| + * |
| + * @param {HTMLElement} The row to insert. |
| + */ |
| +hterm.Screen.prototype.unshiftRow = function(row) { |
| + this.rowsArray.splice(0, 0, row); |
| +}; |
| + |
| +/** |
| + * Insert rows at the top of the screen. |
| + * |
| + * @param {Array.<HTMLElement>} The rows to insert. |
| + */ |
| +hterm.Screen.prototype.unshiftRows = function(rows) { |
| + rows.unshift.apply(this.rowsArray, rows); |
|
dgozman
2011/11/29 11:35:42
Why rows.unshift.apply, and not this.rowsArray.uns
rginda
2011/11/29 19:26:12
Done.
|
| +}; |
| + |
| +/** |
| + * Remove the last row from the screen and return it. |
| + * |
| + * @return {HTMLElement} The last row in this screen. |
| + */ |
| +hterm.Screen.prototype.popRow = function() { |
| + return this.popRows(1)[0]; |
| +}; |
| + |
| +/** |
| + * Remove rows from the bottom of the screen and return them as an array. |
| + * |
| + * @param {integer} count The number of rows to remove. |
| + * @return {Array.<HTMLElement>} The selected rows. |
| + */ |
| +hterm.Screen.prototype.popRows = function(count) { |
| + return this.rowsArray.splice(this.rowsArray.length - count, count); |
| +}; |
| + |
| +/** |
| + * Insert a row at the bottom of the screen. |
| + * |
| + * @param {HTMLElement} The row to insert. |
| + */ |
| +hterm.Screen.prototype.pushRow = function(row) { |
| + var start = this.rowsArray.length; |
|
dgozman
2011/11/29 11:35:42
start not used
rginda
2011/11/29 19:26:12
Done.
|
| + this.rowsArray.push(row); |
| +}; |
| + |
| +/** |
| + * Insert rows at the bottom of the screen. |
| + * |
| + * @param {Array.<HTMLElement>} The rows to insert. |
| + */ |
| +hterm.Screen.prototype.pushRows = function(rows) { |
| + var start = this.rowsArray.length; |
|
dgozman
2011/11/29 11:35:42
start not used
rginda
2011/11/29 19:26:12
Done.
|
| + rows.push.apply(this.rowsArray, rows); |
| +}; |
| + |
| +/** |
| + * Insert a row at the specified column of the screen. |
| + * |
| + * @param {HTMLElement} The row to insert. |
| + */ |
| +hterm.Screen.prototype.insertRow = function(index, row) { |
| + this.rowsArray.splice(index, 0, row); |
| +}; |
| + |
| +/** |
| + * Insert rows at the specified column of the screen. |
| + * |
| + * @param {Array.<HTMLElement>} The rows to insert. |
| + */ |
| +hterm.Screen.prototype.insertRows = function(index, rows) { |
| + for (var i = 0; i < rows.length; i++) { |
| + this.rowsArray.splice(index + i, 0, rows[i]); |
|
arv (Not doing code reviews)
2011/11/28 18:21:00
Array.prototype.splice.apply(this.rowsArray, index
rginda
2011/11/28 20:39:47
That's the way I had written it at first, but it's
arv (Not doing code reviews)
2011/11/28 21:33:22
My bad.
You could use bind but at this point I'm
dgozman
2011/11/29 11:35:42
Maybe, splice.apply will be faster? It's just two
rginda
2011/11/29 19:26:12
If I modify the rows array it'll trash something t
|
| + } |
| +}; |
| + |
| +/** |
| + * Remove a last row from the specified column of the screen and return it. |
| + * |
| + * @return {HTMLElement} The selected row. |
| + */ |
| +hterm.Screen.prototype.removeRow = function(index) { |
| + return this.rowsArray.splice(index, 1)[0]; |
| +}; |
| + |
| +/** |
| + * Remove rows from the bottom of the screen and return them as an array. |
| + * |
| + * @param {integer} count The number of rows to remove. |
| + * @return {Array.<HTMLElement>} The selected rows. |
| + */ |
| +hterm.Screen.prototype.removeRows = function(index, count) { |
| + return this.rowsArray.splice(index, count); |
| +}; |
| + |
| +/** |
| + * Invalidate the current cursor position. |
| + * |
| + * This sets this.cursorPosition to (-1, -1) and clears out some internal |
| + * data. |
| + * |
| + * Attempting to insert or overwrite text while the cursor position is invalid |
| + * will raise an obscure exception. |
| + */ |
| +hterm.Screen.prototype.invalidateCursorPosition = function() { |
| + this.cursorPosition.move(-1, -1); |
| + this.cursorRowNode_ = null; |
| + this.cursorNode_ = null; |
| + this.cursorOffset_ = null; |
| +}; |
| + |
| +/** |
| + * Clear the contents of a selected row. |
| + * |
| + * TODO: Make this clear in the current style... somehow. We can't just |
| + * fill the row with spaces, since they would have potential to mess up the |
| + * terminal (for example, in insert mode, they might wrap around to the next |
| + * line. |
| + * |
| + * @param {integer} index The zero-based index to clear. |
| + */ |
| +hterm.Screen.prototype.clearRow = function(index) { |
| + if (index == this.cursorPosition.row) { |
| + this.clearCursorRow(); |
| + } else { |
| + var row = this.rowsArray[index]; |
| + row.innerHTML = ''; |
| + row.appendChild(row.ownerDocument.createTextNode('')) |
|
dgozman
2011/11/29 11:35:42
semicolon
rginda
2011/11/29 19:26:12
Done.
|
| + } |
| +}; |
| + |
| +/** |
| + * Clear the contents of the cursor row. |
| + * |
| + * TODO: Same comment as clearRow(). |
| + */ |
| +hterm.Screen.prototype.clearCursorRow = function() { |
| + this.cursorRowNode_.innerHTML = ''; |
| + var text = this.cursorRowNode_.ownerDocument.createTextNode(''); |
| + this.cursorRowNode_.appendChild(text); |
| + this.cursorOffset_ = 0; |
| + this.cursorNode_ = text; |
| + this.cursorPosition.column = 0; |
| +}; |
| + |
| +/** |
| + * Relocate the cursor to a give row and column. |
| + * |
| + * @param {integer} row The zero based row. |
| + * @param {integer} column The zero based column. |
| + */ |
| +hterm.Screen.prototype.setCursorPosition = function(row, column) { |
| + var currentColumn = 0; |
| + if (row >= this.rowsArray.length) |
| + throw 'Row out of bounds: ' + row; |
| + |
| + var rowNode = this.rowsArray[row]; |
| + var node = rowNode.firstChild; |
| + |
| + if (!node) { |
| + node = rowNode.ownerDocument.createTextNode(''); |
| + rowNode.appendChild(node); |
| + } |
| + |
| + if (rowNode == this.cursorRowNode_) { |
| + if (column >= this.cursorPosition.column - this.cursorOffset_) { |
| + node = this.cursorNode_; |
| + currentColumn = this.cursorPosition.column - this.cursorOffset_; |
| + } |
| + } else { |
| + this.cursorRowNode_ = rowNode; |
| + } |
| + |
| + this.cursorPosition.move(row, column); |
| + |
| + while (node) { |
| + var offset = column - currentColumn; |
| + var textContent = node.textContent; |
| + if (!node.nextSibling || textContent.length > offset) { |
| + this.cursorNode_ = node; |
| + this.cursorOffset_ = offset; |
| + return; |
| + } |
| + |
| + currentColumn += textContent.length; |
| + node = node.nextSibling; |
|
dgozman
2011/11/29 11:35:42
This code assumes that each row may contain only d
rginda
2011/11/29 19:26:12
We're going to want to keep this requirement to si
|
| + } |
| +}; |
| + |
| +/** |
| + * Insert the given string at the cursor position, with the understanding that |
| + * the insert will cause the column to overflow, and the overflow will be |
| + * in a different text style than where the cursor is currently located. |
| + * |
| + * TODO: Implement this. |
| + */ |
| +hterm.Screen.prototype.spliceStringAndWrap_ = function(str) { |
| + throw 'NOT IMPLEMENTED'; |
| +}; |
| + |
| +/** |
| + * Insert a string at the current cursor position. |
| + * |
| + * If the insert causes the column to overflow, the extra text is returned. |
| + * |
| + * @return {string} Text that overflowed the column, or null if nothing |
| + * overflowed. |
| + */ |
| +hterm.Screen.prototype.insertString = function(str) { |
| + if (this.cursorPosition.column == this.columnCount_) |
| + return str; |
| + |
| + var totalRowText = this.cursorRowNode_.textContent; |
| + |
| + // There may not be underlying characters to support the current cursor |
| + // position, since they don't get inserted until they're necessary. |
| + var missingSpaceCount = Math.max(this.cursorPosition.column - |
| + totalRowText.length, |
| + 0); |
| + |
| + var overflowCount = Math.max(totalRowText.length + missingSpaceCount + |
| + str.length - this.columnCount_, |
|
klimek
2011/11/24 19:13:51
Indent.
rginda
2011/11/28 20:39:47
Done.
|
| + 0); |
| + |
| + if (overflowCount > 0 && this.cursorNode_.nextSibling) { |
| + // We're going to overflow, but there is text after the cursor with a |
| + // different set of attributes. This is going to take some effort. |
| + return this.spliceStringAndWrap_(str); |
| + } |
| + |
| + // Wrapping is simple since the cursor is located in the last block of text |
| + // on the line. |
| + |
| + var cursorNodeText = this.cursorNode_.textContent; |
| + var leadingText = cursorNodeText.substr(0, this.cursorOffset_); |
| + var trailingText = str + cursorNodeText.substr(this.cursorOffset_); |
| + var overflowText = trailingText.substr(trailingText.length - overflowCount); |
| + trailingText = trailingText.substr(0, trailingText.length - overflowCount); |
| + |
| + this.cursorNode_.textContent = ( |
| + leadingText + |
| + hterm.getWhitespace(missingSpaceCount) + |
| + trailingText |
| + ); |
| + |
| + var cursorDelta = Math.min(str.length, trailingText.length); |
| + this.cursorOffset_ += cursorDelta; |
| + this.cursorPosition.column += cursorDelta; |
| + |
| + return overflowText || null; |
| +}; |
| + |
| +/** |
| + * Overwrite the text at the current cursor position. |
| + * |
| + * If the text causes the column to overflow, the extra text is returned. |
| + * |
| + * @return {string} Text that overflowed the column, or null if nothing |
| + * overflowed. |
| + */ |
| +hterm.Screen.prototype.overwriteString = function(str) { |
| + var maxLength = this.columnCount_ - this.cursorPosition.column; |
| + if (!maxLength) |
| + return str; |
| + |
| + this.deleteChars(Math.min(str.length, maxLength)); |
| + return this.insertString(str); |
| +}; |
| + |
| +/** |
| + * Forward-delete one or more characters at the current cursor position. |
| + * |
| + * Text to the right of the deleted characters is shifted left. Only affects |
| + * characters on the same row as the cursor. |
| + * |
| + * @param {integer} count The number of characters to delete. This is clamped |
| + * to the column width minus the cursor column. |
| + */ |
| +hterm.Screen.prototype.deleteChars = function(count) { |
| + var node = this.cursorNode_; |
| + var offset = this.cursorOffset_; |
| + |
| + while (node && count) { |
| + var startLength = node.textContent.length; |
| + |
| + node.textContent = node.textContent.substr(0, offset) + |
| + node.textContent.substr(offset + count); |
| + |
| + var endLength = node.textContent.length; |
| + count -= startLength - endLength; |
| + |
| + if (endLength == 0 && node != this.cursorNode_) { |
| + var nextNode = node.nextSibling; |
| + node.parentNode.removeChild(node); |
| + node = nextNode; |
| + } else { |
| + node = node.nextSibling; |
| + } |
| + |
| + offset = 0; |
| + } |
| +}; |