]> git.proxmox.com Git - mirror_xterm.js.git/commitdiff
Merge branch 'master' into 553_find_api
authorDaniel Imms <tyriar@tyriar.com>
Wed, 21 Jun 2017 00:44:43 +0000 (17:44 -0700)
committerGitHub <noreply@github.com>
Wed, 21 Jun 2017 00:44:43 +0000 (17:44 -0700)
README.md
src/SelectionManager.test.ts
src/SelectionManager.ts
src/SelectionModel.test.ts
src/SelectionModel.ts
src/utils/Mouse.ts

index 05c9651c4a8d09760b874df33492a91ffa615ef8..f5bd1c7ad9249f5d28244eeae1a2fec5e3cad5c5 100644 (file)
--- a/README.md
+++ b/README.md
@@ -36,6 +36,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
 - [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R.
 - [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor.
 - [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud.
+- [**Gravitational Teleport**](https://github.com/gravitational/teleport): Gravitational Teleport is a modern SSH server for remotely accessing clusters of Linux servers via SSH or HTTPS.
 
 Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list.
 
index e0ff6789712daa40fb42045b1c682ebe6ca2e93c..eb9322b6c0168bcf993d505f4e5825ca5de44999 100644 (file)
@@ -142,6 +142,49 @@ describe('SelectionManager', () => {
       selectionManager.selectWordAt([14, 0]);
       assert.equal(selectionManager.selectionText, 'foo');
     });
+    it('should select up to non-path characters that are commonly adjacent to paths', () => {
+      buffer.push(stringToRow(':ab:(cd)[ef]{gh}\'ij"'));
+      selectionManager.selectWordAt([0, 0]);
+      assert.equal(selectionManager.selectionText, ':ab');
+      selectionManager.selectWordAt([1, 0]);
+      assert.equal(selectionManager.selectionText, 'ab');
+      selectionManager.selectWordAt([2, 0]);
+      assert.equal(selectionManager.selectionText, 'ab');
+      selectionManager.selectWordAt([3, 0]);
+      assert.equal(selectionManager.selectionText, 'ab:');
+      selectionManager.selectWordAt([4, 0]);
+      assert.equal(selectionManager.selectionText, '(cd');
+      selectionManager.selectWordAt([5, 0]);
+      assert.equal(selectionManager.selectionText, 'cd');
+      selectionManager.selectWordAt([6, 0]);
+      assert.equal(selectionManager.selectionText, 'cd');
+      selectionManager.selectWordAt([7, 0]);
+      assert.equal(selectionManager.selectionText, 'cd)');
+      selectionManager.selectWordAt([8, 0]);
+      assert.equal(selectionManager.selectionText, '[ef');
+      selectionManager.selectWordAt([9, 0]);
+      assert.equal(selectionManager.selectionText, 'ef');
+      selectionManager.selectWordAt([10, 0]);
+      assert.equal(selectionManager.selectionText, 'ef');
+      selectionManager.selectWordAt([11, 0]);
+      assert.equal(selectionManager.selectionText, 'ef]');
+      selectionManager.selectWordAt([12, 0]);
+      assert.equal(selectionManager.selectionText, '{gh');
+      selectionManager.selectWordAt([13, 0]);
+      assert.equal(selectionManager.selectionText, 'gh');
+      selectionManager.selectWordAt([14, 0]);
+      assert.equal(selectionManager.selectionText, 'gh');
+      selectionManager.selectWordAt([15, 0]);
+      assert.equal(selectionManager.selectionText, 'gh}');
+      selectionManager.selectWordAt([16, 0]);
+      assert.equal(selectionManager.selectionText, '\'ij');
+      selectionManager.selectWordAt([17, 0]);
+      assert.equal(selectionManager.selectionText, 'ij');
+      selectionManager.selectWordAt([18, 0]);
+      assert.equal(selectionManager.selectionText, 'ij');
+      selectionManager.selectWordAt([19, 0]);
+      assert.equal(selectionManager.selectionText, 'ij"');
+    });
   });
 
   describe('_selectLineAt', () => {
index f027e4a3f77dec9fcc11dfcf69e8c5103e6d3fcf..0151427c0caf18fd31e39b4fac31a1c293c09390 100644 (file)
@@ -38,6 +38,12 @@ const CLEAR_MOUSE_DOWN_TIME = 400;
  */
 const CLEAR_MOUSE_DISTANCE = 10;
 
+/**
+ * A string containing all characters that are considered word separated by the
+ * double click to select work logic.
+ */
+const WORD_SEPARATORS = ' ()[]{}:\'"';
+
 // TODO: Move these constants elsewhere, they belong in a buffer or buffer
 //       data/line class.
 const LINE_DATA_CHAR_INDEX = 1;
@@ -46,6 +52,23 @@ const LINE_DATA_WIDTH_INDEX = 2;
 const NON_BREAKING_SPACE_CHAR = String.fromCharCode(160);
 const ALL_NON_BREAKING_SPACE_REGEX = new RegExp(NON_BREAKING_SPACE_CHAR, 'g');
 
+/**
+ * Represents a position of a word on a line.
+ */
+interface IWordPosition {
+  start: number;
+  length: number;
+}
+
+/**
+ * A selection mode, this drives how the selection behaves on mouse move.
+ */
+enum SelectionMode {
+  NORMAL,
+  WORD,
+  LINE
+}
+
 /**
  * A class that manages the selection of the terminal. With help from
  * SelectionModel, SelectionManager handles with all logic associated with
@@ -81,9 +104,9 @@ export class SelectionManager extends EventEmitter {
   private _clickCount: number;
 
   /**
-   * Whether line select mode is active, this occurs after a triple click.
+   * The current selection mode.
    */
-  private _isLineSelectModeActive: boolean;
+  private _activeSelectionMode: SelectionMode;
 
   /**
    * A setInterval timer that is active while the mouse is down whose callback
@@ -113,7 +136,7 @@ export class SelectionManager extends EventEmitter {
 
     this._model = new SelectionModel(_terminal);
     this._lastMouseDownTime = 0;
-    this._isLineSelectModeActive = false;
+    this._activeSelectionMode = SelectionMode.NORMAL;
   }
 
   /**
@@ -264,7 +287,7 @@ export class SelectionManager extends EventEmitter {
    * @param event The mouse event.
    */
   private _getMouseBufferCoords(event: MouseEvent): [number, number] {
-    const coords = Mouse.getCoords(event, this._rowContainer, this._charMeasure, this._terminal.cols, this._terminal.rows);
+    const coords = Mouse.getCoords(event, this._rowContainer, this._charMeasure, this._terminal.cols, this._terminal.rows, true);
     // Convert to 0-based
     coords[0]--;
     coords[1]--;
@@ -366,7 +389,7 @@ export class SelectionManager extends EventEmitter {
   private _onSingleClick(event: MouseEvent): void {
     this._model.selectionStartLength = 0;
     this._model.isSelectAllActive = false;
-    this._isLineSelectModeActive = false;
+    this._activeSelectionMode = SelectionMode.NORMAL;
     this._model.selectionStart = this._getMouseBufferCoords(event);
     if (this._model.selectionStart) {
       this._model.selectionEnd = null;
@@ -386,6 +409,7 @@ export class SelectionManager extends EventEmitter {
   private _onDoubleClick(event: MouseEvent): void {
     const coords = this._getMouseBufferCoords(event);
     if (coords) {
+      this._activeSelectionMode = SelectionMode.WORD;
       this._selectWordAt(coords);
     }
   }
@@ -398,7 +422,7 @@ export class SelectionManager extends EventEmitter {
   private _onTripleClick(event: MouseEvent): void {
     const coords = this._getMouseBufferCoords(event);
     if (coords) {
-      this._isLineSelectModeActive = true;
+      this._activeSelectionMode = SelectionMode.LINE;
       this._selectLineAt(coords[1]);
     }
   }
@@ -443,12 +467,14 @@ export class SelectionManager extends EventEmitter {
     this._model.selectionEnd = this._getMouseBufferCoords(event);
 
     // Select the entire line if line select mode is active.
-    if (this._isLineSelectModeActive) {
+    if (this._activeSelectionMode === SelectionMode.LINE) {
       if (this._model.selectionEnd[1] < this._model.selectionStart[1]) {
         this._model.selectionEnd[0] = 0;
       } else {
         this._model.selectionEnd[0] = this._terminal.cols;
       }
+    } else if (this._activeSelectionMode === SelectionMode.WORD) {
+      this._selectToWordAt(this._model.selectionEnd);
     }
 
     // Determine the amount of scrolling that will happen.
@@ -530,11 +556,10 @@ export class SelectionManager extends EventEmitter {
   }
 
   /**
-   * Selects the word at the coordinates specified. Words are defined as all
-   * non-whitespace characters.
+   * Gets positional information for the word at the coordinated specified.
    * @param coords The coordinates to get the word at.
    */
-  protected _selectWordAt(coords: [number, number]): void {
+  private _getWordAt(coords: [number, number]): IWordPosition {
     const bufferLine = this._buffer.get(coords[1]);
     const line = translateBufferLineToString(bufferLine, false);
 
@@ -573,7 +598,7 @@ export class SelectionManager extends EventEmitter {
         endCol++;
       }
       // Expand the string in both directions until a space is hit
-      while (startIndex > 0 && line.charAt(startIndex - 1) !== ' ') {
+      while (startIndex > 0 && !this._isCharWordSeparator(line.charAt(startIndex - 1))) {
         if (bufferLine[startCol - 1][LINE_DATA_WIDTH_INDEX] === 0) {
           // If the next character is a wide char, record it and skip the column
           leftWideCharCount++;
@@ -582,7 +607,7 @@ export class SelectionManager extends EventEmitter {
         startIndex--;
         startCol--;
       }
-      while (endIndex + 1 < line.length && line.charAt(endIndex + 1) !== ' ') {
+      while (endIndex + 1 < line.length && !this._isCharWordSeparator(line.charAt(endIndex + 1))) {
         if (bufferLine[endCol + 1][LINE_DATA_WIDTH_INDEX] === 2) {
           // If the next character is a wide char, record it and skip the column
           rightWideCharCount++;
@@ -593,9 +618,37 @@ export class SelectionManager extends EventEmitter {
       }
     }
 
-    // Record the resulting selection
-    this._model.selectionStart = [startIndex + charOffset - leftWideCharCount, coords[1]];
-    this._model.selectionStartLength = Math.min(endIndex - startIndex + leftWideCharCount + rightWideCharCount + 1/*include endIndex char*/, this._terminal.cols);
+    const start = startIndex + charOffset - leftWideCharCount;
+    const length = Math.min(endIndex - startIndex + leftWideCharCount + rightWideCharCount + 1/*include endIndex char*/, this._terminal.cols);
+    return {start, length};
+  }
+
+  /**
+   * Selects the word at the coordinates specified.
+   * @param coords The coordinates to get the word at.
+   */
+  protected _selectWordAt(coords: [number, number]): void {
+    const wordPosition = this._getWordAt(coords);
+    this._model.selectionStart = [wordPosition.start, coords[1]];
+    this._model.selectionStartLength = wordPosition.length;
+  }
+
+  /**
+   * Sets the selection end to the word at the coordinated specified.
+   * @param coords The coordinates to get the word at.
+   */
+  private _selectToWordAt(coords: [number, number]): void {
+    const wordPosition = this._getWordAt(coords);
+    this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]];
+  }
+
+  /**
+   * Gets whether the character is considered a word separator by the select
+   * word logic.
+   * @param char The character to check.
+   */
+  private _isCharWordSeparator(char: string): boolean {
+    return WORD_SEPARATORS.indexOf(char) >= 0;
   }
 
   /**
index e8629596c0fc6e4c1b4f4c6480688e5ec654e5ad..ab22b77cc50bb9fb6ccbf8a25df0528dc82de1a4 100644 (file)
@@ -11,8 +11,6 @@ class TestSelectionModel extends SelectionModel {
   ) {
     super(terminal);
   }
-
-  public areSelectionValuesReversed(): boolean { return this._areSelectionValuesReversed(); }
 }
 
 describe('SelectionManager', () => {
@@ -39,7 +37,7 @@ describe('SelectionManager', () => {
     });
   });
 
-  describe('_areSelectionValuesReversed', () => {
+  describe('areSelectionValuesReversed', () => {
     it('should return true when the selection end is before selection start', () => {
       model.selectionStart = [1, 0];
       model.selectionEnd = [0, 0];
index 403f42e01b3e4a35eed397552910ab68a2553bed..410af3b3a681b96c5b9261b5b90ae144171dc60c 100644 (file)
@@ -59,7 +59,7 @@ export class SelectionModel {
       return this.selectionStart;
     }
 
-    return this._areSelectionValuesReversed() ? this.selectionEnd : this.selectionStart;
+    return this.areSelectionValuesReversed() ? this.selectionEnd : this.selectionStart;
   }
 
   /**
@@ -76,7 +76,7 @@ export class SelectionModel {
     }
 
     // Use the selection start if the end doesn't exist or they're reversed
-    if (!this.selectionEnd || this._areSelectionValuesReversed()) {
+    if (!this.selectionEnd || this.areSelectionValuesReversed()) {
       return [this.selectionStart[0] + this.selectionStartLength, this.selectionStart[1]];
     }
 
@@ -93,7 +93,7 @@ export class SelectionModel {
   /**
    * Returns whether the selection start and end are reversed.
    */
-  protected _areSelectionValuesReversed(): boolean {
+  public areSelectionValuesReversed(): boolean {
     const start = this.selectionStart;
     const end = this.selectionEnd;
     return start[1] > end[1] || (start[1] === end[1] && start[0] > end[0]);
index 79c5d5c1160735a0f47ce37f161e3a787840b467..a5d72c1e9310b199ca8df8b0c449548d7912795d 100644 (file)
@@ -30,12 +30,17 @@ export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLEleme
  * @param event The mouse event.
  * @param rowContainer The terminal's row container.
  * @param charMeasure The char measure object used to determine character sizes.
+ * @param colCount The number of columns in the terminal.
+ * @param rowCount The number of rows n the terminal.
+ * @param isSelection Whether the request is for the selection or not. This will
+ * apply an offset to the x value such that the left half of the cell will
+ * select that cell and the right half will select the next cell.
  */
-export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number): [number, number] {
+export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number] {
   const coords = getCoordsRelativeToElement(event, rowContainer);
 
-  // Convert to cols/rows
-  coords[0] = Math.ceil(coords[0] / charMeasure.width);
+  // Convert to cols/rows.
+  coords[0] = Math.ceil((coords[0] + (isSelection ? charMeasure.width / 2 : 0)) / charMeasure.width);
   coords[1] = Math.ceil(coords[1] / charMeasure.height);
 
   // Ensure coordinates are within the terminal viewport.