From 70fda994151c803fccaf51f465f6857fb398c18f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 5 May 2017 17:36:15 -0700 Subject: [PATCH 01/61] Basic fetching of coordinates --- src/EventEmitter.ts | 5 +-- src/Parser.ts | 11 +++--- src/SelectionManager.ts | 65 ++++++++++++++++++++++++++++++++ src/utils/CircularList.ts | 12 +++++- src/utils/Mouse.ts | 30 +++++++++++++++ src/xterm.css | 1 + src/xterm.js | 79 ++++++++++++++++++++------------------- 7 files changed, 154 insertions(+), 49 deletions(-) create mode 100644 src/SelectionManager.ts create mode 100644 src/utils/Mouse.ts diff --git a/src/EventEmitter.ts b/src/EventEmitter.ts index 1db86763..a4fd4542 100644 --- a/src/EventEmitter.ts +++ b/src/EventEmitter.ts @@ -53,14 +53,11 @@ export class EventEmitter { return this.on(type, on); } - public emit(type): void { + public emit(type: string, ...args: any[]): void { if (!this._events[type]) { return; } - - let args = Array.prototype.slice.call(arguments, 1); let obj = this._events[type]; - for (let i = 0; i < obj.length; i++) { obj[i].apply(this, args); } diff --git a/src/Parser.ts b/src/Parser.ts index 01821a5a..92ff1595 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -471,6 +471,8 @@ export class Parser { case ParserState.DCS: if (ch === C0.ESC || ch === C0.BEL) { if (ch === C0.ESC) this._position++; + let pt; + let valid; switch (this._terminal.prefix) { // User-Defined Keys (DECUDK). @@ -480,8 +482,8 @@ export class Parser { // Request Status String (DECRQSS). // test: echo -e '\eP$q"p\e\\' case '$q': - let pt = this._terminal.currentParam - , valid = false; + pt = this._terminal.currentParam; + valid = false; switch (pt) { // DECSCA @@ -526,9 +528,8 @@ export class Parser { // This can cause a small glitch in vim. // test: echo -ne '\eP+q6b64\e\\' case '+q': - // TODO: Don't declare pt twice - /*let*/ pt = this._terminal.currentParam - , valid = false; + pt = this._terminal.currentParam; + valid = false; this._terminal.send(C0.ESC + 'P' + +valid + '+r' + pt + C0.ESC + '\\'); break; diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts new file mode 100644 index 00000000..df9294ba --- /dev/null +++ b/src/SelectionManager.ts @@ -0,0 +1,65 @@ +/** + * @license MIT + */ + +import { CharMeasure } from './utils/CharMeasure'; +import { CircularList } from './utils/CircularList'; +import * as Mouse from './utils/Mouse'; + +export class SelectionManager { + private _selectionStart: [number, number]; + private _selectionEnd: [number, number]; + + private _buffer: CircularList; + private _rowContainer: HTMLElement; + private _charMeasure: CharMeasure; + + private _mouseMoveListener: EventListener; + + constructor(buffer: CircularList, rowContainer: HTMLElement, charMeasure: CharMeasure) { + this._rowContainer = rowContainer; + this._buffer = buffer; + this._charMeasure = charMeasure; + this._attachListeners(); + } + + private _attachListeners() { + this._mouseMoveListener = event => this._onMouseMove(event); + + this._buffer.on('trim', amount => this._onTrim(amount)); + this._rowContainer.addEventListener('mousedown', event => this._onMouseDown(event)); + this._rowContainer.addEventListener('mouseup', event => this._onMouseUp(event)); + } + + public get selectionText(): string { + if (!this._selectionStart || !this._selectionEnd) { + return null; + } + return ''; + } + + private _onTrim(amount: number) { + console.log('trimmed: ' + amount); + } + + private _onMouseDown(event: MouseEvent) { + this._selectionStart = Mouse.getCoords(event, this._rowContainer, this._charMeasure); + if (this._selectionStart) { + this._rowContainer.addEventListener('mousemove', this._mouseMoveListener); + } + } + + private _onMouseMove(event: MouseEvent) { + this._selectionEnd = Mouse.getCoords(event, this._rowContainer, this._charMeasure); + } + + private _onMouseUp(event: MouseEvent) { + console.log('mouseup'); + console.log('start', this._selectionStart); + console.log('end', this._selectionEnd); + if (!this._selectionStart) { + return; + } + this._rowContainer.removeEventListener('mousemove', this._mouseMoveListener); + } +} diff --git a/src/utils/CircularList.ts b/src/utils/CircularList.ts index b72c667f..909df766 100644 --- a/src/utils/CircularList.ts +++ b/src/utils/CircularList.ts @@ -4,12 +4,15 @@ * @module xterm/utils/CircularList * @license MIT */ -export class CircularList { +import { EventEmitter } from '../EventEmitter'; + +export class CircularList extends EventEmitter { private _array: T[]; private _startIndex: number; private _length: number; constructor(maxLength: number) { + super(); this._array = new Array(maxLength); this._startIndex = 0; this._length = 0; @@ -83,6 +86,7 @@ export class CircularList { if (this._startIndex === this.maxLength) { this._startIndex = 0; } + this.emit('trim', 1); } else { this._length++; } @@ -121,8 +125,10 @@ export class CircularList { } if (this._length + items.length > this.maxLength) { - this._startIndex += (this._length + items.length) - this.maxLength; + const countToTrim = (this._length + items.length) - this.maxLength; + this._startIndex += countToTrim; this._length = this.maxLength; + this.emit('trim', countToTrim); } else { this._length += items.length; } @@ -139,6 +145,7 @@ export class CircularList { } this._startIndex += count; this._length -= count; + this.emit('trim', count); } public shiftElements(start: number, count: number, offset: number): void { @@ -162,6 +169,7 @@ export class CircularList { while (this._length > this.maxLength) { this._length--; this._startIndex++; + this.emit('trim', 1); } } } else { diff --git a/src/utils/Mouse.ts b/src/utils/Mouse.ts new file mode 100644 index 00000000..7b18d41e --- /dev/null +++ b/src/utils/Mouse.ts @@ -0,0 +1,30 @@ +/** + * @license MIT + */ + +import { CharMeasure } from './CharMeasure'; + +export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure): [number, number] { + // ignore browsers without pageX for now + if (event.pageX == null) { + return null; + } + + let x = event.pageX; + let y = event.pageY; + let el = rowContainer; + + // should probably check offsetParent + // but this is more portable + while (el && el !== self.document.documentElement) { + x -= el.offsetLeft; + y -= el.offsetTop; + el = 'offsetParent' in el ? el.offsetParent : el.parentElement; + } + + // convert to cols/rows + x = Math.ceil(x / charMeasure.width); + y = Math.ceil(y / charMeasure.height); + + return [x, y]; +} diff --git a/src/xterm.css b/src/xterm.css index a67485e5..72c1b859 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -41,6 +41,7 @@ font-family: courier-new, courier, monospace; font-feature-settings: "liga" 0; position: relative; + user-select: none; } .terminal.focus, diff --git a/src/xterm.js b/src/xterm.js index 75d0bc45..dd3a090d 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -20,9 +20,10 @@ import { InputHandler } from './InputHandler'; import { Parser } from './Parser'; import { Renderer } from './Renderer'; import { Linkifier } from './Linkifier'; +import { SelectionManager } from './SelectionManager'; import { CharMeasure } from './utils/CharMeasure'; import * as Browser from './utils/Browser'; -import * as Keyboard from './utils/Keyboard'; +import * as Mouse from './utils/Mouse'; import { CHARSETS } from './Charsets'; /** @@ -219,6 +220,7 @@ function Terminal(options) { this.parser = new Parser(this.inputHandler, this); // Reuse renderer if the Terminal is being recreated via a Terminal.reset call. this.renderer = this.renderer || null; + this.selectionManager = this.selectionManager || null; this.linkifier = this.linkifier || new Linkifier(); // user input states @@ -689,6 +691,7 @@ Terminal.prototype.open = function(parent, focus) { this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); this.renderer = new Renderer(this); + this.selectionManager = new SelectionManager(this.lines, this.rowContainer, this.charMeasure); // Setup loop that draws to screen this.refresh(0, this.rows - 1); @@ -787,7 +790,7 @@ Terminal.prototype.bindMouse = function() { button = getButton(ev); // get mouse coordinates - pos = getCoords(ev); + pos = Mouse.getCoords(ev, this.rowContainer, this.charMeasure); if (!pos) return; sendEvent(button, pos); @@ -815,7 +818,7 @@ Terminal.prototype.bindMouse = function() { var button = pressed , pos; - pos = getCoords(ev); + pos = Mouse.getCoords(ev, this.rowContainer, this.charMeasure); if (!pos) return; // buttons marked as motions @@ -991,48 +994,48 @@ Terminal.prototype.bindMouse = function() { } // mouse coordinates measured in cols/rows - function getCoords(ev) { - var x, y, w, h, el; + // function getCoords(ev) { + // var x, y, w, h, el; - // ignore browsers without pageX for now - if (ev.pageX == null) return; + // // ignore browsers without pageX for now + // if (ev.pageX == null) return; - x = ev.pageX; - y = ev.pageY; - el = self.element; + // x = ev.pageX; + // y = ev.pageY; + // el = self.rowContainer; - // should probably check offsetParent - // but this is more portable - while (el && el !== self.document.documentElement) { - x -= el.offsetLeft; - y -= el.offsetTop; - el = 'offsetParent' in el - ? el.offsetParent - : el.parentNode; - } + // // should probably check offsetParent + // // but this is more portable + // while (el && el !== self.document.documentElement) { + // x -= el.offsetLeft; + // y -= el.offsetTop; + // el = 'offsetParent' in el + // ? el.offsetParent + // : el.parentNode; + // } - // convert to cols/rows - x = Math.ceil(x / self.charMeasure.width); - y = Math.ceil(y / self.charMeasure.height); + // // convert to cols/rows + // x = Math.ceil(x / self.charMeasure.width); + // y = Math.ceil(y / self.charMeasure.height); - // be sure to avoid sending - // bad positions to the program - if (x < 0) x = 0; - if (x > self.cols) x = self.cols; - if (y < 0) y = 0; - if (y > self.rows) y = self.rows; + // // be sure to avoid sending + // // bad positions to the program + // if (x < 0) x = 0; + // if (x > self.cols) x = self.cols; + // if (y < 0) y = 0; + // if (y > self.rows) y = self.rows; - // xterm sends raw bytes and - // starts at 32 (SP) for each. - x += 32; - y += 32; + // // xterm sends raw bytes and + // // starts at 32 (SP) for each. + // x += 32; + // y += 32; - return { - x: x, - y: y, - type: 'wheel' - }; - } + // return { + // x: x, + // y: y, + // type: 'wheel' + // }; + // } on(el, 'mousedown', function(ev) { if (!self.mouseEvents) return; From b36d8780fded65320f308aff6039097123dd4652 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 5 May 2017 17:40:23 -0700 Subject: [PATCH 02/61] Clean up --- src/SelectionManager.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index df9294ba..4d4533f2 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -39,18 +39,25 @@ export class SelectionManager { } private _onTrim(amount: number) { + // TODO: Somehow map the selection coordinates with the list that is constantly being trimmed + // Maybe we need an ID in the CircularList that starts from 0 for the first entry and increments console.log('trimmed: ' + amount); } + private _getMouseBufferCoords(event: MouseEvent) { + // TODO: Take into account the current terminal viewport when fetching coordinates + return Mouse.getCoords(event, this._rowContainer, this._charMeasure); + } + private _onMouseDown(event: MouseEvent) { - this._selectionStart = Mouse.getCoords(event, this._rowContainer, this._charMeasure); + this._selectionStart = this._getMouseBufferCoords(event); if (this._selectionStart) { this._rowContainer.addEventListener('mousemove', this._mouseMoveListener); } } private _onMouseMove(event: MouseEvent) { - this._selectionEnd = Mouse.getCoords(event, this._rowContainer, this._charMeasure); + this._selectionEnd = this._getMouseBufferCoords(event); } private _onMouseUp(event: MouseEvent) { From 65256c8751831b8f4c7bbc8386953ce042b12897 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 5 May 2017 20:09:57 -0700 Subject: [PATCH 03/61] Support an entry ID in CircularList This is needed to keep track of which row is which outside of the class --- src/utils/CircularList.ts | 58 +++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/src/utils/CircularList.ts b/src/utils/CircularList.ts index 909df766..1a698ff0 100644 --- a/src/utils/CircularList.ts +++ b/src/utils/CircularList.ts @@ -6,14 +6,21 @@ */ import { EventEmitter } from '../EventEmitter'; +interface ListEntry { + id: number; + value: T; +} + export class CircularList extends EventEmitter { - private _array: T[]; + private _array: ListEntry[]; private _startIndex: number; private _length: number; + private _nextId = 0; + constructor(maxLength: number) { super(); - this._array = new Array(maxLength); + this._array = new Array>(maxLength); this._startIndex = 0; this._length = 0; } @@ -25,9 +32,14 @@ export class CircularList extends EventEmitter { public set maxLength(newMaxLength: number) { // Reconstruct array, starting at index 0. Only transfer values from the // indexes 0 to length. - let newArray = new Array(newMaxLength); + let newArray = new Array>(newMaxLength); + // Reset ids when maxLength is changed + this._nextId = 0; for (let i = 0; i < Math.min(newMaxLength, this.length); i++) { - newArray[i] = this._array[this._getCyclicIndex(i)]; + newArray[i] = { + id: this._nextId++, + value: this._array[this._getCyclicIndex(i)].value + }; } this._array = newArray; this._startIndex = 0; @@ -46,8 +58,14 @@ export class CircularList extends EventEmitter { this._length = newLength; } - public get forEach(): (callbackfn: (value: T, index: number, array: T[]) => void) => void { - return this._array.forEach; + public get forEach(): (callbackfn: (value: T, index: number) => void) => void { + return (callbackfn: (value: T, index: number) => void) => { + let i = 0; + let length = this.length; + for (let i = 0; i < length; i++) { + callbackfn(this.get(i), i); + } + }; } /** @@ -59,6 +77,10 @@ export class CircularList extends EventEmitter { * @return The value corresponding to the index. */ public get(index: number): T { + return this.getEntry(index).value; + } + + public getEntry(index: number): ListEntry { return this._array[this._getCyclicIndex(index)]; } @@ -71,7 +93,11 @@ export class CircularList extends EventEmitter { * @param value The value to set. */ public set(index: number, value: T): void { - this._array[this._getCyclicIndex(index)] = value; + this._array[this._getCyclicIndex(index)].value = value; + } + + private _setEntry(index: number, entry: ListEntry): void { + this._array[this._getCyclicIndex(index)] = entry; } /** @@ -80,7 +106,10 @@ export class CircularList extends EventEmitter { * @param value The value to push onto the list. */ public push(value: T): void { - this._array[this._getCyclicIndex(this._length)] = value; + this._array[this._getCyclicIndex(this._length)] = { + id: this._nextId, + value + }; if (this._length === this.maxLength) { this._startIndex++; if (this._startIndex === this.maxLength) { @@ -97,7 +126,7 @@ export class CircularList extends EventEmitter { * @return The popped value. */ public pop(): T { - return this._array[this._getCyclicIndex(this._length-- - 1)]; + return this._array[this._getCyclicIndex(this._length-- - 1)].value; } /** @@ -110,20 +139,27 @@ export class CircularList extends EventEmitter { * @param items The items to insert. */ public splice(start: number, deleteCount: number, ...items: T[]): void { + // Delete items if (deleteCount) { for (let i = start; i < this._length - deleteCount; i++) { this._array[this._getCyclicIndex(i)] = this._array[this._getCyclicIndex(i + deleteCount)]; } this._length -= deleteCount; } + if (items && items.length) { + // Add items for (let i = this._length - 1; i >= start; i--) { this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)]; } for (let i = 0; i < items.length; i++) { - this._array[this._getCyclicIndex(start + i)] = items[i]; + this._array[this._getCyclicIndex(start + i)] = { + id: this._nextId, + value: items[i] + }; } + // Adjust length as needed if (this._length + items.length > this.maxLength) { const countToTrim = (this._length + items.length) - this.maxLength; this._startIndex += countToTrim; @@ -161,7 +197,7 @@ export class CircularList extends EventEmitter { if (offset > 0) { for (let i = count - 1; i >= 0; i--) { - this.set(start + i + offset, this.get(start + i)); + this._setEntry(start + i + offset, this.getEntry(start + i)); } const expandListBy = (start + count + offset) - this._length; if (expandListBy > 0) { From fe11837a61b0dae268c98cb4ce7bb21c6f10b1e6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 19 May 2017 19:11:28 -0700 Subject: [PATCH 04/61] Create new entries when shifting --- src/utils/CircularList.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils/CircularList.ts b/src/utils/CircularList.ts index 1a698ff0..bc693260 100644 --- a/src/utils/CircularList.ts +++ b/src/utils/CircularList.ts @@ -197,7 +197,10 @@ export class CircularList extends EventEmitter { if (offset > 0) { for (let i = count - 1; i >= 0; i--) { - this._setEntry(start + i + offset, this.getEntry(start + i)); + this._setEntry(start + i + offset, { + id: this._nextId++, + value: this.get(start + i) + }); } const expandListBy = (start + count + offset) - this._length; if (expandListBy > 0) { From 207c4cf961c76f809166099fe548238a15cff4b8 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 19 May 2017 19:57:52 -0700 Subject: [PATCH 05/61] More work on SelectionManager --- src/SelectionManager.ts | 39 ++++++++++++++++++++++++++++++++++++++- src/utils/CircularList.ts | 1 + src/xterm.css | 12 ++++++++++++ src/xterm.js | 8 +++++++- 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 4d4533f2..3c99ef66 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -12,12 +12,14 @@ export class SelectionManager { private _buffer: CircularList; private _rowContainer: HTMLElement; + private _selectionContainer: HTMLElement; private _charMeasure: CharMeasure; private _mouseMoveListener: EventListener; - constructor(buffer: CircularList, rowContainer: HTMLElement, charMeasure: CharMeasure) { + constructor(buffer: CircularList, rowContainer: HTMLElement, selectionContainer: HTMLElement, charMeasure: CharMeasure) { this._rowContainer = rowContainer; + this._selectionContainer = selectionContainer; this._buffer = buffer; this._charMeasure = charMeasure; this._attachListeners(); @@ -38,10 +40,43 @@ export class SelectionManager { return ''; } + /** + * Redraws the selection. + */ + public refresh(): void { + console.log(`Selection: Start: (${this._selectionStart[0]}, ${this._selectionStart[1]}), End: (${this._selectionEnd[0]}, ${this._selectionEnd[1]})`); + this._selectionContainer.innerHTML = `


`; + } + + /** + * Handle the buffer being trimmed, adjust the selection position. + * @param amount The amount the buffer is being trimmed. + */ private _onTrim(amount: number) { // TODO: Somehow map the selection coordinates with the list that is constantly being trimmed // Maybe we need an ID in the CircularList that starts from 0 for the first entry and increments console.log('trimmed: ' + amount); + + // Adjust the selection position based on the trimmed amount. + this._selectionStart[0] -= amount; + this._selectionEnd[0] -= amount; + + // The selection has moved off the buffer, clear it. + if (this._selectionEnd[0] < 0) { + this._selectionStart = null; + this._selectionEnd = null; + this.refresh(); + return; + } + + // If the selection start is trimmed, ensure the start column is 0. + if (this._selectionStart[0] < 0) { + this._selectionStart[1] = 0; + } + + // Maybe SelectionManager could maintain it's own ID concept from 0 (top of + // buffer) to n (size of buffer). On trim just increment the selection if + // necessary. This would reduce complexity and potentially not need the } private _getMouseBufferCoords(event: MouseEvent) { @@ -58,6 +93,8 @@ export class SelectionManager { private _onMouseMove(event: MouseEvent) { this._selectionEnd = this._getMouseBufferCoords(event); + // TODO: Only draw here if the selection changes + this.refresh(); } private _onMouseUp(event: MouseEvent) { diff --git a/src/utils/CircularList.ts b/src/utils/CircularList.ts index bc693260..b6fafe7c 100644 --- a/src/utils/CircularList.ts +++ b/src/utils/CircularList.ts @@ -6,6 +6,7 @@ */ import { EventEmitter } from '../EventEmitter'; +// TODO: Do we need the ID here? interface ListEntry { id: number; value: T; diff --git a/src/xterm.css b/src/xterm.css index 8f78741d..54f53316 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -181,6 +181,18 @@ left: -9999em; } +.terminal .xterm-selection { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.terminal .xterm-selection div { + background-color: #777; +} + /* * Determine default colors for xterm.js */ diff --git a/src/xterm.js b/src/xterm.js index 3b340b6f..8aa51b73 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -642,6 +642,12 @@ Terminal.prototype.open = function(parent, focus) { this.viewportScrollArea.classList.add('xterm-scroll-area'); this.viewportElement.appendChild(this.viewportScrollArea); + // Create the selection container. This needs to be added before the + // rowContainer as the selection must be below the text. + this.selectionContainer = document.createElement('div'); + this.selectionContainer.classList.add('xterm-selection'); + this.element.appendChild(this.selectionContainer); + // Create the container that will hold the lines of the terminal and then // produce the lines the lines. this.rowContainer = document.createElement('div'); @@ -691,7 +697,7 @@ Terminal.prototype.open = function(parent, focus) { this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); this.renderer = new Renderer(this); - this.selectionManager = new SelectionManager(this.lines, this.rowContainer, this.charMeasure); + this.selectionManager = new SelectionManager(this.lines, this.rowContainer, this.selectionContainer, this.charMeasure); // Setup loop that draws to screen this.refresh(0, this.rows - 1); From b594407cd5e6befd619bfd10f5473e5bc22a214b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 19 May 2017 20:06:05 -0700 Subject: [PATCH 06/61] Tell renderer to draw selection --- src/Renderer.ts | 4 ++++ src/SelectionManager.ts | 7 ++++++- src/xterm.js | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Renderer.ts b/src/Renderer.ts index af95d51e..21d48328 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -318,6 +318,10 @@ export class Renderer { this._terminal.emit('refresh', {element: this._terminal.element, start: start, end: end}); }; + + public refreshSelection(start: [number, number], end: [number, number]) { + console.log('renderer, refresh:', start, end); + } } diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 3c99ef66..e0e5bdbc 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -4,9 +4,11 @@ import { CharMeasure } from './utils/CharMeasure'; import { CircularList } from './utils/CircularList'; +import { EventEmitter } from './EventEmitter'; import * as Mouse from './utils/Mouse'; -export class SelectionManager { +export class SelectionManager extends EventEmitter { + // TODO: Create a SelectionModel private _selectionStart: [number, number]; private _selectionEnd: [number, number]; @@ -18,6 +20,7 @@ export class SelectionManager { private _mouseMoveListener: EventListener; constructor(buffer: CircularList, rowContainer: HTMLElement, selectionContainer: HTMLElement, charMeasure: CharMeasure) { + super(); this._rowContainer = rowContainer; this._selectionContainer = selectionContainer; this._buffer = buffer; @@ -44,6 +47,8 @@ export class SelectionManager { * Redraws the selection. */ public refresh(): void { + // TODO: Figure out when to refresh the selection vs when to refresh the viewport + this.emit('refresh', { start: this._selectionStart, end: this._selectionEnd }); console.log(`Selection: Start: (${this._selectionStart[0]}, ${this._selectionStart[1]}), End: (${this._selectionEnd[0]}, ${this._selectionEnd[1]})`); this._selectionContainer.innerHTML = `


`; } diff --git a/src/xterm.js b/src/xterm.js index 8aa51b73..028debb6 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -698,6 +698,7 @@ Terminal.prototype.open = function(parent, focus) { this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); this.renderer = new Renderer(this); this.selectionManager = new SelectionManager(this.lines, this.rowContainer, this.selectionContainer, this.charMeasure); + this.selectionManager.on('refresh', data => this.renderer.refreshSelection(data.start, data.end)); // Setup loop that draws to screen this.refresh(0, this.rows - 1); From ad3ae67e70d57ce75a76a80189b5a4340ab35a1d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 19 May 2017 23:29:51 -0700 Subject: [PATCH 07/61] Get selection partially rendering --- src/Interfaces.ts | 2 ++ src/Renderer.ts | 62 +++++++++++++++++++++++++++++++++++++++++ src/SelectionManager.ts | 31 +++++++++++---------- src/xterm.css | 1 + src/xterm.js | 6 +++- 5 files changed, 87 insertions(+), 15 deletions(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index ca228ce0..2f2bef73 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -20,6 +20,8 @@ export interface IBrowser { export interface ITerminal { element: HTMLElement; rowContainer: HTMLElement; + selectionContainer: HTMLElement; + charMeasure: ICharMeasure; textarea: HTMLTextAreaElement; ybase: number; ydisp: number; diff --git a/src/Renderer.ts b/src/Renderer.ts index 21d48328..b78567d5 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -321,6 +321,68 @@ export class Renderer { public refreshSelection(start: [number, number], end: [number, number]) { console.log('renderer, refresh:', start, end); + + // Remove all selections + while (this._terminal.selectionContainer.children.length) { + this._terminal.selectionContainer.removeChild(this._terminal.selectionContainer.children[0]); + } + + // Selection does not exist + if (!start || !end) { + return; + } + + // Swap the start and end if necessary + if (start[1] > end[1] || (start[1] === end[1] && start[0] > end[0])) { + const temp = start; + start = end; + end = temp; + } + + // Translate from buffer position to viewport position + const viewportStartRow = start[1] - this._terminal.ydisp; + const viewportEndRow = end[1] - this._terminal.ydisp; + const viewportCappedStartRow = Math.max(viewportStartRow, 0); + const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1); + + // No need to draw the selection + if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) { + return; + } + + console.log('viewportStartRow', viewportCappedStartRow); + console.log('viewportEndRow', viewportCappedEndRow); + + // TODO: Only redraw selections when necessary + + // Create the selections + const documentFragment = document.createDocumentFragment(); + // Draw first row + const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0; + const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols; + documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol)); + // Draw middle rows + for (let i = viewportCappedStartRow + 1; i < viewportCappedEndRow; i++) { + documentFragment.appendChild(this._createSelectionElement(i, 0, this._terminal.cols)); + } + // Draw final row + if (viewportCappedStartRow !== viewportCappedEndRow) { + // Only draw viewportEndRow if it's not the same as viewporttartRow + const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols; + documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol)); + } + this._terminal.selectionContainer.appendChild(documentFragment); + } + + private _createSelectionElement(row: number, colStart: number, colEnd: number): HTMLElement { + const element = document.createElement('div'); + // TODO: Move into a generated