Merge branch 'master' into extract_vt_features

This commit is contained in:
jerch
2019-08-23 20:37:29 +02:00
committed by GitHub
13 changed files with 159 additions and 76 deletions
+25
View File
@@ -224,6 +224,31 @@ describe('Terminal', () => {
});
});
describe('paste', () => {
it('should fire data event', done => {
term.onData(e => {
assert.equal(e, 'foo');
done();
});
term.paste('foo');
});
it('should sanitize \n chars', done => {
term.onData(e => {
assert.equal(e, '\rfoo\rbar\r');
done();
});
term.paste('\r\nfoo\nbar\r');
});
it('should respect bracketed paste mode', done => {
term.onData(e => {
assert.equal(e, '\x1b[200~foo\x1b[201~');
done();
});
term.write('\x1b[?2004h');
term.paste('foo');
});
});
describe('scroll', () => {
describe('scrollLines', () => {
let startYDisp: number;
+12 -6
View File
@@ -25,7 +25,7 @@ import { IInputHandlingTerminal, ICompositionHelper, ITerminalOptions, ITerminal
import { IRenderer, CharacterJoinerHandler } from 'browser/renderer/Types';
import { CompositionHelper } from 'browser/input/CompositionHelper';
import { Viewport } from 'browser/Viewport';
import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from 'browser/Clipboard';
import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard';
import { C0 } from 'common/data/EscapeSequences';
import { InputHandler } from './InputHandler';
import { Renderer } from './renderer/Renderer';
@@ -498,7 +498,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
}
copyHandler(event, this._selectionService);
}));
const pasteHandlerWrapper = (event: ClipboardEvent) => pasteHandler(event, this.textarea, this.bracketedPasteMode, e => this._coreService.triggerDataEvent(e, true));
const pasteHandlerWrapper = (event: ClipboardEvent) => handlePasteEvent(event, this.textarea, this.bracketedPasteMode, this._coreService);
this.register(addDisposableDomListener(this.textarea, 'paste', pasteHandlerWrapper));
this.register(addDisposableDomListener(this.element, 'paste', pasteHandlerWrapper));
@@ -1037,8 +1037,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
// the shell for example
this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => {
if (this.mouseEvents) return;
this.viewport.onWheel(ev);
return this.cancel(ev);
if (!this.viewport.onWheel(ev)) {
return this.cancel(ev);
}
}));
this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => {
@@ -1049,8 +1050,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => {
if (this.mouseEvents) return;
this.viewport.onTouchMove(ev);
return this.cancel(ev);
if (!this.viewport.onTouchMove(ev)) {
return this.cancel(ev);
}
}));
}
@@ -1387,6 +1389,10 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this.write(data + '\r\n');
}
public paste(data: string): void {
paste(data, this.textarea, this.bracketedPasteMode, this._coreService);
}
/**
* Attaches a custom key event handler which is run before keys are processed,
* giving consumers of xterm.js ultimate control as to what keys should be
+5 -2
View File
@@ -68,6 +68,9 @@ export class MockTerminal implements ITerminal {
writeln(data: string): void {
throw new Error('Method not implemented.');
}
paste(data: string): void {
throw new Error('Method not implemented.');
}
open(parent: HTMLElement): void {
throw new Error('Method not implemented.');
}
@@ -407,13 +410,13 @@ export class MockViewport implements IViewport {
onThemeChange(colors: IColorSet): void {
throw new Error('Method not implemented.');
}
onWheel(ev: WheelEvent): void {
onWheel(ev: WheelEvent): boolean {
throw new Error('Method not implemented.');
}
onTouchStart(ev: TouchEvent): void {
throw new Error('Method not implemented.');
}
onTouchMove(ev: TouchEvent): void {
onTouchMove(ev: TouchEvent): boolean {
throw new Error('Method not implemented.');
}
syncScrollArea(): void { }
+1
View File
@@ -226,6 +226,7 @@ export interface IPublicTerminal extends IDisposable {
clear(): void;
write(data: string): void;
writeUtf8(data: Uint8Array): void;
paste(data: string): void;
refresh(start: number, end: number): void;
reset(): void;
}
+11 -13
View File
@@ -4,6 +4,7 @@
*/
import { ISelectionService } from 'browser/services/Services';
import { ICoreService } from 'common/services/Services';
/**
* Prepares text to be pasted into the terminal by normalizing the line endings
@@ -41,24 +42,21 @@ export function copyHandler(ev: ClipboardEvent, selectionService: ISelectionServ
* @param ev The original paste event to be handled
* @param term The terminal on which to apply the handled paste event
*/
export function pasteHandler(ev: ClipboardEvent, textarea: HTMLTextAreaElement, bracketedPasteMode: boolean, triggerUserInput: (data: string) => void): void {
export function handlePasteEvent(ev: ClipboardEvent, textarea: HTMLTextAreaElement, bracketedPasteMode: boolean, coreService: ICoreService): void {
ev.stopPropagation();
let text: string;
const dispatchPaste = function(text: string): void {
text = prepareTextForTerminal(text);
text = bracketTextForPaste(text, bracketedPasteMode);
triggerUserInput(text);
textarea.value = '';
};
if (ev.clipboardData) {
text = ev.clipboardData.getData('text/plain');
dispatchPaste(text);
const text = ev.clipboardData.getData('text/plain');
paste(text, textarea, bracketedPasteMode, coreService);
}
}
export function paste(text: string, textarea: HTMLTextAreaElement, bracketedPasteMode: boolean, coreService: ICoreService): void {
text = prepareTextForTerminal(text);
text = bracketTextForPaste(text, bracketedPasteMode);
coreService.triggerDataEvent(text, true);
textarea.value = '';
}
/**
* Moves the textarea under the mouse cursor and focuses it.
* @param ev The original right click event to be handled.
+2 -2
View File
@@ -37,9 +37,9 @@ export interface IViewport extends IDisposable {
scrollBarWidth: number;
syncScrollArea(): void;
getLinesScrolled(ev: WheelEvent): number;
onWheel(ev: WheelEvent): void;
onWheel(ev: WheelEvent): boolean;
onTouchStart(ev: TouchEvent): void;
onTouchMove(ev: TouchEvent): void;
onTouchMove(ev: TouchEvent): boolean;
onThemeChange(colors: IColorSet): void;
}
+23 -7
View File
@@ -152,20 +152,36 @@ export class Viewport extends Disposable implements IViewport {
this._scrollLines(diff, true);
}
/**
* Handles bubbling of scroll event in case the viewport has reached top or bottom
* @param ev The scroll event.
* @param amount The amount scrolled
*/
private _bubbleScroll(ev: Event, amount: number): boolean {
const scrollPosFromTop = this._viewportElement.scrollTop + this._lastRecordedViewportHeight;
if ((amount < 0 && this._viewportElement.scrollTop !== 0) ||
(amount > 0 && scrollPosFromTop < this._lastRecordedBufferHeight)) {
if (ev.cancelable) {
ev.preventDefault();
}
return false;
}
return true;
}
/**
* Handles mouse wheel events by adjusting the viewport's scrollTop and delegating the actual
* scrolling to `onScroll`, this event needs to be attached manually by the consumer of
* `Viewport`.
* @param ev The mouse wheel event.
*/
public onWheel(ev: WheelEvent): void {
public onWheel(ev: WheelEvent): boolean {
const amount = this._getPixelsScrolled(ev);
if (amount === 0) {
return;
return false;
}
this._viewportElement.scrollTop += amount;
// Prevent the page from scrolling when the terminal scrolls
ev.preventDefault();
return this._bubbleScroll(ev, amount);
}
private _getPixelsScrolled(ev: WheelEvent): number {
@@ -220,13 +236,13 @@ export class Viewport extends Disposable implements IViewport {
* Handles the touchmove event, scrolling the viewport if the position shifted.
* @param ev The touch event.
*/
public onTouchMove(ev: TouchEvent): void {
public onTouchMove(ev: TouchEvent): boolean {
const deltaY = this._lastTouchY - ev.touches[0].pageY;
this._lastTouchY = ev.touches[0].pageY;
if (deltaY === 0) {
return;
return false;
}
this._viewportElement.scrollTop += deltaY;
ev.preventDefault();
return this._bubbleScroll(ev, deltaY);
}
}
+1 -5
View File
@@ -543,20 +543,16 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
break;
case ParserAction.PARAM:
// inner loop: digits (0x30 - 0x39) and ; (0x3b) and : (0x3a)
let isSub = false;
do {
switch (code) {
case 0x3b:
params.addParam(0); // ZDM
isSub = false;
break;
case 0x3a:
params.addSubParam(-1);
isSub = true;
break;
default: // 0x30 - 0x39
if (isSub) params.addSubParamDigit(code - 48);
else params.addParamDigit(code - 48);
params.addDigit(code - 48);
}
} while (++i < length && (code = data[i]) > 0x2f && code < 0x3c);
i--;
+36 -19
View File
@@ -16,27 +16,29 @@ class TestParams extends Params {
}
/** `Params` parser shim */
function parse(params: Params, s: string): void {
function parse(params: Params, s: string | string[]): void {
params.reset();
params.addParam(0);
let isSub = false;
for (let i = 0; i < s.length; ++i) {
let code = s.charCodeAt(i);
do {
switch (code) {
case 0x3b:
params.addParam(0);
isSub = false;
break;
case 0x3a:
params.addSubParam(-1);
isSub = true;
break;
default: // 0x30 - 0x39
if (isSub) params.addSubParamDigit(code - 48);
else params.addParamDigit(code - 48);
}
} while (++i < s.length && (code = s.charCodeAt(i)) > 0x2f && code < 0x3c);
if (typeof s === 'string') {
s = [s];
}
for (const chunk of s) {
for (let i = 0; i < chunk.length; ++i) {
let code = chunk.charCodeAt(i);
do {
switch (code) {
case 0x3b:
params.addParam(0);
break;
case 0x3a:
params.addSubParam(-1);
break;
default: // 0x30 - 0x39
params.addDigit(code - 48);
}
} while (++i < s.length && (code = chunk.charCodeAt(i)) > 0x2f && code < 0x3c);
i--;
}
}
}
@@ -217,4 +219,19 @@ describe('Params', () => {
assert.deepEqual(params.toArray(), [0, [0x7FFFFFFF]]);
});
});
describe('issue 2389', () => {
it('should cancel subdigits if beyond params limit', () => {
const params = new Params();
parse(params, ';;;;;;;;;10;;;;;;;;;;20;;;;;;;;;;30;31;32;33;34;35::::::::');
assert.deepEqual(params.toArray(), [
0, 0, 0, 0, 0, 0, 0, 0, 0, 10,
0, 0, 0, 0, 0, 0, 0, 0, 0, 20,
0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 31, 32]);
});
it('should carry forward isSub state', () => {
const params = new Params();
parse(params, ['1:22:33', '44']);
assert.deepEqual(params.toArray(), [1, [22, 3344]]);
});
});
});
+18 -22
View File
@@ -41,6 +41,7 @@ export class Params implements IParams {
private _subParamsIdx: Uint16Array;
private _rejectDigits: boolean;
private _rejectSubDigits: boolean;
private _digitIsSub: boolean;
/**
* Create a `Params` type from JS array representation.
@@ -79,6 +80,7 @@ export class Params implements IParams {
this._subParamsIdx = new Uint16Array(maxLength);
this._rejectDigits = false;
this._rejectSubDigits = false;
this._digitIsSub = false;
}
/**
@@ -91,6 +93,9 @@ export class Params implements IParams {
newParams._subParams.set(this._subParams);
newParams._subParamsLength = this._subParamsLength;
newParams._subParamsIdx.set(this._subParamsIdx);
newParams._rejectDigits = this._rejectDigits;
newParams._rejectSubDigits = this._rejectSubDigits;
newParams._digitIsSub = this._digitIsSub;
return newParams;
}
@@ -121,6 +126,7 @@ export class Params implements IParams {
this._subParamsLength = 0;
this._rejectDigits = false;
this._rejectSubDigits = false;
this._digitIsSub = false;
}
/**
@@ -131,6 +137,7 @@ export class Params implements IParams {
* store up to 30.
*/
public addParam(value: number): void {
this._digitIsSub = false;
if (this.length >= this.maxLength) {
this._rejectDigits = true;
return;
@@ -150,10 +157,11 @@ export class Params implements IParams {
* sub parameter will be ignored.
*/
public addSubParam(value: number): void {
this._digitIsSub = true;
if (!this.length) {
return;
}
if (this._subParamsLength >= this.maxSubParamsLength) {
if (this._rejectDigits || this._subParamsLength >= this.maxSubParamsLength) {
this._rejectSubDigits = true;
return;
}
@@ -204,30 +212,18 @@ export class Params implements IParams {
/**
* Add a single digit value to current parameter.
* This is used by the parser to account digits on a char by char basis.
* Do not use this method directly, consider using `addParam` instead.
*/
public addParamDigit(value: number): void {
if (this._rejectDigits) {
public addDigit(value: number): void {
let length;
if (this._rejectDigits
|| !(length = this._digitIsSub ? this._subParamsLength : this.length)
|| (this._digitIsSub && this._rejectSubDigits)
) {
return;
}
const v = this.params[this.length - 1] * 10 + value;
this.params[this.length - 1] = v > MAX_VALUE ? MAX_VALUE : v;
}
/**
* Add a single digit value to current sub parameter.
* This is used by the parser to account digits on a char by char basis.
* Do not use this method directly, consider using `addSubParam` instead.
*/
public addSubParamDigit(value: number): void {
if (!this._subParamsLength || this._rejectDigits || this._rejectSubDigits) {
return;
}
if (this._subParams[this._subParamsLength - 1] === -1) {
this._subParams[this._subParamsLength - 1] = value;
} else {
const v = this._subParams[this._subParamsLength - 1] * 10 + value;
this._subParams[this._subParamsLength - 1] = v > MAX_VALUE ? MAX_VALUE : v;
}
const store = this._digitIsSub ? this._subParams : this.params;
const cur = store[length - 1];
store[length - 1] = ~cur ? Math.min(cur * 10 + value, MAX_VALUE) : value;
}
}
+3
View File
@@ -134,6 +134,9 @@ export class Terminal implements ITerminalApi {
public writeUtf8(data: Uint8Array): void {
this._core.writeUtf8(data);
}
public paste(data: string): void {
this._core.paste(data);
}
public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold' | 'logLevel' | 'rendererType' | 'termName' | 'wordSeparator'): string;
public getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean;
public getOption(key: 'colors'): string[];
+16
View File
@@ -76,6 +76,22 @@ describe('API Integration Tests', function(): void {
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文');
});
it('paste', async function(): Promise<any> {
await openTerminal();
await page.evaluate(`
window.calls = [];
window.term.onData(e => calls.push(e));
window.term.paste('foo');
window.term.paste('\\r\\nfoo\\nbar\\r');
window.term.write('\\x1b[?2004h');
// TODO: Use promise/callback for write when we support that
// Force sync write
window.term._core._innerWrite();
window.term.paste('foo');
`);
assert.deepEqual(await page.evaluate(`window.calls`), ['foo', '\rfoo\rbar\r', '\x1b[200~foo\x1b[201~']);
});
it('clear', async function(): Promise<any> {
await openTerminal({ rows: 5 });
await page.evaluate(`
+6
View File
@@ -669,6 +669,12 @@ declare module 'xterm' {
*/
writeUtf8(data: Uint8Array): void;
/**
* Writes text to the terminal, performing the necessary transformations for pasted text.
* @param data The text to write to the terminal.
*/
paste(data: string): void;
/**
* Retrieves an option's value from the terminal.
* @param key The option key.