From 06a058d731f387883bba6ba70d682f13fa6b0ce5 Mon Sep 17 00:00:00 2001 From: Alex Kocharin Date: Tue, 20 Mar 2012 15:14:40 -0700 Subject: [PATCH] readline: row-agnostic multiline readline implementation Fixes #2959. --- lib/readline.js | 129 ++++++++++++++++++++++++++++++++++-------------- lib/repl.js | 8 ++- lib/tty.js | 5 ++ 3 files changed, 100 insertions(+), 42 deletions(-) diff --git a/lib/readline.js b/lib/readline.js index f0504c8db5..7534f8785f 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -102,6 +102,10 @@ function Interface(input, output, completer) { process.on('SIGWINCH', function() { var winSize = output.getWindowSize(); exports.columns = winSize[0]; + + // FIXME: when #2922 will be approved, change this to + // output.on('resize', ... + self._refreshLine(); }); } } @@ -166,11 +170,8 @@ Interface.prototype._addHistory = function() { if (this.line.length === 0) return ''; this.history.unshift(this.line); - this.line = ''; this.historyIndex = -1; - this.cursor = 0; - // Only store so many if (this.history.length > kHistorySize) this.history.pop(); @@ -179,18 +180,45 @@ Interface.prototype._addHistory = function() { Interface.prototype._refreshLine = function() { + var columns = this.columns; + + // line length + var line = this._prompt + this.line; + var lineLength = line.length; + var lineCols = lineLength % columns; + var lineRows = (lineLength - lineCols) / columns; + + // cursor position + var cursorPos = this._getCursorPos(); + + // first move to the bottom of the current line, based on cursor pos + var prevRows = this.prevRows || 0; + if (prevRows > 0) { + this.output.moveCursor(0, -prevRows); + } + // Cursor to left edge. this.output.cursorTo(0); + // erase data + this.output.clearScreenDown(); // Write the prompt and the current buffer content. - this.output.write(this._prompt); - this.output.write(this.line); + this.output.write(line); - // Erase to right. - this.output.clearLine(1); + // Force terminal to allocate a new line + if (lineCols === 0) { + this.output.write(" "); + } // Move cursor to original position. - this.output.cursorTo(this._promptLength + this.cursor); + this.output.cursorTo(cursorPos.cols); + + var diff = lineRows - cursorPos.rows; + if (diff > 0) { + this.output.moveCursor(0, -diff); + } + + this.prevRows = cursorPos.rows; }; @@ -242,6 +270,9 @@ Interface.prototype._insertString = function(c) { this.line += c; this.cursor += c.length; this.output.write(c); + + // a hack to get the line refreshed if it's needed + this._moveCursor(0); } }; @@ -342,8 +373,7 @@ Interface.prototype._wordLeft = function() { if (this.cursor > 0) { var leading = this.line.slice(0, this.cursor); var match = leading.match(/([^\w\s]+|\w+|)\s*$/); - this.cursor -= match[0].length; - this._refreshLine(); + this._moveCursor(-match[0].length); } }; @@ -352,8 +382,7 @@ Interface.prototype._wordRight = function() { if (this.cursor < this.line.length) { var trailing = this.line.slice(this.cursor); var match = trailing.match(/^(\s+|\W+|\w+)\s*/); - this.cursor += match[0].length; - this._refreshLine(); + this._moveCursor(match[0].length); } }; @@ -412,9 +441,18 @@ Interface.prototype._deleteLineRight = function() { }; +Interface.prototype.clearLine = function() { + this._moveCursor(+Infinity); + this.output.write('\r\n'); + this.line = ''; + this.cursor = 0; + this.prevRows = 0; +}; + + Interface.prototype._line = function() { var line = this._addHistory(); - this.output.write('\r\n'); + this.clearLine(); this._onLine(line); }; @@ -446,6 +484,39 @@ Interface.prototype._historyPrev = function() { }; +// Returns current cursor's position and line +Interface.prototype._getCursorPos = function() { + var columns = this.columns; + var cursorPos = this.cursor + this._promptLength; + var cols = cursorPos % columns; + var rows = (cursorPos - cols) / columns; + return {cols: cols, rows: rows}; +}; + + +// This function moves cursor dx places to the right +// (-dx for left) and refreshes the line if it is needed +Interface.prototype._moveCursor = function(dx) { + var oldcursor = this.cursor; + var oldPos = this._getCursorPos(); + this.cursor += dx; + + // bounds check + if (this.cursor < 0) this.cursor = 0; + if (this.cursor > this.line.length) this.cursor = this.line.length; + + var newPos = this._getCursorPos(); + + // check if cursors are in the same line + if (oldPos.rows == newPos.rows && newPos.cols != 0) { + this.output.moveCursor(this.cursor - oldcursor, 0); + this.prevRows = newPos.rows; + } else { + this._refreshLine(); + } +}; + + // handle a write from the tty Interface.prototype._ttyWrite = function(s, key) { var next_word, next_non_word, previous_word, previous_non_word; @@ -502,27 +573,19 @@ Interface.prototype._ttyWrite = function(s, key) { break; case 'a': // go to the start of the line - this.cursor = 0; - this._refreshLine(); + this._moveCursor(-Infinity); break; case 'e': // go to the end of the line - this.cursor = this.line.length; - this._refreshLine(); + this._moveCursor(+Infinity); break; case 'b': // back one character - if (this.cursor > 0) { - this.cursor--; - this._refreshLine(); - } + this._moveCursor(-1); break; case 'f': // forward one character - if (this.cursor != this.line.length) { - this.cursor++; - this._refreshLine(); - } + this._moveCursor(+1); break; case 'n': // next history item @@ -618,27 +681,19 @@ Interface.prototype._ttyWrite = function(s, key) { break; case 'left': - if (this.cursor > 0) { - this.cursor--; - this.output.moveCursor(-1, 0); - } + this._moveCursor(-1); break; case 'right': - if (this.cursor != this.line.length) { - this.cursor++; - this.output.moveCursor(1, 0); - } + this._moveCursor(+1); break; case 'home': - this.cursor = 0; - this._refreshLine(); + this._moveCursor(-Infinity); break; case 'end': - this.cursor = this.line.length; - this._refreshLine(); + this._moveCursor(+Infinity); break; case 'up': diff --git a/lib/repl.js b/lib/repl.js index 5ca927a58f..9f2d486b21 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -138,10 +138,10 @@ function REPLServer(prompt, stream, eval, useGlobal, ignoreUndefined) { var sawSIGINT = false; rli.on('SIGINT', function() { - rli.output.write('\n'); + var empty = rli.line.length === 0; + rli.clearLine(); - if (!(self.bufferedCommand && self.bufferedCommand.length > 0) && - rli.line.length === 0) { + if (!(self.bufferedCommand && self.bufferedCommand.length > 0) && empty) { if (sawSIGINT) { rli.pause(); self.emit('exit'); @@ -154,8 +154,6 @@ function REPLServer(prompt, stream, eval, useGlobal, ignoreUndefined) { sawSIGINT = false; } - rli.line = ''; - self.bufferedCommand = ''; self.displayPrompt(); }); diff --git a/lib/tty.js b/lib/tty.js index c1bbba1b06..72fc5e57ab 100644 --- a/lib/tty.js +++ b/lib/tty.js @@ -393,6 +393,11 @@ WriteStream.prototype.clearLine = function(dir) { }; +WriteStream.prototype.clearScreenDown = function() { + this.write('\x1b[0J'); +}; + + WriteStream.prototype.getWindowSize = function() { return this._handle.getWindowSize(); };