readline: row-agnostic multiline readline implementation
authorAlex Kocharin <alex@kocharin.ru>
Tue, 20 Mar 2012 22:14:40 +0000 (15:14 -0700)
committerNathan Rajlich <nathan@tootallnate.net>
Tue, 20 Mar 2012 22:37:06 +0000 (15:37 -0700)
Fixes #2959.

lib/readline.js
lib/repl.js
lib/tty.js

index f0504c8db5fb5a5bfc219ca418a356789f26a7ef..7534f8785fa07562307663dc63631ec906c7f308 100644 (file)
@@ -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':
index 5ca927a58fb106bec235219d7651cb1b30fe1f3d..9f2d486b21caca7011b3ab60e25755dfa6222a84 100644 (file)
@@ -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();
   });
index c1bbba1b06a15353492385c964a525da84156a9b..72fc5e57ab1086e23545c8d36da0db1a73ec9648 100644 (file)
@@ -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();
 };