blob: 8268969f5937e164863113fd47a2fd72b4b3b538 [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
7lib.rtdep('lib.f',
8 '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
65 // The node containing the row that the cursor is positioned on.
66 this.cursorRowNode_ = null;
67
68 // The node containing the span of text that the cursor is positioned on.
69 this.cursorNode_ = null;
70
71 // The offset into cursorNode_ where the cursor is positioned.
72 this.cursorOffset_ = null;
73};
74
75/**
76 * Return the screen size as an hterm.Size object.
77 *
78 * @return {hterm.Size} hterm.Size object representing the current number
79 * of rows and columns in this screen.
80 */
81hterm.Screen.prototype.getSize = function() {
82 return new hterm.Size(this.columnCount_, this.rowsArray.length);
83};
84
85/**
86 * Return the current number of rows in this screen.
87 *
88 * @return {integer} The number of rows in this screen.
89 */
90hterm.Screen.prototype.getHeight = function() {
91 return this.rowsArray.length;
92};
93
94/**
95 * Return the current number of columns in this screen.
96 *
97 * @return {integer} The number of columns in this screen.
98 */
99hterm.Screen.prototype.getWidth = function() {
100 return this.columnCount_;
101};
102
103/**
104 * Set the maximum number of columns per row.
105 *
rginda8ba33642011-12-14 12:31:31 -0800106 * @param {integer} count The maximum number of columns per row.
107 */
108hterm.Screen.prototype.setColumnCount = function(count) {
rginda2312fff2012-01-05 16:20:52 -0800109 this.columnCount_ = count;
110
rgindacbbd7482012-06-13 15:06:16 -0700111 if (this.cursorPosition.column >= count)
112 this.setCursorPosition(this.cursorPosition.row, count - 1);
rginda8ba33642011-12-14 12:31:31 -0800113};
114
115/**
116 * Remove the first row from the screen and return it.
117 *
118 * @return {HTMLElement} The first row in this screen.
119 */
120hterm.Screen.prototype.shiftRow = function() {
121 return this.shiftRows(1)[0];
rginda87b86462011-12-14 13:48:03 -0800122};
rginda8ba33642011-12-14 12:31:31 -0800123
124/**
125 * Remove rows from the top of the screen and return them as an array.
126 *
127 * @param {integer} count The number of rows to remove.
128 * @return {Array.<HTMLElement>} The selected rows.
129 */
130hterm.Screen.prototype.shiftRows = function(count) {
131 return this.rowsArray.splice(0, count);
132};
133
134/**
135 * Insert a row at the top of the screen.
136 *
137 * @param {HTMLElement} The row to insert.
138 */
139hterm.Screen.prototype.unshiftRow = function(row) {
140 this.rowsArray.splice(0, 0, row);
141};
142
143/**
144 * Insert rows at the top of the screen.
145 *
146 * @param {Array.<HTMLElement>} The rows to insert.
147 */
148hterm.Screen.prototype.unshiftRows = function(rows) {
149 this.rowsArray.unshift.apply(this.rowsArray, rows);
150};
151
152/**
153 * Remove the last row from the screen and return it.
154 *
155 * @return {HTMLElement} The last row in this screen.
156 */
157hterm.Screen.prototype.popRow = function() {
158 return this.popRows(1)[0];
159};
160
161/**
162 * Remove rows from the bottom of the screen and return them as an array.
163 *
164 * @param {integer} count The number of rows to remove.
165 * @return {Array.<HTMLElement>} The selected rows.
166 */
167hterm.Screen.prototype.popRows = function(count) {
168 return this.rowsArray.splice(this.rowsArray.length - count, count);
169};
170
171/**
172 * Insert a row at the bottom of the screen.
173 *
174 * @param {HTMLElement} The row to insert.
175 */
176hterm.Screen.prototype.pushRow = function(row) {
177 this.rowsArray.push(row);
178};
179
180/**
181 * Insert rows at the bottom of the screen.
182 *
183 * @param {Array.<HTMLElement>} The rows to insert.
184 */
185hterm.Screen.prototype.pushRows = function(rows) {
186 rows.push.apply(this.rowsArray, rows);
187};
188
189/**
190 * Insert a row at the specified column of the screen.
191 *
192 * @param {HTMLElement} The row to insert.
193 */
194hterm.Screen.prototype.insertRow = function(index, row) {
195 this.rowsArray.splice(index, 0, row);
196};
197
198/**
199 * Insert rows at the specified column of the screen.
200 *
201 * @param {Array.<HTMLElement>} The rows to insert.
202 */
203hterm.Screen.prototype.insertRows = function(index, rows) {
204 for (var i = 0; i < rows.length; i++) {
205 this.rowsArray.splice(index + i, 0, rows[i]);
206 }
207};
208
209/**
210 * Remove a last row from the specified column of the screen and return it.
211 *
212 * @return {HTMLElement} The selected row.
213 */
214hterm.Screen.prototype.removeRow = function(index) {
215 return this.rowsArray.splice(index, 1)[0];
216};
217
218/**
219 * Remove rows from the bottom of the screen and return them as an array.
220 *
221 * @param {integer} count The number of rows to remove.
222 * @return {Array.<HTMLElement>} The selected rows.
223 */
224hterm.Screen.prototype.removeRows = function(index, count) {
225 return this.rowsArray.splice(index, count);
226};
227
228/**
229 * Invalidate the current cursor position.
230 *
rginda87b86462011-12-14 13:48:03 -0800231 * This sets this.cursorPosition to (0, 0) and clears out some internal
rginda8ba33642011-12-14 12:31:31 -0800232 * data.
233 *
234 * Attempting to insert or overwrite text while the cursor position is invalid
235 * will raise an obscure exception.
236 */
237hterm.Screen.prototype.invalidateCursorPosition = function() {
rginda87b86462011-12-14 13:48:03 -0800238 this.cursorPosition.move(0, 0);
rginda8ba33642011-12-14 12:31:31 -0800239 this.cursorRowNode_ = null;
240 this.cursorNode_ = null;
241 this.cursorOffset_ = null;
242};
243
244/**
rginda8ba33642011-12-14 12:31:31 -0800245 * Clear the contents of the cursor row.
rginda8ba33642011-12-14 12:31:31 -0800246 */
247hterm.Screen.prototype.clearCursorRow = function() {
248 this.cursorRowNode_.innerHTML = '';
rgindaa09e7332012-08-17 12:49:51 -0700249 this.cursorRowNode_.removeAttribute('line-overflow');
rginda8ba33642011-12-14 12:31:31 -0800250 this.cursorOffset_ = 0;
rginda8ba33642011-12-14 12:31:31 -0800251 this.cursorPosition.column = 0;
rginda2312fff2012-01-05 16:20:52 -0800252 this.cursorPosition.overflow = false;
Robert Ginda7fd57082012-09-25 14:41:47 -0700253
254 var text;
255 if (this.textAttributes.isDefault()) {
256 text = '';
257 } else {
258 text = lib.f.getWhitespace(this.columnCount_);
259 }
260
261 var node = this.textAttributes.createContainer(text);
262 this.cursorRowNode_.appendChild(node);
263 this.cursorNode_ = node;
rginda8ba33642011-12-14 12:31:31 -0800264};
265
266/**
rgindaa09e7332012-08-17 12:49:51 -0700267 * Mark the current row as having overflowed to the next line.
268 *
269 * The line overflow state is used when converting a range of rows into text.
270 * It makes it possible to recombine two or more overflow terminal rows into
271 * a single line.
272 *
273 * This is distinct from the cursor being in the overflow state. Cursor
274 * overflow indicates that printing at the cursor position will commit a
275 * line overflow, unless it is preceded by a repositioning of the cursor
276 * to a non-overflow state.
277 */
278hterm.Screen.prototype.commitLineOverflow = function() {
279 this.cursorRowNode_.setAttribute('line-overflow', true);
280};
281
282/**
rginda8ba33642011-12-14 12:31:31 -0800283 * Relocate the cursor to a give row and column.
284 *
285 * @param {integer} row The zero based row.
286 * @param {integer} column The zero based column.
287 */
288hterm.Screen.prototype.setCursorPosition = function(row, column) {
rginda11057d52012-04-25 12:29:56 -0700289 if (!this.rowsArray.length) {
290 console.warn('Attempt to set cursor position on empty screen.');
291 return;
292 }
293
rginda87b86462011-12-14 13:48:03 -0800294 if (row >= this.rowsArray.length) {
rgindacbbd7482012-06-13 15:06:16 -0700295 console.error('Row out of bounds: ' + row);
rginda87b86462011-12-14 13:48:03 -0800296 row = this.rowsArray.length - 1;
297 } else if (row < 0) {
rgindacbbd7482012-06-13 15:06:16 -0700298 console.error('Row out of bounds: ' + row);
rginda87b86462011-12-14 13:48:03 -0800299 row = 0;
300 }
301
302 if (column >= this.columnCount_) {
rgindacbbd7482012-06-13 15:06:16 -0700303 console.error('Column out of bounds: ' + column);
rginda87b86462011-12-14 13:48:03 -0800304 column = this.columnCount_ - 1;
305 } else if (column < 0) {
rgindacbbd7482012-06-13 15:06:16 -0700306 console.error('Column out of bounds: ' + column);
rginda87b86462011-12-14 13:48:03 -0800307 column = 0;
308 }
rginda8ba33642011-12-14 12:31:31 -0800309
rginda2312fff2012-01-05 16:20:52 -0800310 this.cursorPosition.overflow = false;
311
rginda8ba33642011-12-14 12:31:31 -0800312 var rowNode = this.rowsArray[row];
313 var node = rowNode.firstChild;
314
315 if (!node) {
316 node = rowNode.ownerDocument.createTextNode('');
317 rowNode.appendChild(node);
318 }
319
rgindaa19afe22012-01-25 15:40:22 -0800320 var currentColumn = 0;
321
rginda8ba33642011-12-14 12:31:31 -0800322 if (rowNode == this.cursorRowNode_) {
323 if (column >= this.cursorPosition.column - this.cursorOffset_) {
324 node = this.cursorNode_;
325 currentColumn = this.cursorPosition.column - this.cursorOffset_;
326 }
327 } else {
328 this.cursorRowNode_ = rowNode;
329 }
330
331 this.cursorPosition.move(row, column);
332
333 while (node) {
334 var offset = column - currentColumn;
335 var textContent = node.textContent;
336 if (!node.nextSibling || textContent.length > offset) {
337 this.cursorNode_ = node;
338 this.cursorOffset_ = offset;
339 return;
340 }
341
342 currentColumn += textContent.length;
343 node = node.nextSibling;
344 }
345};
346
347/**
rginda87b86462011-12-14 13:48:03 -0800348 * Set the provided selection object to be a caret selection at the current
349 * cursor position.
350 */
351hterm.Screen.prototype.syncSelectionCaret = function(selection) {
352 selection.collapse(this.cursorNode_, this.cursorOffset_);
353};
354
355/**
rgindaa19afe22012-01-25 15:40:22 -0800356 * Split a single node into two nodes at the given offset.
rginda8ba33642011-12-14 12:31:31 -0800357 *
rgindaa19afe22012-01-25 15:40:22 -0800358 * For example:
359 * Given the DOM fragment '<div><span>Hello World</span></div>', call splitNode_
360 * passing the span and an offset of 6. This would modifiy the fragment to
361 * become: '<div><span>Hello </span><span>World</span></div>'. If the span
362 * had any attributes they would have been copied to the new span as well.
363 *
364 * The to-be-split node must have a container, so that the new node can be
365 * placed next to it.
366 *
367 * @param {HTMLNode} node The node to split.
368 * @param {integer} offset The offset into the node where the split should
369 * occur.
rginda8ba33642011-12-14 12:31:31 -0800370 */
rgindaa19afe22012-01-25 15:40:22 -0800371hterm.Screen.prototype.splitNode_ = function(node, offset) {
rginda35c456b2012-02-09 17:29:05 -0800372 var afterNode = node.cloneNode(false);
rgindaa19afe22012-01-25 15:40:22 -0800373
rginda35c456b2012-02-09 17:29:05 -0800374 var textContent = node.textContent;
375 node.textContent = textContent.substr(0, offset);
376 afterNode.textContent = textContent.substr(offset);
rgindaa19afe22012-01-25 15:40:22 -0800377
378 node.parentNode.insertBefore(afterNode, node.nextSibling);
rginda8ba33642011-12-14 12:31:31 -0800379};
380
381/**
rgindaa9abdd82012-08-06 18:05:09 -0700382 * Ensure that text is clipped and the cursor is clamped to the column count.
rgindaa19afe22012-01-25 15:40:22 -0800383 */
rgindaa9abdd82012-08-06 18:05:09 -0700384hterm.Screen.prototype.maybeClipCurrentRow = function() {
385 if (this.cursorRowNode_.textContent.length <= this.columnCount_) {
386 // Current row does not need clipping, but may need clamping.
387 if (this.cursorPosition.column >= this.columnCount_) {
388 this.setCursorPosition(this.cursorPosition.row, this.columnCount_ - 1);
389 this.cursorPosition.overflow = true;
390 }
rgindaa19afe22012-01-25 15:40:22 -0800391
rgindaa9abdd82012-08-06 18:05:09 -0700392 return;
393 }
394
395 // Save off the current column so we can maybe restore it later.
396 var currentColumn = this.cursorPosition.column;
397
398 // Move the cursor to the final column.
399 this.setCursorPosition(this.cursorPosition.row, this.columnCount_ - 1);
400
401 // Remove any text that partially overflows.
402 var cursorNodeText = this.cursorNode_.textContent;
403 if (this.cursorOffset_ < cursorNodeText.length - 1) {
404 this.cursorNode_.textContent = cursorNodeText.substr(
405 0, this.cursorOffset_ + 1);
406 }
407
408 // Remove all nodes after the cursor.
rgindaa19afe22012-01-25 15:40:22 -0800409 var rowNode = this.cursorRowNode_;
410 var node = this.cursorNode_.nextSibling;
411
412 while (node) {
rgindaa19afe22012-01-25 15:40:22 -0800413 rowNode.removeChild(node);
414 node = this.cursorNode_.nextSibling;
415 }
416
Robert Ginda7fd57082012-09-25 14:41:47 -0700417 if (currentColumn < this.columnCount_) {
rgindaa9abdd82012-08-06 18:05:09 -0700418 // If the cursor was within the screen before we started then restore its
419 // position.
rgindaa19afe22012-01-25 15:40:22 -0800420 this.setCursorPosition(this.cursorPosition.row, currentColumn);
rgindaa9abdd82012-08-06 18:05:09 -0700421 } else {
422 // Otherwise leave it at the the last column in the overflow state.
423 this.cursorPosition.overflow = true;
rgindaa19afe22012-01-25 15:40:22 -0800424 }
rgindaa19afe22012-01-25 15:40:22 -0800425};
426
427/**
428 * Insert a string at the current character position using the current
429 * text attributes.
430 *
rgindaa09e7332012-08-17 12:49:51 -0700431 * You must call maybeClipCurrentRow() after in order to clip overflowed
432 * text and clamp the cursor.
433 *
434 * It is also up to the caller to properly maintain the line overflow state
435 * using hterm.Screen..commitLineOverflow().
rginda8ba33642011-12-14 12:31:31 -0800436 */
437hterm.Screen.prototype.insertString = function(str) {
rgindaa19afe22012-01-25 15:40:22 -0800438 var cursorNode = this.cursorNode_;
439 var cursorNodeText = cursorNode.textContent;
rginda8ba33642011-12-14 12:31:31 -0800440
Robert Gindaa21dfb32013-10-31 14:17:45 -0700441 this.cursorRowNode_.removeAttribute('line-overflow');
442
rgindaa19afe22012-01-25 15:40:22 -0800443 // We may alter the length of the string by prepending some missing
444 // whitespace, so we need to record the string length ahead of time.
445 var strLength = str.length;
rginda8ba33642011-12-14 12:31:31 -0800446
rgindaa19afe22012-01-25 15:40:22 -0800447 // No matter what, before this function exits the cursor column will have
448 // moved this much.
449 this.cursorPosition.column += strLength;
rginda8ba33642011-12-14 12:31:31 -0800450
rgindaa19afe22012-01-25 15:40:22 -0800451 // Local cache of the cursor offset.
452 var offset = this.cursorOffset_;
rginda8ba33642011-12-14 12:31:31 -0800453
rgindaa19afe22012-01-25 15:40:22 -0800454 // Reverse offset is the offset measured from the end of the string.
455 // Zero implies that the cursor is at the end of the cursor node.
456 var reverseOffset = cursorNodeText.length - offset
457
458 if (reverseOffset < 0) {
459 // A negative reverse offset means the cursor is positioned past the end
460 // of the characters on this line. We'll need to insert the missing
461 // whitespace.
rgindacbbd7482012-06-13 15:06:16 -0700462 var ws = lib.f.getWhitespace(-reverseOffset);
rgindaa19afe22012-01-25 15:40:22 -0800463
464 // This whitespace should be completely unstyled. Underline and background
465 // color would be visible on whitespace, so we can't use one of those
466 // spans to hold the text.
467 if (!(this.textAttributes.underline || this.textAttributes.background)) {
468 // Best case scenario, we can just pretend the spaces were part of the
469 // original string.
470 str = ws + str;
471 } else if (cursorNode.nodeType == 3 ||
472 !(cursorNode.style.textDecoration ||
473 cursorNode.style.backgroundColor)) {
474 // Second best case, the current node is able to hold the whitespace.
475 cursorNode.textContent = (cursorNodeText += ws);
476 } else {
477 // Worst case, we have to create a new node to hold the whitespace.
478 var wsNode = cursorNode.ownerDocument.createTextNode(ws);
479 this.cursorRowNode_.insertBefore(wsNode, cursorNode.nextSibling);
480 this.cursorNode_ = cursorNode = wsNode;
481 this.cursorOffset_ = offset = -reverseOffset;
482 cursorNodeText = ws;
483 }
484
485 // We now know for sure that we're at the last character of the cursor node.
486 reverseOffset = 0;
rginda8ba33642011-12-14 12:31:31 -0800487 }
488
rgindaa19afe22012-01-25 15:40:22 -0800489 if (this.textAttributes.matchesContainer(cursorNode)) {
490 // The new text can be placed directly in the cursor node.
491 if (reverseOffset == 0) {
492 cursorNode.textContent = cursorNodeText + str;
493 } else if (offset == 0) {
494 cursorNode.textContent = str + cursorNodeText;
495 } else {
496 cursorNode.textContent = cursorNodeText.substr(0, offset) + str +
497 cursorNodeText.substr(offset);
498 }
rginda8ba33642011-12-14 12:31:31 -0800499
rgindaa19afe22012-01-25 15:40:22 -0800500 this.cursorOffset_ += strLength;
501 return;
rginda87b86462011-12-14 13:48:03 -0800502 }
503
rgindaa19afe22012-01-25 15:40:22 -0800504 // The cursor node is the wrong style for the new text. If we're at the
505 // beginning or end of the cursor node, then the adjacent node is also a
506 // potential candidate.
rginda8ba33642011-12-14 12:31:31 -0800507
rgindaa19afe22012-01-25 15:40:22 -0800508 if (offset == 0) {
509 // At the beginning of the cursor node, the check the previous sibling.
510 var previousSibling = cursorNode.previousSibling;
511 if (previousSibling &&
512 this.textAttributes.matchesContainer(previousSibling)) {
513 previousSibling.textContent += str;
514 this.cursorNode_ = previousSibling;
515 this.cursorOffset_ = previousSibling.textContent.length;
516 return;
517 }
518
519 var newNode = this.textAttributes.createContainer(str);
520 this.cursorRowNode_.insertBefore(newNode, cursorNode);
521 this.cursorNode_ = newNode;
522 this.cursorOffset_ = strLength;
523 return;
524 }
525
526 if (reverseOffset == 0) {
527 // At the end of the cursor node, the check the next sibling.
528 var nextSibling = cursorNode.nextSibling;
529 if (nextSibling &&
530 this.textAttributes.matchesContainer(nextSibling)) {
531 nextSibling.textContent = str + nextSibling.textContent;
532 this.cursorNode_ = nextSibling;
533 this.cursorOffset_ = strLength;
534 return;
535 }
536
537 var newNode = this.textAttributes.createContainer(str);
538 this.cursorRowNode_.insertBefore(newNode, nextSibling);
539 this.cursorNode_ = newNode;
540 // We specifically need to include any missing whitespace here, since it's
541 // going in a new node.
542 this.cursorOffset_ = str.length;
543 return;
544 }
545
546 // Worst case, we're somewhere in the middle of the cursor node. We'll
547 // have to split it into two nodes and insert our new container in between.
548 this.splitNode_(cursorNode, offset);
549 var newNode = this.textAttributes.createContainer(str);
550 this.cursorRowNode_.insertBefore(newNode, cursorNode.nextSibling);
551 this.cursorNode_ = newNode;
552 this.cursorOffset_ = strLength;
553};
554
555/**
rginda8ba33642011-12-14 12:31:31 -0800556 * Overwrite the text at the current cursor position.
557 *
rgindaa09e7332012-08-17 12:49:51 -0700558 * You must call maybeClipCurrentRow() after in order to clip overflowed
559 * text and clamp the cursor.
560 *
561 * It is also up to the caller to properly maintain the line overflow state
562 * using hterm.Screen..commitLineOverflow().
rginda8ba33642011-12-14 12:31:31 -0800563 */
564hterm.Screen.prototype.overwriteString = function(str) {
565 var maxLength = this.columnCount_ - this.cursorPosition.column;
566 if (!maxLength)
rgindaa19afe22012-01-25 15:40:22 -0800567 return [str];
568
569 if ((this.cursorNode_.textContent.substr(this.cursorOffset_) == str) &&
570 this.textAttributes.matchesContainer(this.cursorNode_)) {
571 // This overwrite would be a no-op, just move the cursor and return.
572 this.cursorOffset_ += str.length;
573 this.cursorPosition.column += str.length;
574 return;
575 }
rginda8ba33642011-12-14 12:31:31 -0800576
577 this.deleteChars(Math.min(str.length, maxLength));
rgindaa19afe22012-01-25 15:40:22 -0800578 this.insertString(str);
rginda8ba33642011-12-14 12:31:31 -0800579};
580
581/**
582 * Forward-delete one or more characters at the current cursor position.
583 *
584 * Text to the right of the deleted characters is shifted left. Only affects
585 * characters on the same row as the cursor.
586 *
587 * @param {integer} count The number of characters to delete. This is clamped
588 * to the column width minus the cursor column.
Robert Ginda7fd57082012-09-25 14:41:47 -0700589 * @return {integer} The number of characters actually deleted.
rginda8ba33642011-12-14 12:31:31 -0800590 */
591hterm.Screen.prototype.deleteChars = function(count) {
592 var node = this.cursorNode_;
593 var offset = this.cursorOffset_;
rginda8e92a692012-05-20 19:37:20 -0700594 var textContent = node.textContent;
rginda8ba33642011-12-14 12:31:31 -0800595
Robert Ginda7fd57082012-09-25 14:41:47 -0700596 var currentCursorColumn = this.cursorPosition.column;
597 count = Math.min(count, this.columnCount_ - currentCursorColumn);
598 if (!count)
599 return 0;
600
601 var rv = count;
rgindaa19afe22012-01-25 15:40:22 -0800602
rginda8ba33642011-12-14 12:31:31 -0800603 while (node && count) {
rginda8e92a692012-05-20 19:37:20 -0700604 var startLength = textContent.length;
rginda8ba33642011-12-14 12:31:31 -0800605
rginda8e92a692012-05-20 19:37:20 -0700606 textContent = textContent.substr(0, offset) +
607 textContent.substr(offset + count);
rginda8ba33642011-12-14 12:31:31 -0800608
rginda8e92a692012-05-20 19:37:20 -0700609 var endLength = textContent.length;
rginda8ba33642011-12-14 12:31:31 -0800610 count -= startLength - endLength;
611
612 if (endLength == 0 && node != this.cursorNode_) {
613 var nextNode = node.nextSibling;
614 node.parentNode.removeChild(node);
615 node = nextNode;
616 } else {
Robert Ginda7fd57082012-09-25 14:41:47 -0700617 node.textContent = textContent;
rginda8ba33642011-12-14 12:31:31 -0800618 node = node.nextSibling;
619 }
620
rginda8e92a692012-05-20 19:37:20 -0700621 if (node)
622 textContent = node.textContent;
Robert Ginda7fd57082012-09-25 14:41:47 -0700623
rginda8ba33642011-12-14 12:31:31 -0800624 offset = 0;
625 }
Robert Ginda7fd57082012-09-25 14:41:47 -0700626
627 return rv;
rginda8ba33642011-12-14 12:31:31 -0800628};
John Macinnesfb683832013-07-22 14:46:30 -0400629
630/**
631 * Finds first X-ROW of a line containing specified X-ROW.
632 * Used to support line overflow.
633 *
634 * @param {Node} row X-ROW to begin search for first row of line.
635 * @return {Node} The X-ROW that is at the beginning of the line.
636 **/
637hterm.Screen.prototype.getLineStartRow_ = function(row) {
638 while (row.previousSibling &&
639 row.previousSibling.hasAttribute('line-overflow')) {
640 row = row.previousSibling;
641 }
642 return row;
643};
644
645/**
646 * Gets text of a line beginning with row.
647 * Supports line overflow.
648 *
649 * @param {Node} row First X-ROW of line.
650 * @return {string} Text content of line.
651 **/
652hterm.Screen.prototype.getLineText_ = function(row) {
653 var rowText = "";
654 while (row) {
655 rowText += row.textContent;
656 if (row.hasAttribute('line-overflow')) {
657 row = row.nextSibling;
658 } else {
659 break;
660 }
661 }
662 return rowText;
663};
664
665/**
666 * Returns X-ROW that is ancestor of the node.
667 *
668 * @param {Node} node Node to get X-ROW ancestor for.
669 * @return {Node} X-ROW ancestor of node, or null if not found.
670 **/
671hterm.Screen.prototype.getXRowAncestor_ = function(node) {
672 while (node) {
673 if (node.nodeName === 'X-ROW')
674 break;
675 node = node.parentNode;
676 }
677 return node;
678};
679
680/**
681 * Returns position within line of character at offset within node.
682 * Supports line overflow.
683 *
684 * @param {Node} row X-ROW at beginning of line.
685 * @param {Node} node Node to get position of.
686 * @param {integer} offset Offset into node.
687 *
688 * @return {integer} Position within line of character at offset within node.
689 **/
690hterm.Screen.prototype.getPositionWithOverflow_ = function(row, node, offset) {
691 if (!node)
692 return -1;
693 var ancestorRow = this.getXRowAncestor_(node);
694 if (!ancestorRow)
695 return -1;
696 var position = 0;
697 while (ancestorRow != row) {
698 position += row.textContent.length;
699 if (row.hasAttribute('line-overflow') && row.nextSibling) {
700 row = row.nextSibling;
701 } else {
702 return -1;
703 }
704 }
705 return position + this.getPositionWithinRow_(row, node, offset);
706};
707
708/**
709 * Returns position within row of character at offset within node.
710 * Does not support line overflow.
711 *
712 * @param {Node} row X-ROW to get position within.
713 * @param {Node} node Node to get position for.
714 * @param {integer} offset Offset within node to get position for.
715 * @return {integer} Position within row of character at offset within node.
716 **/
717hterm.Screen.prototype.getPositionWithinRow_ = function(row, node, offset) {
718 if (node.parentNode != row) {
719 return this.getPositionWithinRow_(node.parentNode, node, offset) +
720 this.getPositionWithinRow_(row, node.parentNode, 0);
721 }
722 var position = 0;
723 for (var i = 0; i < row.childNodes.length; i++) {
724 var currentNode = row.childNodes[i];
725 if (currentNode == node)
726 return position + offset;
727 position += currentNode.textContent.length;
728 }
729 return -1;
730};
731
732/**
733 * Returns the node and offset corresponding to position within line.
734 * Supports line overflow.
735 *
736 * @param {Node} row X-ROW at beginning of line.
737 * @param {integer} position Position within line to retrieve node and offset.
738 * @return {Array} Two element array containing node and offset respectively.
739 **/
740hterm.Screen.prototype.getNodeAndOffsetWithOverflow_ = function(row, position) {
741 while (row && position > row.textContent.length) {
742 if (row.hasAttribute('line-overflow') && row.nextSibling) {
743 position -= row.textContent.length;
744 row = row.nextSibling;
745 } else {
746 return -1;
747 }
748 }
749 return this.getNodeAndOffsetWithinRow_(row, position);
750};
751
752/**
753 * Returns the node and offset corresponding to position within row.
754 * Does not support line overflow.
755 *
756 * @param {Node} row X-ROW to get position within.
757 * @param {integer} position Position within row to retrieve node and offset.
758 * @return {Array} Two element array containing node and offset respectively.
759 **/
760hterm.Screen.prototype.getNodeAndOffsetWithinRow_ = function(row, position) {
761 for (var i = 0; i < row.childNodes.length; i++) {
762 var node = row.childNodes[i];
763 if (position <= node.textContent.length) {
764 if (node.nodeName === 'SPAN') {
765 /** Drill down to node contained by SPAN. **/
766 return this.getNodeAndOffsetWithinRow_(node, position);
767 } else {
768 return [node, position];
769 }
770 }
771 position -= node.textContent.length;
772 }
773 return null;
774};
775
776/**
777 * Returns the node and offset corresponding to position within line.
778 * Supports line overflow.
779 *
780 * @param {Node} row X-ROW at beginning of line.
781 * @param {integer} start Start position of range within line.
782 * @param {integer} end End position of range within line.
783 * @param {Range} range Range to modify.
784 **/
785hterm.Screen.prototype.setRange_ = function(row, start, end, range) {
786 var startNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, start);
787 if (startNodeAndOffset == null)
788 return;
789 var endNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, end);
790 if (endNodeAndOffset == null)
791 return;
792 range.setStart(startNodeAndOffset[0], startNodeAndOffset[1]);
793 range.setEnd(endNodeAndOffset[0], endNodeAndOffset[1]);
794};
795
796/**
797 * Expands selection to surround URLs.
798 *
799 * Uses this regular expression to expand the selection:
800 * [^\s\[\](){}<>"'\^!@#$%&*,.;:~`]
801 * [^\s\[\](){}<>"'\^]*
802 * [^\s\[\](){}<>"'\^!@#$%&*,.;:~`]
803 *
804 * @param {Selection} selection Selection to expand.
805 **/
806hterm.Screen.prototype.expandSelection = function(selection) {
807 if (!selection)
808 return;
809
810 var range = selection.getRangeAt(0);
811 if (!range || range.toString().match(/\s/))
812 return;
813
814 var row = this.getLineStartRow_(this.getXRowAncestor_(range.startContainer));
815 if (!row)
816 return;
817
818 var startPosition = this.getPositionWithOverflow_(row,
819 range.startContainer,
820 range.startOffset);
821 if (startPosition == -1)
822 return;
823 var endPosition = this.getPositionWithOverflow_(row,
824 range.endContainer,
825 range.endOffset);
826 if (endPosition == -1)
827 return;
828
829 var outsideMatch = '[^\\s\\[\\](){}<>"\'\\^!@#$%&*,.;:~`]';
830 var insideMatch = '[^\\s\\[\\](){}<>"\'\\^]*';
831
832 //Move start to the left.
833 var rowText = this.getLineText_(row);
834 var lineUpToRange = rowText.substring(0, endPosition);
835 var leftRegularExpression = new RegExp(outsideMatch + insideMatch + "$");
836 var expandedStart = lineUpToRange.search(leftRegularExpression);
837 if (expandedStart == -1 || expandedStart > startPosition)
838 return;
839
840 //Move end to the right.
841 var lineFromRange = rowText.substring(startPosition, rowText.length);
842 var rightRegularExpression = new RegExp("^" + insideMatch + outsideMatch);
843 var found = lineFromRange.match(rightRegularExpression);
844 if (!found)
845 return;
846 var expandedEnd = startPosition + found[0].length;
847 if (expandedEnd == -1 || expandedEnd < endPosition)
848 return;
849
850 this.setRange_(row, expandedStart, expandedEnd, range);
851 selection.addRange(range);
852};