hterm: change CSI-J-3 to clear scrollback

We've long had CSI-J-3 (clear scrollback) wired to behave like CSI-J-2
(clear screen) because letting the remote clear local scrollback might
not be the best behavior.  Add a config setting to control it so we can
change the CSI-J-3 extension to match xterm where we got it from.

Bug: chromium:881507
Change-Id: I13cfa6d087e2ba456c80c9d0c7518b3db650fcfa
Reviewed-on: https://chromium-review.googlesource.com/1214627
Reviewed-by: Vitaliy Shipitsyn <vsh@google.com>
Tested-by: Mike Frysinger <vapier@chromium.org>
diff --git a/hterm/js/hterm_preference_manager.js b/hterm/js/hterm_preference_manager.js
index 9bb17bc..2b0b5ef 100644
--- a/hterm/js/hterm_preference_manager.js
+++ b/hterm/js/hterm_preference_manager.js
@@ -289,6 +289,13 @@
    'Respect the host\'s attempt to change the text cursor blink status using ' +
    'DEC Private Mode 12.'],
 
+  'enable-csi-j-3':
+  [hterm.PreferenceManager.categories.Miscellaneous, true, 'bool',
+   'Whether CSI-J (Erase Display) mode 3 may clear the terminal scrollback ' +
+   'buffer.\n' +
+   '\n' +
+   'Enabling this by default is safe.'],
+
   'environment':
   [hterm.PreferenceManager.categories.Miscellaneous,
    {
diff --git a/hterm/js/hterm_terminal.js b/hterm/js/hterm_terminal.js
index ada7478..30c3adc 100644
--- a/hterm/js/hterm_terminal.js
+++ b/hterm/js/hterm_terminal.js
@@ -410,6 +410,10 @@
       terminal.vt.enableDec12 = !!v;
     },
 
+    'enable-csi-j-3': function(v) {
+      terminal.vt.enableCsiJ3 = !!v;
+    },
+
     'font-family': function(v) {
       terminal.syncFontFamily();
     },
diff --git a/hterm/js/hterm_vt.js b/hterm/js/hterm_vt.js
index 58f471f..fbf9ee4 100644
--- a/hterm/js/hterm_vt.js
+++ b/hterm/js/hterm_vt.js
@@ -80,6 +80,11 @@
   this.enableDec12 = false;
 
   /**
+   * Respect the host's attempt to clear the scrollback buffer using CSI-J-3.
+   */
+  this.enableCsiJ3 = true;
+
+  /**
    * The expected encoding method for data received from the host.
    */
   this.characterEncoding = 'utf-8';
@@ -2152,9 +2157,9 @@
   } else if (arg == 2) {
     this.terminal.clear();
   } else if (arg == 3) {
-    // The xterm docs say this means "Erase saved lines", but we'll just clear
-    // the display since killing the scrollback seems rude.
-    this.terminal.clear();
+    if (this.enableCsiJ3) {
+      this.terminal.clearScrollback();
+    }
   }
 };
 
diff --git a/hterm/js/hterm_vt_tests.js b/hterm/js/hterm_vt_tests.js
index 7c841d7..66629d2 100644
--- a/hterm/js/hterm_vt_tests.js
+++ b/hterm/js/hterm_vt_tests.js
@@ -3399,3 +3399,160 @@
 
   result.pass();
 });
+
+/**
+ * Verify CSI-J-0 (erase below) works.
+ */
+hterm.VT.Tests.addTest('csi-j-0', function(result, cx) {
+  const terminal = this.terminal;
+
+  // Fill the screen with something useful.
+  for (let i = 0; i < this.visibleRowCount * 2; ++i) {
+    terminal.interpret(`ab${i}\n\r`);
+  }
+  const rowCount = terminal.getRowCount();
+  terminal.scrollEnd();
+  terminal.scrollPort_.redraw_();
+
+  // Move to the middle of the screen.
+  terminal.setCursorPosition(3, 1);
+  result.assertEQ('ab9', terminal.getRowText(9));
+  result.assertEQ('ab10', terminal.getRowText(10));
+
+  // Clear after & including the cursor (implicit arg=0).
+  terminal.interpret('\x1b[J');
+  result.assertEQ(3, terminal.getCursorRow());
+  result.assertEQ(1, terminal.getCursorColumn());
+  result.assertEQ('ab9', terminal.getRowText(9));
+  result.assertEQ('a', terminal.getRowText(10));
+  result.assertEQ('', terminal.getRowText(11));
+
+  // Move up and clear after & including the cursor (explicit arg=0).
+  terminal.setCursorPosition(2, 1);
+  terminal.interpret('\x1b[0J');
+  result.assertEQ(2, terminal.getCursorRow());
+  result.assertEQ(1, terminal.getCursorColumn());
+  result.assertEQ('ab8', terminal.getRowText(8));
+  result.assertEQ('a', terminal.getRowText(9));
+  result.assertEQ('', terminal.getRowText(10));
+
+  // The scrollback should stay intact.
+  result.assertEQ('ab0', terminal.getRowText(0));
+  result.assertEQ(rowCount, terminal.getRowCount());
+
+  result.pass();
+});
+
+/**
+ * Verify CSI-J-1 (erase above) works.
+ */
+hterm.VT.Tests.addTest('csi-j-1', function(result, cx) {
+  const terminal = this.terminal;
+
+  // Fill the screen with something useful.
+  for (let i = 0; i < this.visibleRowCount * 2; ++i) {
+    terminal.interpret(`ab${i}\n\r`);
+  }
+  const rowCount = terminal.getRowCount();
+  terminal.scrollEnd();
+  terminal.scrollPort_.redraw_();
+
+  // Move to the middle of the screen.
+  terminal.setCursorPosition(3, 1);
+  result.assertEQ('ab9', terminal.getRowText(9));
+  result.assertEQ('ab10', terminal.getRowText(10));
+
+  // Clear before & including the cursor (arg=1).
+  terminal.interpret('\x1b[1J');
+  result.assertEQ(3, terminal.getCursorRow());
+  result.assertEQ(1, terminal.getCursorColumn());
+  result.assertEQ('', terminal.getRowText(9));
+  result.assertEQ('  10', terminal.getRowText(10));
+  result.assertEQ('ab11', terminal.getRowText(11));
+
+  // The scrollback should stay intact.
+  result.assertEQ('ab0', terminal.getRowText(0));
+  result.assertEQ(rowCount, terminal.getRowCount());
+
+  result.pass();
+});
+
+/**
+ * Verify CSI-J-2 (erase screen) works.
+ */
+hterm.VT.Tests.addTest('csi-j-2', function(result, cx) {
+  const terminal = this.terminal;
+
+  // Fill the screen with something useful.
+  for (let i = 0; i < this.visibleRowCount * 2; ++i) {
+    terminal.interpret(`ab${i}\n\r`);
+  }
+  const rowCount = terminal.getRowCount();
+  terminal.scrollEnd();
+  terminal.scrollPort_.redraw_();
+
+  // Move to the middle of the screen.
+  terminal.setCursorPosition(3, 1);
+  result.assertEQ('ab9', terminal.getRowText(9));
+  result.assertEQ('ab10', terminal.getRowText(10));
+
+  // Clear the screen (arg=2).
+  terminal.interpret('\x1b[2J');
+  result.assertEQ(3, terminal.getCursorRow());
+  result.assertEQ(1, terminal.getCursorColumn());
+  result.assertEQ('', terminal.getRowText(9));
+  result.assertEQ('', terminal.getRowText(10));
+  result.assertEQ('', terminal.getRowText(11));
+
+  // The scrollback should stay intact.
+  result.assertEQ('ab0', terminal.getRowText(0));
+  result.assertEQ(rowCount, terminal.getRowCount());
+
+  result.pass();
+});
+
+/**
+ * Verify CSI-J-3 (erase scrollback) works.
+ */
+hterm.VT.Tests.addTest('csi-j-3', function(result, cx) {
+  const terminal = this.terminal;
+
+  // Fill the screen with something useful.
+  for (let i = 0; i < this.visibleRowCount * 2; ++i) {
+    terminal.interpret(`ab${i}\n\r`);
+  }
+  const rowCount = terminal.getRowCount();
+  terminal.scrollEnd();
+  terminal.scrollPort_.redraw_();
+
+  // Move to the middle of the screen.
+  terminal.setCursorPosition(3, 1);
+  result.assertEQ('ab9', terminal.getRowText(9));
+  result.assertEQ('ab10', terminal.getRowText(10));
+
+  // Disable this feature.  It should make it a nop.
+  terminal.vt.enableCsiJ3 = false;
+  terminal.interpret('\x1b[3J');
+  result.assertEQ(3, terminal.getCursorRow());
+  result.assertEQ(1, terminal.getCursorColumn());
+  result.assertEQ('ab0', terminal.getRowText(0));
+  result.assertEQ(rowCount, terminal.getRowCount());
+
+  // Re-enable the feature.
+  terminal.vt.enableCsiJ3 = true;
+
+  // Clear the scrollback (arg=3).
+  // The current screen should stay intact.
+  terminal.interpret('\x1b[3J');
+  result.assertEQ(3, terminal.getCursorRow());
+  result.assertEQ(1, terminal.getCursorColumn());
+  result.assertEQ('ab7', terminal.getRowText(0));
+  result.assertEQ('ab8', terminal.getRowText(1));
+  result.assertEQ('ab11', terminal.getRowText(this.visibleRowCount - 2));
+
+  // The scrollback should be gone.
+  result.assertEQ(this.visibleRowCount, terminal.getRowCount());
+  result.assertEQ([], terminal.scrollbackRows_);
+
+  result.pass();
+});