blob: 4c5f459384c5da74ca51028fe92684c80cbf6a2e [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
rgindaa19afe22012-01-25 15:40:22 -0800441 // We may alter the length of the string by prepending some missing
442 // whitespace, so we need to record the string length ahead of time.
443 var strLength = str.length;
rginda8ba33642011-12-14 12:31:31 -0800444
rgindaa19afe22012-01-25 15:40:22 -0800445 // No matter what, before this function exits the cursor column will have
446 // moved this much.
447 this.cursorPosition.column += strLength;
rginda8ba33642011-12-14 12:31:31 -0800448
rgindaa19afe22012-01-25 15:40:22 -0800449 // Local cache of the cursor offset.
450 var offset = this.cursorOffset_;
rginda8ba33642011-12-14 12:31:31 -0800451
rgindaa19afe22012-01-25 15:40:22 -0800452 // Reverse offset is the offset measured from the end of the string.
453 // Zero implies that the cursor is at the end of the cursor node.
454 var reverseOffset = cursorNodeText.length - offset
455
456 if (reverseOffset < 0) {
457 // A negative reverse offset means the cursor is positioned past the end
458 // of the characters on this line. We'll need to insert the missing
459 // whitespace.
rgindacbbd7482012-06-13 15:06:16 -0700460 var ws = lib.f.getWhitespace(-reverseOffset);
rgindaa19afe22012-01-25 15:40:22 -0800461
462 // This whitespace should be completely unstyled. Underline and background
463 // color would be visible on whitespace, so we can't use one of those
464 // spans to hold the text.
465 if (!(this.textAttributes.underline || this.textAttributes.background)) {
466 // Best case scenario, we can just pretend the spaces were part of the
467 // original string.
468 str = ws + str;
469 } else if (cursorNode.nodeType == 3 ||
470 !(cursorNode.style.textDecoration ||
471 cursorNode.style.backgroundColor)) {
472 // Second best case, the current node is able to hold the whitespace.
473 cursorNode.textContent = (cursorNodeText += ws);
474 } else {
475 // Worst case, we have to create a new node to hold the whitespace.
476 var wsNode = cursorNode.ownerDocument.createTextNode(ws);
477 this.cursorRowNode_.insertBefore(wsNode, cursorNode.nextSibling);
478 this.cursorNode_ = cursorNode = wsNode;
479 this.cursorOffset_ = offset = -reverseOffset;
480 cursorNodeText = ws;
481 }
482
483 // We now know for sure that we're at the last character of the cursor node.
484 reverseOffset = 0;
rginda8ba33642011-12-14 12:31:31 -0800485 }
486
rgindaa19afe22012-01-25 15:40:22 -0800487 if (this.textAttributes.matchesContainer(cursorNode)) {
488 // The new text can be placed directly in the cursor node.
489 if (reverseOffset == 0) {
490 cursorNode.textContent = cursorNodeText + str;
491 } else if (offset == 0) {
492 cursorNode.textContent = str + cursorNodeText;
493 } else {
494 cursorNode.textContent = cursorNodeText.substr(0, offset) + str +
495 cursorNodeText.substr(offset);
496 }
rginda8ba33642011-12-14 12:31:31 -0800497
rgindaa19afe22012-01-25 15:40:22 -0800498 this.cursorOffset_ += strLength;
499 return;
rginda87b86462011-12-14 13:48:03 -0800500 }
501
rgindaa19afe22012-01-25 15:40:22 -0800502 // The cursor node is the wrong style for the new text. If we're at the
503 // beginning or end of the cursor node, then the adjacent node is also a
504 // potential candidate.
rginda8ba33642011-12-14 12:31:31 -0800505
rgindaa19afe22012-01-25 15:40:22 -0800506 if (offset == 0) {
507 // At the beginning of the cursor node, the check the previous sibling.
508 var previousSibling = cursorNode.previousSibling;
509 if (previousSibling &&
510 this.textAttributes.matchesContainer(previousSibling)) {
511 previousSibling.textContent += str;
512 this.cursorNode_ = previousSibling;
513 this.cursorOffset_ = previousSibling.textContent.length;
514 return;
515 }
516
517 var newNode = this.textAttributes.createContainer(str);
518 this.cursorRowNode_.insertBefore(newNode, cursorNode);
519 this.cursorNode_ = newNode;
520 this.cursorOffset_ = strLength;
521 return;
522 }
523
524 if (reverseOffset == 0) {
525 // At the end of the cursor node, the check the next sibling.
526 var nextSibling = cursorNode.nextSibling;
527 if (nextSibling &&
528 this.textAttributes.matchesContainer(nextSibling)) {
529 nextSibling.textContent = str + nextSibling.textContent;
530 this.cursorNode_ = nextSibling;
531 this.cursorOffset_ = strLength;
532 return;
533 }
534
535 var newNode = this.textAttributes.createContainer(str);
536 this.cursorRowNode_.insertBefore(newNode, nextSibling);
537 this.cursorNode_ = newNode;
538 // We specifically need to include any missing whitespace here, since it's
539 // going in a new node.
540 this.cursorOffset_ = str.length;
541 return;
542 }
543
544 // Worst case, we're somewhere in the middle of the cursor node. We'll
545 // have to split it into two nodes and insert our new container in between.
546 this.splitNode_(cursorNode, offset);
547 var newNode = this.textAttributes.createContainer(str);
548 this.cursorRowNode_.insertBefore(newNode, cursorNode.nextSibling);
549 this.cursorNode_ = newNode;
550 this.cursorOffset_ = strLength;
551};
552
553/**
rginda8ba33642011-12-14 12:31:31 -0800554 * Overwrite the text at the current cursor position.
555 *
rgindaa09e7332012-08-17 12:49:51 -0700556 *
557 * You must call maybeClipCurrentRow() after in order to clip overflowed
558 * text and clamp the cursor.
559 *
560 * It is also up to the caller to properly maintain the line overflow state
561 * using hterm.Screen..commitLineOverflow().
rginda8ba33642011-12-14 12:31:31 -0800562 */
563hterm.Screen.prototype.overwriteString = function(str) {
564 var maxLength = this.columnCount_ - this.cursorPosition.column;
565 if (!maxLength)
rgindaa19afe22012-01-25 15:40:22 -0800566 return [str];
567
568 if ((this.cursorNode_.textContent.substr(this.cursorOffset_) == str) &&
569 this.textAttributes.matchesContainer(this.cursorNode_)) {
570 // This overwrite would be a no-op, just move the cursor and return.
571 this.cursorOffset_ += str.length;
572 this.cursorPosition.column += str.length;
573 return;
574 }
rginda8ba33642011-12-14 12:31:31 -0800575
576 this.deleteChars(Math.min(str.length, maxLength));
rgindaa19afe22012-01-25 15:40:22 -0800577 this.insertString(str);
rginda8ba33642011-12-14 12:31:31 -0800578};
579
580/**
581 * Forward-delete one or more characters at the current cursor position.
582 *
583 * Text to the right of the deleted characters is shifted left. Only affects
584 * characters on the same row as the cursor.
585 *
586 * @param {integer} count The number of characters to delete. This is clamped
587 * to the column width minus the cursor column.
Robert Ginda7fd57082012-09-25 14:41:47 -0700588 * @return {integer} The number of characters actually deleted.
rginda8ba33642011-12-14 12:31:31 -0800589 */
590hterm.Screen.prototype.deleteChars = function(count) {
591 var node = this.cursorNode_;
592 var offset = this.cursorOffset_;
rginda8e92a692012-05-20 19:37:20 -0700593 var textContent = node.textContent;
rginda8ba33642011-12-14 12:31:31 -0800594
Robert Ginda7fd57082012-09-25 14:41:47 -0700595 var currentCursorColumn = this.cursorPosition.column;
596 count = Math.min(count, this.columnCount_ - currentCursorColumn);
597 if (!count)
598 return 0;
599
600 var rv = count;
rgindaa19afe22012-01-25 15:40:22 -0800601
rginda8ba33642011-12-14 12:31:31 -0800602 while (node && count) {
rginda8e92a692012-05-20 19:37:20 -0700603 var startLength = textContent.length;
rginda8ba33642011-12-14 12:31:31 -0800604
rginda8e92a692012-05-20 19:37:20 -0700605 textContent = textContent.substr(0, offset) +
606 textContent.substr(offset + count);
rginda8ba33642011-12-14 12:31:31 -0800607
rginda8e92a692012-05-20 19:37:20 -0700608 var endLength = textContent.length;
rginda8ba33642011-12-14 12:31:31 -0800609 count -= startLength - endLength;
610
611 if (endLength == 0 && node != this.cursorNode_) {
612 var nextNode = node.nextSibling;
613 node.parentNode.removeChild(node);
614 node = nextNode;
615 } else {
Robert Ginda7fd57082012-09-25 14:41:47 -0700616 node.textContent = textContent;
rginda8ba33642011-12-14 12:31:31 -0800617 node = node.nextSibling;
618 }
619
rginda8e92a692012-05-20 19:37:20 -0700620 if (node)
621 textContent = node.textContent;
Robert Ginda7fd57082012-09-25 14:41:47 -0700622
rginda8ba33642011-12-14 12:31:31 -0800623 offset = 0;
624 }
Robert Ginda7fd57082012-09-25 14:41:47 -0700625
626 return rv;
rginda8ba33642011-12-14 12:31:31 -0800627};