terminal: implement addBindings() and others for nasftp with xterm.js

Bug: b/295745056
Change-Id: Ie32f2c6b057f62f60edbb768cd7cc56bde1a0934
Reviewed-on: https://chromium-review.googlesource.com/c/apps/libapps/+/4782290
Reviewed-by: Joel Hockey <joelhockey@chromium.org>
Tested-by: kokoro <noreply+kokoro@google.com>
diff --git a/terminal/js/terminal_emulator.js b/terminal/js/terminal_emulator.js
index 8b53cfc..9e0b63c 100644
--- a/terminal/js/terminal_emulator.js
+++ b/terminal/js/terminal_emulator.js
@@ -524,6 +524,12 @@
     });
     this.term.onBell(() => this.ringBell());
 
+    // This is for supporting `this.keyboard.bindings.addBindings()`, which is
+    // used by nasftp_cli.js.
+    this.htermKeyBindings_ = new hterm.Keyboard.Bindings();
+    this.keyboard = {
+      bindings: this.htermKeyBindings_,
+    };
     /**
      * A mapping from key combo (see encodeKeyCombo()) to a handler function.
      *
@@ -614,13 +620,43 @@
   }
 
   /** @override */
+  clearHome() {
+    for (let i = 0; i < this.term.rows; ++i) {
+      this.xtermInternal_.eraseInBufferLine(i, 0, this.term.cols);
+    }
+    this.xtermInternal_.setCursor(0, 0);
+  }
+
+  /** @override */
+  eraseToLeft() {
+    const {cursorX, cursorY} = this.term.buffer.active;
+    this.xtermInternal_.eraseInBufferLine(cursorY, 0, cursorX + 1);
+  }
+
+  /** @override */
+  eraseLine() {
+    const {cursorY} = this.term.buffer.active;
+    this.xtermInternal_.eraseInBufferLine(cursorY, 0, this.term.cols);
+  }
+
+  /** @override */
+  setCursorPosition(row, column) {
+    this.xtermInternal_.setCursor(column, row);
+  }
+
+  /** @override */
+  setCursorColumn(column) {
+    this.xtermInternal_.setCursor(column, this.term.buffer.active.cursorY);
+  }
+
+  /** @override */
   newLine() {
     this.xtermInternal_.newLine();
   }
 
   /** @override */
   cursorLeft(number) {
-    this.xtermInternal_.cursorLeft(number ?? 1);
+    this.xtermInternal_.moveCursor(-(number ?? 1), 0);
   }
 
   /** @override */
@@ -656,24 +692,7 @@
    * still runs.
    */
   installUnimplementedStubs_() {
-    this.keyboard = {
-      keyMap: {
-        keyDefs: [],
-      },
-      bindings: {
-        clear: () => {},
-        addBinding: () => {},
-        addBindings: () => {},
-        OsDefaults: {},
-      },
-    };
-    this.keyboard.keyMap.keyDefs[78] = {};
-    this.keyboard.keyMap.keyDefs[84] = {};
-
     const methodNames = [
-        'eraseLine',
-        'setCursorColumn',
-        'setCursorPosition',
         'setCursorVisible',
         'uninstallKeyboard',
     ];
@@ -1272,6 +1291,10 @@
       return true;
     }
 
+    if (this.handleHtermKeyBindings_(ev)) {
+      return true;
+    }
+
     const handler = this.keyDownHandlers_.get(
         encodeKeyCombo(modifiers, ev.keyCode));
     if (handler) {
@@ -1285,6 +1308,57 @@
   }
 
   /**
+   * @param {!KeyboardEvent} ev
+   * @return {boolean} Return true if the key event is handled.
+   */
+  handleHtermKeyBindings_(ev) {
+    // The logic here is a simplified version of
+    // hterm.Keyboard.prototype.onKeyDown_;
+    const htermKeyDown = {
+      keyCode: ev.keyCode,
+      shift: ev.shiftKey,
+      ctrl: ev.ctrlKey,
+      alt: ev.altKey,
+      meta: ev.metaKey,
+    };
+    const htermBinding = this.htermKeyBindings_.getBinding(htermKeyDown);
+
+    if (!htermBinding) {
+      return false;
+    }
+
+    // If there is a handler but the event is not keydown (e.g. keypress,
+    // keyup), we just do nothing.
+    if (ev.type !== 'keydown') {
+      ev.preventDefault();
+      ev.stopPropagation();
+      return true;
+    }
+
+    let action;
+    if (typeof htermBinding.action === 'function') {
+      action = htermBinding.action.call(this.keyboard, this, htermKeyDown);
+    } else {
+      action = htermBinding.action;
+    }
+
+    const KeyActions = hterm.Keyboard.KeyActions;
+    switch (action) {
+      case KeyActions.DEFAULT:
+        return false;
+      case KeyActions.PASS:
+        return true;
+      default:
+        console.warn(`KeyAction ${action} is not supported`);
+        // Fall through.
+      case KeyActions.CANCEL:
+        ev.preventDefault();
+        ev.stopPropagation();
+        return true;
+    }
+  }
+
+  /**
    * Handle arrow keys and the "six pack keys" (e.g. home, insert...) because
    * xterm.js does not always handle them correctly with modifier keys.
    *
diff --git a/terminal/js/terminal_xterm_internal.js b/terminal/js/terminal_xterm_internal.js
index 8842868..77aadd3 100644
--- a/terminal/js/terminal_xterm_internal.js
+++ b/terminal/js/terminal_xterm_internal.js
@@ -126,7 +126,10 @@
         _inputHandler: {
           nextLine: function(),
           print: function(!Uint32Array, number, number),
+          _eraseInBufferLine: function(number, number, number, boolean,
+            boolean),
           _moveCursor: function(number, number),
+          _setCursor: function(number, number),
           _parser: {
             registerDcsHandler: function(!Object, !TmuxDcsPHandler),
             _transitions: {
@@ -186,10 +189,35 @@
   }
 
   /**
-   * @param {number} number
+   * Move the cursor relative to the current position.
+   *
+   * @param {number} x Can be negative.
+   * @param {number} y Can be negative.
    */
-  cursorLeft(number) {
-    this.core_._inputHandler._moveCursor(-number, 0);
+  moveCursor(x, y) {
+    this.core_._inputHandler._moveCursor(x, y);
+    this.scheduleFullRefresh_();
+  }
+
+  /**
+   * @param {number} y The row number
+   * @param {number} start The starting column
+   * @param {number} end The ending column (not inclusive)
+   */
+  eraseInBufferLine(y, start, end) {
+    this.core_._inputHandler._eraseInBufferLine(y, start, end,
+        /* clearWrap= */false, /* respectProtect= */false);
+    this.scheduleFullRefresh_();
+  }
+
+  /**
+   * Set the absolute position of the cursor.
+   *
+   * @param {number} x
+   * @param {number} y
+   */
+  setCursor(x, y) {
+    this.core_._inputHandler._setCursor(x, y);
     this.scheduleFullRefresh_();
   }
 
diff --git a/terminal/js/terminal_xterm_internal_tests.js b/terminal/js/terminal_xterm_internal_tests.js
index 7baec8b..615e55b 100644
--- a/terminal/js/terminal_xterm_internal_tests.js
+++ b/terminal/js/terminal_xterm_internal_tests.js
@@ -76,14 +76,36 @@
     assert.equal(buffer.cursorY, 1);
   });
 
-  it('cursorLeft()', async function() {
-    await this.write('012');
+  it('moveCursor()', async function() {
+    await this.write('012\r\n345');
     const buffer = this.terminal.buffer.active;
     assert.equal(buffer.cursorX, 3);
+    assert.equal(buffer.cursorY, 1);
+    this.xtermInternal.moveCursor(-1, 0);
+    assert.equal(buffer.cursorX, 2);
+    assert.equal(buffer.cursorY, 1);
+    this.xtermInternal.moveCursor(-1, -1);
+    assert.equal(buffer.cursorX, 1);
     assert.equal(buffer.cursorY, 0);
-    this.xtermInternal.cursorLeft(1);
-    assert.equal(this.terminal.buffer.active.cursorX, 2);
+    this.xtermInternal.moveCursor(2, 3);
+    assert.equal(buffer.cursorX, 3);
+    assert.equal(buffer.cursorY, 3);
+  });
+
+  it('setCursor()', async function() {
+    const buffer = this.terminal.buffer.active;
+    assert.equal(buffer.cursorX, 0);
     assert.equal(buffer.cursorY, 0);
+    this.xtermInternal.setCursor(10, 20);
+    assert.equal(buffer.cursorX, 10);
+    assert.equal(buffer.cursorY, 20);
+  });
+
+  it('eraseInBufferLine()', async function() {
+    await this.write('012345');
+    this.xtermInternal.eraseInBufferLine(0, 2, 4);
+    assert.equal(this.terminal.buffer.active.getLine(0).translateToString(true),
+        '01  45');
   });
 
   it('installEscKHandler()', async function() {