Merge branch 'master' into local_fonts
This commit is contained in:
@@ -41,7 +41,9 @@ export class AttachAddon implements ITerminalAddon {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._disposables.forEach(d => d.dispose());
|
||||
for (const d of this._disposables) {
|
||||
d.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private _sendData(data: string): void {
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('AttachAddon', () => {
|
||||
before(async function(): Promise<any> {
|
||||
const browserType = getBrowserType();
|
||||
browser = await browserType.launch({
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
headless: process.argv.includes('--headless')
|
||||
});
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('FitAddon', () => {
|
||||
before(async function(): Promise<any> {
|
||||
const browserType = getBrowserType();
|
||||
browser = await browserType.launch({
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
headless: process.argv.includes('--headless')
|
||||
});
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
@@ -45,7 +45,7 @@ describe('FitAddon', () => {
|
||||
|
||||
describe('proposeDimensions', () => {
|
||||
afterEach(async () => {
|
||||
return unloadFit();
|
||||
return await unloadFit();
|
||||
});
|
||||
|
||||
it('default', async function(): Promise<any> {
|
||||
@@ -84,7 +84,7 @@ describe('FitAddon', () => {
|
||||
|
||||
describe('fit', () => {
|
||||
afterEach(async () => {
|
||||
return unloadFit();
|
||||
return await unloadFit();
|
||||
});
|
||||
|
||||
it('default', async function(): Promise<any> {
|
||||
|
||||
@@ -68,7 +68,7 @@ function parseString(context: IParseContext, quoteChar: '\'' | '"'): string {
|
||||
while (context.offset < context.input.length) {
|
||||
const char = context.input[context.offset++];
|
||||
if (escaped) {
|
||||
if (/[0-9a-fA-F]/.test(char)) {
|
||||
if (/[\dA-Fa-f]/.test(char)) {
|
||||
// Unicode escape
|
||||
context.offset--;
|
||||
str += parseUnicode(context);
|
||||
@@ -107,7 +107,7 @@ function parseIdentifier(context: IParseContext): string {
|
||||
while (context.offset < context.input.length) {
|
||||
const char = context.input[context.offset++];
|
||||
if (escaped) {
|
||||
if (/[0-9a-fA-F]/.test(char)) {
|
||||
if (/[\dA-Fa-f]/.test(char)) {
|
||||
// Unicode escape
|
||||
context.offset--;
|
||||
str += parseUnicode(context);
|
||||
@@ -156,7 +156,7 @@ function parseUnicode(context: IParseContext): string {
|
||||
// of the escape and is swallowed.
|
||||
return unicodeToString(str);
|
||||
}
|
||||
if (str.length >= 6 || !/[0-9a-fA-F]/.test(char)) {
|
||||
if (str.length >= 6 || !/[\dA-Fa-f]/.test(char)) {
|
||||
// If the next character is not a valid hex digit or we have reached the
|
||||
// maximum of 6 digits in the escape, terminate the escape.
|
||||
context.offset--;
|
||||
|
||||
@@ -240,8 +240,8 @@ export class SearchAddon implements ITerminalAddon {
|
||||
* @param term the substring that starts at searchIndex
|
||||
*/
|
||||
private _isWholeWord(searchIndex: number, line: string, term: string): boolean {
|
||||
return (((searchIndex === 0) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex - 1]) !== -1)) &&
|
||||
(((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex + term.length]) !== -1)));
|
||||
return ((searchIndex === 0) || (NON_WORD_CHARACTERS.includes(line[searchIndex - 1]))) &&
|
||||
(((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.includes(line[searchIndex + term.length])));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -262,7 +262,7 @@ export class SearchAddon implements ITerminalAddon {
|
||||
|
||||
// Ignore wrapped lines, only consider on unwrapped line (first row of command string).
|
||||
const firstLine = terminal.buffer.active.getLine(row);
|
||||
if (firstLine && firstLine.isWrapped) {
|
||||
if (firstLine?.isWrapped) {
|
||||
if (isReverseSearch) {
|
||||
searchPosition.startCol += terminal.cols;
|
||||
return;
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('Search Tests', function(): void {
|
||||
before(async function(): Promise<any> {
|
||||
const browserType = getBrowserType();
|
||||
browser = await browserType.launch({
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
headless: process.argv.includes('--headless')
|
||||
});
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('SerializeAddon', () => {
|
||||
before(async function(): Promise<any> {
|
||||
const browserType = getBrowserType();
|
||||
browser = await browserType.launch({
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
headless: process.argv.includes('--headless')
|
||||
});
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('Unicode11Addon', () => {
|
||||
before(async function(): Promise<any> {
|
||||
const browserType = getBrowserType();
|
||||
browser = await browserType.launch({
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
headless: process.argv.includes('--headless')
|
||||
});
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
|
||||
@@ -41,7 +41,7 @@ export class WebLinkProvider implements ILinkProvider {
|
||||
}
|
||||
|
||||
export class LinkComputer {
|
||||
public static computeLink(y: number, regex: RegExp, terminal: Terminal, handler: (event: MouseEvent, uri: string) => void): ILink[] {
|
||||
public static computeLink(y: number, regex: RegExp, terminal: Terminal, activate: (event: MouseEvent, uri: string) => void): ILink[] {
|
||||
const rex = new RegExp(regex.source, (regex.flags || '') + 'g');
|
||||
|
||||
const [line, startLineIndex] = LinkComputer._translateBufferLineToStringWithWrap(y - 1, false, terminal);
|
||||
@@ -89,7 +89,7 @@ export class LinkComputer {
|
||||
}
|
||||
};
|
||||
|
||||
result.push({ range, text, activate: handler });
|
||||
result.push({ range, text, activate });
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('WebLinksAddon', () => {
|
||||
before(async function(): Promise<any> {
|
||||
const browserType = getBrowserType();
|
||||
browser = await browserType.launch({
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
headless: process.argv.includes('--headless')
|
||||
});
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
## xterm-addon-webgl
|
||||
|
||||
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a WebGL-based renderer. This addon requires xterm.js v4+.
|
||||
|
||||
⚠️ This is an experimental addon that is [missing some features and may be unstable](https://github.com/xtermjs/xterm.js/issues?q=is%3Aopen+is%3Aissue+label%3Aarea%2Faddon%2Fwebgl) ⚠️
|
||||
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a WebGL2-based renderer. This addon requires xterm.js v4+.
|
||||
|
||||
### Install
|
||||
|
||||
@@ -21,3 +19,18 @@ terminal.loadAddon(new WebglAddon());
|
||||
```
|
||||
|
||||
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts) for more advanced usage.
|
||||
|
||||
### Handling Context Loss
|
||||
|
||||
The browser may drop WebGL contexts for various reasons like OOM or after the system has been suspended. There is an API exposed that fires the `webglcontextlost` event fired on the canvas so embedders can handle it however they wish. An easy, but suboptimal way, to handle this is by disposing of WebglAddon when the event fires:
|
||||
|
||||
```ts
|
||||
const terminal = new Terminal();
|
||||
const addon = new WebglAddon();
|
||||
addon.onContextLoss(e => {
|
||||
addon.dispose();
|
||||
});
|
||||
terminal.loadAddon(addon);
|
||||
```
|
||||
|
||||
Read more about handling WebGL context losses on the [Khronos wiki](https://www.khronos.org/webgl/wiki/HandlingContextLost).
|
||||
|
||||
@@ -3,14 +3,17 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon } from 'xterm';
|
||||
import { Terminal, ITerminalAddon, IEvent } from 'xterm';
|
||||
import { WebglRenderer } from './WebglRenderer';
|
||||
import { IRenderService } from 'browser/services/Services';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
|
||||
export class WebglAddon implements ITerminalAddon {
|
||||
private _terminal?: Terminal;
|
||||
private _renderer?: WebglRenderer;
|
||||
private _onContextLoss = new EventEmitter<void>();
|
||||
public get onContextLoss(): IEvent<void> { return this._onContextLoss.event; }
|
||||
|
||||
constructor(
|
||||
private _preserveDrawingBuffer?: boolean
|
||||
@@ -24,6 +27,7 @@ export class WebglAddon implements ITerminalAddon {
|
||||
const renderService: IRenderService = (<any>terminal)._core._renderService;
|
||||
const colors: IColorSet = (<any>terminal)._core._colorManager.colors;
|
||||
this._renderer = new WebglRenderer(terminal, colors, this._preserveDrawingBuffer);
|
||||
this._renderer.onContextLoss(() => this._onContextLoss.fire());
|
||||
renderService.setRenderer(this._renderer);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/rende
|
||||
import { ITerminal, IColorSet } from 'browser/Types';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
|
||||
export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _renderLayers: IRenderLayer[];
|
||||
@@ -41,6 +42,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _onRequestRedraw = new EventEmitter<IRequestRedrawEvent>();
|
||||
public get onRequestRedraw(): IEvent<IRequestRedrawEvent> { return this._onRequestRedraw.event; }
|
||||
|
||||
private _onContextLoss = new EventEmitter<void>();
|
||||
public get onContextLoss(): IEvent<void> { return this._onContextLoss.event; }
|
||||
|
||||
constructor(
|
||||
private _terminal: Terminal,
|
||||
private _colors: IColorSet,
|
||||
@@ -82,6 +86,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
if (!this._gl) {
|
||||
throw new Error('WebGL2 not supported ' + this._gl);
|
||||
}
|
||||
|
||||
this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => { this._onContextLoss.fire(e); }));
|
||||
|
||||
this._core.screenElement!.appendChild(this._canvas);
|
||||
|
||||
this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions);
|
||||
@@ -94,7 +101,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._renderLayers.forEach(l => l.dispose());
|
||||
for (const l of this._renderLayers) {
|
||||
l.dispose();
|
||||
}
|
||||
this._core.screenElement!.removeChild(this._canvas);
|
||||
super.dispose();
|
||||
}
|
||||
@@ -106,10 +115,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
public setColors(colors: IColorSet): void {
|
||||
this._colors = colors;
|
||||
// Clear layers and force a full render
|
||||
this._renderLayers.forEach(l => {
|
||||
for (const l of this._renderLayers) {
|
||||
l.setColors(this._terminal, this._colors);
|
||||
l.reset(this._terminal);
|
||||
});
|
||||
}
|
||||
|
||||
this._rectangleRenderer.setColors();
|
||||
this._glyphRenderer.setColors();
|
||||
@@ -136,7 +145,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._model.resize(this._terminal.cols, this._terminal.rows);
|
||||
|
||||
// Resize all render layers
|
||||
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions));
|
||||
for (const l of this._renderLayers) {
|
||||
l.resize(this._terminal, this.dimensions);
|
||||
}
|
||||
|
||||
// Resize the canvas
|
||||
this._canvas.width = this.dimensions.scaledCanvasWidth;
|
||||
@@ -168,15 +179,21 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
public onBlur(): void {
|
||||
this._renderLayers.forEach(l => l.onBlur(this._terminal));
|
||||
for (const l of this._renderLayers) {
|
||||
l.onBlur(this._terminal);
|
||||
}
|
||||
}
|
||||
|
||||
public onFocus(): void {
|
||||
this._renderLayers.forEach(l => l.onFocus(this._terminal));
|
||||
for (const l of this._renderLayers) {
|
||||
l.onFocus(this._terminal);
|
||||
}
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
this._renderLayers.forEach(l => l.onSelectionChanged(this._terminal, start, end, columnSelectMode));
|
||||
for (const l of this._renderLayers) {
|
||||
l.onSelectionChanged(this._terminal, start, end, columnSelectMode);
|
||||
}
|
||||
|
||||
this._updateSelectionModel(start, end, columnSelectMode);
|
||||
|
||||
@@ -184,11 +201,15 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
this._renderLayers.forEach(l => l.onCursorMove(this._terminal));
|
||||
for (const l of this._renderLayers) {
|
||||
l.onCursorMove(this._terminal);
|
||||
}
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
this._renderLayers.forEach(l => l.onOptionsChanged(this._terminal));
|
||||
for (const l of this._renderLayers) {
|
||||
l.onOptionsChanged(this._terminal);
|
||||
}
|
||||
this._updateDimensions();
|
||||
this._refreshCharAtlas();
|
||||
}
|
||||
@@ -222,7 +243,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._renderLayers.forEach(l => l.reset(this._terminal));
|
||||
for (const l of this._renderLayers) {
|
||||
l.reset(this._terminal);
|
||||
}
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
||||
@@ -245,7 +268,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
// Update render layers
|
||||
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
|
||||
for (const l of this._renderLayers) {
|
||||
l.onGridChanged(this._terminal, start, end);
|
||||
}
|
||||
|
||||
// Tell renderer the frame is beginning
|
||||
if (this._glyphRenderer.beginFrame()) {
|
||||
|
||||
@@ -358,7 +358,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
const fontStyle = italic ? 'italic' : '';
|
||||
this._tmpCtx.font =
|
||||
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
|
||||
this._tmpCtx.textBaseline = 'middle';
|
||||
this._tmpCtx.textBaseline = 'ideographic';
|
||||
|
||||
this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold);
|
||||
|
||||
@@ -367,8 +367,22 @@ export class WebglCharAtlas implements IDisposable {
|
||||
this._tmpCtx.globalAlpha = DIM_OPACITY;
|
||||
}
|
||||
|
||||
// Check if the char is a powerline glyph, these will be restricted to a single cell glyph, no
|
||||
// padding on either side that are allowed for other glyphs since they are designed to be pixel
|
||||
// perfect but may render with "bad" anti-aliasing
|
||||
let isPowerlineGlyph = false;
|
||||
if (chars.length === 1) {
|
||||
const code = chars.charCodeAt(0);
|
||||
if (code >= 0xE0A0 && code <= 0xE0D6) {
|
||||
isPowerlineGlyph = true;
|
||||
}
|
||||
}
|
||||
|
||||
// For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129)
|
||||
const padding = isPowerlineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING;
|
||||
|
||||
// Draw the character
|
||||
this._tmpCtx.fillText(chars, TMP_CANVAS_GLYPH_PADDING, TMP_CANVAS_GLYPH_PADDING + this._config.scaledCharHeight / 2);
|
||||
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight);
|
||||
this._tmpCtx.restore();
|
||||
|
||||
// clear the background from the character to avoid issues with drawing over the previous
|
||||
@@ -391,7 +405,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
return NULL_RASTERIZED_GLYPH;
|
||||
}
|
||||
|
||||
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox);
|
||||
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, isPowerlineGlyph);
|
||||
const clippedImageData = this._clipImageData(imageData, this._workBoundingBox);
|
||||
|
||||
// Check if there is enough room in the current row and go to next if needed
|
||||
@@ -424,12 +438,14 @@ export class WebglCharAtlas implements IDisposable {
|
||||
* @param imageData The image data to read.
|
||||
* @param boundingBox An IBoundingBox to put the clipped bounding box values.
|
||||
*/
|
||||
private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox): IRasterizedGlyph {
|
||||
private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, restrictedGlyph: boolean): IRasterizedGlyph {
|
||||
boundingBox.top = 0;
|
||||
const height = restrictedGlyph ? this._config.scaledCharHeight : this._tmpCanvas.height;
|
||||
const width = restrictedGlyph ? this._config.scaledCharWidth : this._tmpCanvas.width;
|
||||
let found = false;
|
||||
for (let y = 0; y < this._tmpCanvas.height; y++) {
|
||||
for (let x = 0; x < this._tmpCanvas.width; x++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const alphaOffset = y * width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.top = y;
|
||||
found = true;
|
||||
@@ -442,9 +458,9 @@ export class WebglCharAtlas implements IDisposable {
|
||||
}
|
||||
boundingBox.left = 0;
|
||||
found = false;
|
||||
for (let x = 0; x < this._tmpCanvas.width; x++) {
|
||||
for (let y = 0; y < this._tmpCanvas.height; y++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
for (let x = 0; x < width; x++) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
const alphaOffset = y * width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.left = x;
|
||||
found = true;
|
||||
@@ -455,11 +471,11 @@ export class WebglCharAtlas implements IDisposable {
|
||||
break;
|
||||
}
|
||||
}
|
||||
boundingBox.right = this._tmpCanvas.width;
|
||||
boundingBox.right = width;
|
||||
found = false;
|
||||
for (let x = this._tmpCanvas.width - 1; x >= 0; x--) {
|
||||
for (let y = 0; y < this._tmpCanvas.height; y++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
for (let x = width - 1; x >= 0; x--) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
const alphaOffset = y * width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.right = x;
|
||||
found = true;
|
||||
@@ -470,11 +486,11 @@ export class WebglCharAtlas implements IDisposable {
|
||||
break;
|
||||
}
|
||||
}
|
||||
boundingBox.bottom = this._tmpCanvas.height;
|
||||
boundingBox.bottom = height;
|
||||
found = false;
|
||||
for (let y = this._tmpCanvas.height - 1; y >= 0; y--) {
|
||||
for (let x = 0; x < this._tmpCanvas.width; x++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
for (let y = height - 1; y >= 0; y--) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const alphaOffset = y * width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.bottom = y;
|
||||
found = true;
|
||||
@@ -497,8 +513,8 @@ export class WebglCharAtlas implements IDisposable {
|
||||
y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT
|
||||
},
|
||||
offset: {
|
||||
x: -boundingBox.left + TMP_CANVAS_GLYPH_PADDING,
|
||||
y: -boundingBox.top + TMP_CANVAS_GLYPH_PADDING
|
||||
x: -boundingBox.left + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING),
|
||||
y: -boundingBox.top + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -224,12 +224,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
protected _fillCharTrueColor(terminal: Terminal, cell: CellData, x: number, y: number): void {
|
||||
this._ctx.font = this._getFont(terminal, false, false);
|
||||
this._ctx.textBaseline = 'middle';
|
||||
this._ctx.textBaseline = 'ideographic';
|
||||
this._clipRow(terminal, y);
|
||||
this._ctx.fillText(
|
||||
cell.getChars(),
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,7 @@ const height = 600;
|
||||
|
||||
describe('WebGL Renderer Integration Tests', async () => {
|
||||
const browserType = getBrowserType();
|
||||
const isHeadless = process.argv.indexOf('--headless') !== -1;
|
||||
const isHeadless = process.argv.includes('--headless');
|
||||
// Firefox works only in non-headless mode https://github.com/microsoft/playwright/issues/1032
|
||||
const areTestsEnabled = browserType.name() === 'chromium' || (browserType.name() === 'firefox' && !isHeadless);
|
||||
const itWebgl = areTestsEnabled ? it : it.skip;
|
||||
@@ -893,7 +893,7 @@ async function getCellColor(col: number, row: number): Promise<number[]> {
|
||||
async function setupBrowser(options: ITerminalOptions = { rendererType: 'dom' }): Promise<void> {
|
||||
const browserType = getBrowserType();
|
||||
browser = await browserType.launch({
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
headless: process.argv.includes('--headless')
|
||||
});
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IEvent } from 'node-pty';
|
||||
import { Terminal, ITerminalAddon } from 'xterm';
|
||||
|
||||
declare module 'xterm-addon-webgl' {
|
||||
@@ -29,5 +30,10 @@ declare module 'xterm-addon-webgl' {
|
||||
* Clears the terminal's texture atlas and triggers a redraw.
|
||||
*/
|
||||
public clearTextureAtlas(): void;
|
||||
|
||||
/**
|
||||
* Fired when the WebglRenderer loses context
|
||||
*/
|
||||
public get onContextLoss(): IEvent<void>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, ILinkDecorations } from 'browser/Types';
|
||||
import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, ILinkDecorations, ILinkWithState } from 'browser/Types';
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { IMouseService, IRenderService } from './services/Services';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
@@ -11,21 +11,12 @@ import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { Disposable, getDisposeArrayDisposable, disposeArray } from 'common/Lifecycle';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
|
||||
interface ILinkState {
|
||||
decorations: ILinkDecorations;
|
||||
isHovered: boolean;
|
||||
}
|
||||
|
||||
interface ILinkWithState {
|
||||
link: ILink;
|
||||
state?: ILinkState;
|
||||
}
|
||||
|
||||
export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
private _element: HTMLElement | undefined;
|
||||
private _mouseService: IMouseService | undefined;
|
||||
private _renderService: IRenderService | undefined;
|
||||
private _linkProviders: ILinkProvider[] = [];
|
||||
public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
|
||||
protected _currentLink: ILinkWithState | undefined;
|
||||
private _lastMouseEvent: MouseEvent | undefined;
|
||||
private _linkCacheDisposables: IDisposable[] = [];
|
||||
|
||||
@@ -156,9 +156,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
private _onTooltip(e: MouseEvent): void {
|
||||
this._tooltipTimeout = undefined;
|
||||
const zone = this._findZoneEventAt(e);
|
||||
if (zone && zone.tooltipCallback) {
|
||||
zone.tooltipCallback(e);
|
||||
}
|
||||
zone?.tooltipCallback(e);
|
||||
}
|
||||
|
||||
private _onMouseDown(e: MouseEvent): void {
|
||||
|
||||
+37
-152
@@ -56,25 +56,25 @@ describe('Terminal', () => {
|
||||
// term.handler('fake');
|
||||
// });
|
||||
it('should fire the onCursorMove event', () => {
|
||||
return new Promise(async r => {
|
||||
return new Promise<void>(async r => {
|
||||
term.onCursorMove(() => r());
|
||||
await term.writeP('foo');
|
||||
});
|
||||
});
|
||||
it('should fire the onLineFeed event', () => {
|
||||
return new Promise(async r => {
|
||||
return new Promise<void>(async r => {
|
||||
term.onLineFeed(() => r());
|
||||
await term.writeP('\n');
|
||||
});
|
||||
});
|
||||
it('should fire a scroll event when scrollback is created', () => {
|
||||
return new Promise(async r => {
|
||||
return new Promise<void>(async r => {
|
||||
term.onScroll(() => r());
|
||||
await term.writeP('\n'.repeat(INIT_ROWS));
|
||||
});
|
||||
});
|
||||
it('should fire a scroll event when scrollback is cleared', () => {
|
||||
return new Promise(async r => {
|
||||
return new Promise<void>(async r => {
|
||||
await term.writeP('\n'.repeat(INIT_ROWS));
|
||||
term.onScroll(() => r());
|
||||
term.clear();
|
||||
@@ -233,7 +233,7 @@ describe('Terminal', () => {
|
||||
term.paste('\r\nfoo\nbar\r');
|
||||
});
|
||||
it('should respect bracketed paste mode', () => {
|
||||
return new Promise(async r => {
|
||||
return new Promise<void>(async r => {
|
||||
term.onData(e => {
|
||||
assert.equal(e, '\x1b[200~foo\x1b[201~');
|
||||
r();
|
||||
@@ -1054,10 +1054,10 @@ describe('Terminal', () => {
|
||||
linkifier.attachToDom({} as any, mouseZoneManager);
|
||||
});
|
||||
|
||||
function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[]): Promise<void> {
|
||||
function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: { x1: number, y1: number, x2: number, y2: number }[]): Promise<void> {
|
||||
return new Promise(async r => {
|
||||
await terminal.writeP(rowText);
|
||||
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
|
||||
linkifier.registerLinkMatcher(linkMatcherRegex, () => { });
|
||||
linkifier.linkifyRows();
|
||||
// Allow linkify to happen
|
||||
setTimeout(() => {
|
||||
@@ -1075,66 +1075,66 @@ describe('Terminal', () => {
|
||||
|
||||
describe('unicode before the match', () => {
|
||||
it('combining - match within one line', () => {
|
||||
return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}]);
|
||||
return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]);
|
||||
});
|
||||
it('combining - match over two lines', () => {
|
||||
return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]);
|
||||
return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]);
|
||||
});
|
||||
it('surrogate - match within one line', () => {
|
||||
return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}]);
|
||||
return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]);
|
||||
});
|
||||
it('surrogate - match over two lines', () => {
|
||||
return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]);
|
||||
return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]);
|
||||
});
|
||||
it('combining surrogate - match within one line', () => {
|
||||
return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}]);
|
||||
return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]);
|
||||
});
|
||||
it('combining surrogate - match over two lines', () => {
|
||||
return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]);
|
||||
return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]);
|
||||
});
|
||||
it('fullwidth - match within one line', () => {
|
||||
return assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}]);
|
||||
return assertLinkifiesInTerminal('12 foo', /foo/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]);
|
||||
});
|
||||
it('fullwidth - match over two lines', () => {
|
||||
return assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]);
|
||||
return assertLinkifiesInTerminal('12 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]);
|
||||
});
|
||||
it('combining fullwidth - match within one line', () => {
|
||||
return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}]);
|
||||
return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]);
|
||||
});
|
||||
it('combining fullwidth - match over two lines', () => {
|
||||
return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]);
|
||||
return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]);
|
||||
});
|
||||
});
|
||||
describe('unicode within the match', () => {
|
||||
it('combining - match within one line', () => {
|
||||
return assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{x1: 5, x2: 9, y1: 0, y2: 0}]);
|
||||
return assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]);
|
||||
});
|
||||
it('combining - match over two lines', () => {
|
||||
return assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{x1: 9, x2: 3, y1: 0, y2: 1}]);
|
||||
return assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]);
|
||||
});
|
||||
it('surrogate - match within one line', () => {
|
||||
return assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{x1: 5, x2: 8, y1: 0, y2: 0}]);
|
||||
return assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]);
|
||||
});
|
||||
it('surrogate - match over two lines', () => {
|
||||
return assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{x1: 9, x2: 2, y1: 0, y2: 1}]);
|
||||
return assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{ x1: 9, x2: 2, y1: 0, y2: 1 }]);
|
||||
});
|
||||
it('combining surrogate - match within one line', () => {
|
||||
return assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 5, x2: 8, y1: 0, y2: 0}]);
|
||||
return assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]);
|
||||
});
|
||||
it('combining surrogate - match over two lines', () => {
|
||||
return assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 9, x2: 2, y1: 0, y2: 1}]);
|
||||
return assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{ x1: 9, x2: 2, y1: 0, y2: 1 }]);
|
||||
});
|
||||
it('fullwidth - match within one line', () => {
|
||||
return assertLinkifiesInTerminal('test a1b', /a1b/, [{x1: 5, x2: 9, y1: 0, y2: 0}]);
|
||||
return assertLinkifiesInTerminal('test a1b', /a1b/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]);
|
||||
});
|
||||
it('fullwidth - match over two lines', () => {
|
||||
return assertLinkifiesInTerminal('testtest a1b', /a1b/, [{x1: 9, x2: 3, y1: 0, y2: 1}]);
|
||||
return assertLinkifiesInTerminal('testtest a1b', /a1b/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]);
|
||||
});
|
||||
it('combining fullwidth - match within one line', () => {
|
||||
return assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{x1: 5, x2: 9, y1: 0, y2: 0}]);
|
||||
return assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]);
|
||||
});
|
||||
it('combining fullwidth - match over two lines', () => {
|
||||
return assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{x1: 9, x2: 3, y1: 0, y2: 1}]);
|
||||
return assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1143,7 +1143,7 @@ describe('Terminal', () => {
|
||||
let terminal: TestTerminal;
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
|
||||
terminal = new TestTerminal({ rows: 5, cols: 10, scrollback: 5 });
|
||||
});
|
||||
|
||||
it('multiline ascii', async () => {
|
||||
@@ -1318,7 +1318,7 @@ describe('Terminal', () => {
|
||||
|
||||
it('test fully wrapped buffer up to last char with full width odd', async () => {
|
||||
const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'
|
||||
+ 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301';
|
||||
+ 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301';
|
||||
await terminal.writeP(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
@@ -1342,9 +1342,9 @@ describe('Terminal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('BufferStringIterator', function(): void {
|
||||
describe('BufferStringIterator', function (): void {
|
||||
it('iterator does not overflow buffer limits', async () => {
|
||||
const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
|
||||
const terminal = new TestTerminal({ rows: 5, cols: 10, scrollback: 5 });
|
||||
const data = [
|
||||
'aaaaaaaaaa',
|
||||
'aaaaaaaaa\n',
|
||||
@@ -1382,13 +1382,13 @@ describe('Terminal', () => {
|
||||
'aaaaaaaaa' // not wrapped
|
||||
];
|
||||
|
||||
const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false});
|
||||
const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: false });
|
||||
await normalTerminal.writeP(data.join(''));
|
||||
assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false);
|
||||
assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false);
|
||||
assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false);
|
||||
|
||||
const windowsModeTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: true});
|
||||
const windowsModeTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: true });
|
||||
await windowsModeTerminal.writeP(data.join(''));
|
||||
assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false);
|
||||
assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character');
|
||||
@@ -1402,13 +1402,13 @@ describe('Terminal', () => {
|
||||
'aaaaaaaaa' // not wrapped
|
||||
];
|
||||
|
||||
const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false});
|
||||
const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: false });
|
||||
await normalTerminal.writeP(data.join(''));
|
||||
assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false);
|
||||
assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false);
|
||||
assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false);
|
||||
|
||||
const windowsModeTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: true});
|
||||
const windowsModeTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: true });
|
||||
await windowsModeTerminal.writeP(data.join(''));
|
||||
assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false);
|
||||
assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character');
|
||||
@@ -1417,7 +1417,7 @@ describe('Terminal', () => {
|
||||
});
|
||||
it('convertEol setting', async () => {
|
||||
// not converting
|
||||
const termNotConverting = new TestTerminal({cols: 15, rows: 10});
|
||||
const termNotConverting = new TestTerminal({ cols: 15, rows: 10 });
|
||||
await termNotConverting.writeP('Hello\nWorld');
|
||||
assert.equal(termNotConverting.buffer.lines.get(0)!.translateToString(false), 'Hello ');
|
||||
assert.equal(termNotConverting.buffer.lines.get(1)!.translateToString(false), ' World ');
|
||||
@@ -1425,128 +1425,13 @@ describe('Terminal', () => {
|
||||
assert.equal(termNotConverting.buffer.lines.get(1)!.translateToString(true), ' World');
|
||||
|
||||
// converting
|
||||
const termConverting = new TestTerminal({cols: 15, rows: 10, convertEol: true});
|
||||
const termConverting = new TestTerminal({ cols: 15, rows: 10, convertEol: true });
|
||||
await termConverting.writeP('Hello\nWorld');
|
||||
assert.equal(termConverting.buffer.lines.get(0)!.translateToString(false), 'Hello ');
|
||||
assert.equal(termConverting.buffer.lines.get(1)!.translateToString(false), 'World ');
|
||||
assert.equal(termConverting.buffer.lines.get(0)!.translateToString(true), 'Hello');
|
||||
assert.equal(termConverting.buffer.lines.get(1)!.translateToString(true), 'World');
|
||||
});
|
||||
describe('Terminal InputHandler integration', () => {
|
||||
function getLines(term: TestTerminal, limit: number = term.rows): string[] {
|
||||
const res: string[] = [];
|
||||
for (let i = 0; i < limit; ++i) {
|
||||
res.push(term.buffer.lines.get(i)!.translateToString(true));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// This suite cannot live in InputHandler unless Terminal.scroll moved into IBufferService
|
||||
describe('SL/SR/DECIC/DECDC', () => {
|
||||
let term: TestTerminal;
|
||||
beforeEach(() => {
|
||||
term = new TestTerminal({cols: 5, rows: 5, scrollback: 1});
|
||||
});
|
||||
it('SL (scrollLeft)', async () => {
|
||||
await term.writeP('12345'.repeat(6));
|
||||
await term.writeP('\x1b[ @');
|
||||
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '2345', '2345', '2345', '2345', '2345']);
|
||||
await term.writeP('\x1b[0 @');
|
||||
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '345', '345', '345', '345', '345']);
|
||||
await term.writeP('\x1b[2 @');
|
||||
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '5', '5', '5', '5', '5']);
|
||||
});
|
||||
it('SR (scrollRight)', async () => {
|
||||
await term.writeP('12345'.repeat(6));
|
||||
await term.writeP('\x1b[ A');
|
||||
assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']);
|
||||
await term.writeP('\x1b[0 A');
|
||||
assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']);
|
||||
await term.writeP('\x1b[2 A');
|
||||
assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']);
|
||||
});
|
||||
it('insertColumns (DECIC)', async () => {
|
||||
await term.writeP('12345'.repeat(6));
|
||||
await term.writeP('\x1b[3;3H');
|
||||
await term.writeP('\x1b[\'}');
|
||||
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
|
||||
term.reset();
|
||||
await term.writeP('12345'.repeat(6));
|
||||
await term.writeP('\x1b[3;3H');
|
||||
await term.writeP('\x1b[1\'}');
|
||||
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
|
||||
term.reset();
|
||||
await term.writeP('12345'.repeat(6));
|
||||
await term.writeP('\x1b[3;3H');
|
||||
await term.writeP('\x1b[2\'}');
|
||||
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']);
|
||||
});
|
||||
it('deleteColumns (DECDC)', async () => {
|
||||
await term.writeP('12345'.repeat(6));
|
||||
await term.writeP('\x1b[3;3H');
|
||||
await term.writeP('\x1b[\'~');
|
||||
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']);
|
||||
term.reset();
|
||||
await term.writeP('12345'.repeat(6));
|
||||
await term.writeP('\x1b[3;3H');
|
||||
await term.writeP('\x1b[1\'~');
|
||||
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']);
|
||||
term.reset();
|
||||
await term.writeP('12345'.repeat(6));
|
||||
await term.writeP('\x1b[3;3H');
|
||||
await term.writeP('\x1b[2\'~');
|
||||
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '125', '125', '125', '125', '125']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BS with reverseWraparound set/unset', () => {
|
||||
const ttyBS = '\x08 \x08'; // tty ICANON sends <BS SP BS> on pressing BS
|
||||
|
||||
beforeEach(() => {
|
||||
term = new TestTerminal({cols: 5, rows: 5, scrollback: 1});
|
||||
});
|
||||
|
||||
describe('reverseWraparound set', () => {
|
||||
it('should not reverse outside of scroll margins', async () => {
|
||||
// prepare buffer content
|
||||
await term.writeP('#####abcdefghijklmnopqrstuvwxy');
|
||||
assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', 'uvwxy']);
|
||||
assert.equal(term.buffer.ydisp, 1);
|
||||
assert.equal(term.buffer.x, 5);
|
||||
assert.equal(term.buffer.y, 4);
|
||||
await term.writeP(ttyBS.repeat(100));
|
||||
assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' y']);
|
||||
|
||||
await term.writeP('\x1b[?45h');
|
||||
await term.writeP('uvwxy');
|
||||
|
||||
// set top/bottom to 1/3 (0-based)
|
||||
await term.writeP('\x1b[2;4r');
|
||||
// place cursor below scroll bottom
|
||||
term.buffer.x = 5;
|
||||
term.buffer.y = 4;
|
||||
await term.writeP(ttyBS.repeat(100));
|
||||
assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' ']);
|
||||
|
||||
await term.writeP('uvwxy');
|
||||
// place cursor within scroll margins
|
||||
term.buffer.x = 5;
|
||||
term.buffer.y = 3;
|
||||
await term.writeP(ttyBS.repeat(100));
|
||||
assert.deepEqual(getLines(term, 6), ['#####', 'abcde', ' ', ' ', ' ', 'uvwxy']);
|
||||
assert.equal(term.buffer.x, 0);
|
||||
assert.equal(term.buffer.y, term.buffer.scrollTop); // stops at 0, scrollTop
|
||||
|
||||
await term.writeP('fghijklmnopqrst');
|
||||
// place cursor above scroll top
|
||||
term.buffer.x = 5;
|
||||
term.buffer.y = 0;
|
||||
await term.writeP(ttyBS.repeat(100));
|
||||
assert.deepEqual(getLines(term, 6), ['#####', ' ', 'fghij', 'klmno', 'pqrst', 'uvwxy']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// FIXME: move to common/CoreTerminal.test once the trimming is moved over
|
||||
describe('marker lifecycle', () => {
|
||||
|
||||
+37
-27
@@ -39,7 +39,7 @@ import { MouseZoneManager } from 'browser/MouseZoneManager';
|
||||
import { AccessibilityManager } from './AccessibilityManager';
|
||||
import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider } from 'xterm';
|
||||
import { DomRenderer } from 'browser/renderer/dom/DomRenderer';
|
||||
import { IKeyboardEvent, KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, IAnsiColorChangeEvent } from 'common/Types';
|
||||
import { IKeyboardEvent, KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, ScrollSource, IAnsiColorChangeEvent } from 'common/Types';
|
||||
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
|
||||
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
|
||||
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
@@ -147,7 +147,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this.register(this._inputHandler.onRequestBell(() => this.bell()));
|
||||
this.register(this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end)));
|
||||
this.register(this._inputHandler.onRequestReset(() => this.reset()));
|
||||
this.register(this._inputHandler.onRequestScroll((eraseAttr, isWrapped) => this.scroll(eraseAttr, isWrapped || undefined)));
|
||||
this.register(this._inputHandler.onRequestWindowsOptionsReport(type => this._reportWindowsOptions(type)));
|
||||
this.register(this._inputHandler.onAnsiColorChange((event) => this._changeAnsiColor(event)));
|
||||
this.register(forwardEvent(this._inputHandler.onCursorMove, this._onCursorMove));
|
||||
@@ -162,11 +161,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
private _changeAnsiColor(event: IAnsiColorChangeEvent): void {
|
||||
if (!this._colorManager) { return; }
|
||||
|
||||
event.colors.forEach(ansiColor => {
|
||||
for (const ansiColor of event.colors) {
|
||||
const color = rgba.toColor(ansiColor.red, ansiColor.green, ansiColor.blue);
|
||||
|
||||
this._colorManager!.colors.ansi[ansiColor.colorIndex] = color;
|
||||
});
|
||||
}
|
||||
|
||||
this._renderService?.setColors(this._colorManager!.colors);
|
||||
this.viewport?.onThemeChange(this._colorManager!.colors);
|
||||
@@ -297,19 +296,26 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
}
|
||||
|
||||
private _syncTextArea(): void {
|
||||
if (!this.textarea || !this.buffer.isCursorInViewport || this._compositionHelper!.isComposing) {
|
||||
if (!this.textarea || !this.buffer.isCursorInViewport || this._compositionHelper!.isComposing || !this._renderService) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cellHeight = Math.ceil(this._charSizeService!.height * this.optionsService.options.lineHeight);
|
||||
const cursorTop = this._bufferService.buffer.y * cellHeight;
|
||||
const cursorLeft = this._bufferService.buffer.x * this._charSizeService!.width;
|
||||
const cursorY = this.buffer.ybase + this.buffer.y;
|
||||
const bufferLine = this.buffer.lines.get(cursorY);
|
||||
if (!bufferLine) {
|
||||
return;
|
||||
}
|
||||
const cursorX = Math.min(this.buffer.x, this.cols - 1);
|
||||
const cellHeight = this._renderService.dimensions.actualCellHeight;
|
||||
const width = bufferLine.getWidth(cursorX);
|
||||
const cellWidth = this._renderService.dimensions.actualCellWidth * width;
|
||||
const cursorTop = this.buffer.y * this._renderService.dimensions.actualCellHeight;
|
||||
const cursorLeft = cursorX * this._renderService.dimensions.actualCellWidth;
|
||||
|
||||
// Sync the textarea to the exact position of the composition view so the IME knows where the
|
||||
// text is.
|
||||
this.textarea.style.left = cursorLeft + 'px';
|
||||
this.textarea.style.top = cursorTop + 'px';
|
||||
this.textarea.style.width = this._charSizeService!.width + 'px';
|
||||
this.textarea.style.width = cellWidth + 'px';
|
||||
this.textarea.style.height = cellHeight + 'px';
|
||||
this.textarea.style.lineHeight = cellHeight + 'px';
|
||||
this.textarea.style.zIndex = '-5';
|
||||
@@ -438,14 +444,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer);
|
||||
this._instantiationService.setService(ICharSizeService, this._charSizeService);
|
||||
|
||||
this._compositionView = document.createElement('div');
|
||||
this._compositionView.classList.add('composition-view');
|
||||
this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView);
|
||||
this._helperContainer.appendChild(this._compositionView);
|
||||
|
||||
// Performance: Add viewport and helper elements from the fragment
|
||||
this.element.appendChild(fragment);
|
||||
|
||||
this._theme = this.options.theme || this._theme;
|
||||
this._colorManager = new ColorManager(document, this.options.allowTransparency);
|
||||
this.register(this.optionsService.onOptionChange(e => this._colorManager!.onOptionsChange(e)));
|
||||
@@ -457,13 +455,21 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this.register(this._renderService.onRenderedBufferChange(e => this._onRender.fire(e)));
|
||||
this.onResize(e => this._renderService!.resize(e.cols, e.rows));
|
||||
|
||||
this._compositionView = document.createElement('div');
|
||||
this._compositionView.classList.add('composition-view');
|
||||
this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView);
|
||||
this._helperContainer.appendChild(this._compositionView);
|
||||
|
||||
// Performance: Add viewport and helper elements from the fragment
|
||||
this.element.appendChild(fragment);
|
||||
|
||||
this._soundService = this._instantiationService.createInstance(SoundService);
|
||||
this._instantiationService.setService(ISoundService, this._soundService);
|
||||
this._mouseService = this._instantiationService.createInstance(MouseService);
|
||||
this._instantiationService.setService(IMouseService, this._mouseService);
|
||||
|
||||
this.viewport = this._instantiationService.createInstance(Viewport,
|
||||
(amount: number, suppressEvent: boolean) => this.scrollLines(amount, suppressEvent),
|
||||
(amount: number) => this.scrollLines(amount, false, ScrollSource.VIEWPORT),
|
||||
this._viewportElement,
|
||||
this._viewportScrollArea
|
||||
);
|
||||
@@ -482,7 +488,9 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
this._selectionService = this.register(this._instantiationService.createInstance(SelectionService,
|
||||
this.element,
|
||||
this.screenElement));
|
||||
this.screenElement,
|
||||
this.linkifier2
|
||||
));
|
||||
this._instantiationService.setService(ISelectionService, this._selectionService);
|
||||
this.register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent)));
|
||||
this.register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire()));
|
||||
@@ -495,8 +503,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this.textarea!.focus();
|
||||
this.textarea!.select();
|
||||
}));
|
||||
this.register(this.onScroll(() => {
|
||||
this.viewport!.syncScrollArea();
|
||||
this.register(this._onScroll.event(ev => {
|
||||
if (ev.source !== ScrollSource.VIEWPORT) {
|
||||
this.viewport!.syncScrollArea();
|
||||
}
|
||||
this._selectionService!.refresh();
|
||||
}));
|
||||
this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this._selectionService!.refresh()));
|
||||
@@ -834,7 +844,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
* Change the cursor style for different selection modes
|
||||
*/
|
||||
public updateCursorStyle(ev: KeyboardEvent): void {
|
||||
if (this._selectionService && this._selectionService.shouldColumnSelect(ev)) {
|
||||
if (this._selectionService?.shouldColumnSelect(ev)) {
|
||||
this.element!.classList.add('column-select');
|
||||
} else {
|
||||
this.element!.classList.remove('column-select');
|
||||
@@ -851,8 +861,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
}
|
||||
}
|
||||
|
||||
public scrollLines(disp: number, suppressScrollEvent?: boolean): void {
|
||||
super.scrollLines(disp, suppressScrollEvent);
|
||||
public scrollLines(disp: number, suppressScrollEvent?: boolean, source = ScrollSource.TERMINAL): void {
|
||||
super.scrollLines(disp, suppressScrollEvent, source);
|
||||
this.refresh(0, this.rows - 1);
|
||||
}
|
||||
|
||||
@@ -999,7 +1009,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
if (!this._compositionHelper!.keydown(event)) {
|
||||
if (this.buffer.ybase !== this.buffer.ydisp) {
|
||||
this.scrollToBottom();
|
||||
this._bufferService.scrollToBottom();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1183,7 +1193,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
}
|
||||
this.refresh(0, this.rows - 1);
|
||||
this._onScroll.fire(this.buffer.ydisp);
|
||||
this._onScroll.fire({ position: this.buffer.ydisp, source: ScrollSource.TERMINAL });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+10
@@ -205,9 +205,19 @@ export interface ILinkifier {
|
||||
deregisterLinkMatcher(matcherId: number): boolean;
|
||||
}
|
||||
|
||||
interface ILinkState {
|
||||
decorations: ILinkDecorations;
|
||||
isHovered: boolean;
|
||||
}
|
||||
export interface ILinkWithState {
|
||||
link: ILink;
|
||||
state?: ILinkState;
|
||||
}
|
||||
|
||||
export interface ILinkifier2 {
|
||||
onShowLinkUnderline: IEvent<ILinkifierEvent>;
|
||||
onHideLinkUnderline: IEvent<ILinkifierEvent>;
|
||||
readonly currentLink: ILinkWithState | undefined;
|
||||
|
||||
attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void;
|
||||
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
|
||||
|
||||
@@ -33,7 +33,7 @@ export class Viewport extends Disposable implements IViewport {
|
||||
private _ignoreNextScrollEvent: boolean = false;
|
||||
|
||||
constructor(
|
||||
private readonly _scrollLines: (amount: number, suppressEvent: boolean) => void,
|
||||
private readonly _scrollLines: (amount: number) => void,
|
||||
private readonly _viewportElement: HTMLElement,
|
||||
private readonly _scrollArea: HTMLElement,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@@ -156,7 +156,7 @@ export class Viewport extends Disposable implements IViewport {
|
||||
|
||||
const newRow = Math.round(this._lastScrollTop / this._currentRowHeight);
|
||||
const diff = newRow - this._bufferService.buffer.ydisp;
|
||||
this._scrollLines(diff, true);
|
||||
this._scrollLines(diff);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { CompositionHelper } from 'browser/input/CompositionHelper';
|
||||
import { MockCharSizeService } from 'browser/TestUtils.test';
|
||||
import { MockRenderService } from 'browser/TestUtils.test';
|
||||
import { MockCoreService, MockBufferService, MockOptionsService } from 'common/TestUtils.test';
|
||||
|
||||
describe('CompositionHelper', () => {
|
||||
@@ -42,7 +42,7 @@ describe('CompositionHelper', () => {
|
||||
};
|
||||
handledText = '';
|
||||
const bufferService = new MockBufferService(10, 5);
|
||||
compositionHelper = new CompositionHelper(textarea, compositionView, bufferService, new MockOptionsService(), new MockCharSizeService(10, 10), coreService);
|
||||
compositionHelper = new CompositionHelper(textarea, compositionView, bufferService, new MockOptionsService(), coreService, new MockRenderService());
|
||||
});
|
||||
|
||||
describe('Input', () => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharSizeService } from 'browser/services/Services';
|
||||
import { IRenderService } from 'browser/services/Services';
|
||||
import { IBufferService, ICoreService, IOptionsService } from 'common/services/Services';
|
||||
|
||||
interface IPosition {
|
||||
@@ -45,8 +45,8 @@ export class CompositionHelper {
|
||||
private readonly _compositionView: HTMLElement,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService,
|
||||
@ICoreService private readonly _coreService: ICoreService
|
||||
@ICoreService private readonly _coreService: ICoreService,
|
||||
@IRenderService private readonly _renderService: IRenderService
|
||||
) {
|
||||
this._isComposing = false;
|
||||
this._isSendingComposition = false;
|
||||
@@ -207,9 +207,11 @@ export class CompositionHelper {
|
||||
}
|
||||
|
||||
if (this._bufferService.buffer.isCursorInViewport) {
|
||||
const cellHeight = Math.ceil(this._charSizeService.height * this._optionsService.options.lineHeight);
|
||||
const cursorTop = this._bufferService.buffer.y * cellHeight;
|
||||
const cursorLeft = this._bufferService.buffer.x * this._charSizeService.width;
|
||||
const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1);
|
||||
|
||||
const cellHeight = this._renderService.dimensions.actualCellHeight;
|
||||
const cursorTop = this._bufferService.buffer.y * this._renderService.dimensions.actualCellHeight;
|
||||
const cursorLeft = cursorX * this._renderService.dimensions.actualCellWidth;
|
||||
|
||||
this._compositionView.style.left = cursorLeft + 'px';
|
||||
this._compositionView.style.top = cursorTop + 'px';
|
||||
|
||||
@@ -242,12 +242,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
protected _fillCharTrueColor(cell: CellData, x: number, y: number): void {
|
||||
this._ctx.font = this._getFont(false, false);
|
||||
this._ctx.textBaseline = 'middle';
|
||||
this._ctx.textBaseline = 'ideographic';
|
||||
this._clipRow(y);
|
||||
this._ctx.fillText(
|
||||
cell.getChars(),
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -320,7 +320,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
private _drawUncachedChars(cell: ICellData, x: number, y: number, fgOverride?: IColor): void {
|
||||
this._ctx.save();
|
||||
this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic());
|
||||
this._ctx.textBaseline = 'middle';
|
||||
this._ctx.textBaseline = 'ideographic';
|
||||
|
||||
if (cell.isInverse()) {
|
||||
if (fgOverride) {
|
||||
@@ -362,7 +362,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._ctx.fillText(
|
||||
cell.getChars(),
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ export class DynamicCharAtlas extends BaseCharAtlas {
|
||||
const fontStyle = glyph.italic ? 'italic' : '';
|
||||
this._tmpCtx.font =
|
||||
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
|
||||
this._tmpCtx.textBaseline = 'middle';
|
||||
this._tmpCtx.textBaseline = 'ideographic';
|
||||
|
||||
this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css;
|
||||
|
||||
@@ -265,7 +265,7 @@ export class DynamicCharAtlas extends BaseCharAtlas {
|
||||
this._tmpCtx.globalAlpha = DIM_OPACITY;
|
||||
}
|
||||
// Draw the character
|
||||
this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight / 2);
|
||||
this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight);
|
||||
this._tmpCtx.restore();
|
||||
|
||||
// clear the background from the character to avoid issues with drawing over the previous
|
||||
|
||||
@@ -21,7 +21,7 @@ class TestSelectionService extends SelectionService {
|
||||
optionsService: IOptionsService,
|
||||
renderService: IRenderService
|
||||
) {
|
||||
super(null!, null!, bufferService, new MockCoreService(), new MockMouseService(), optionsService, renderService);
|
||||
super(null!, null!, null!, bufferService, new MockCoreService(), new MockMouseService(), optionsService, renderService);
|
||||
}
|
||||
|
||||
public get model(): SelectionModel { return this._model; }
|
||||
|
||||
@@ -11,6 +11,7 @@ import { SelectionModel } from 'browser/selection/SelectionModel';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { ICharSizeService, IMouseService, ISelectionService, IRenderService } from 'browser/services/Services';
|
||||
import { ILinkifier2 } from 'browser/Types';
|
||||
import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
|
||||
import { getCoordsRelativeToElement } from 'browser/input/Mouse';
|
||||
import { moveToCellSequence } from 'browser/input/MoveToCell';
|
||||
@@ -121,6 +122,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
constructor(
|
||||
private readonly _element: HTMLElement,
|
||||
private readonly _screenElement: HTMLElement,
|
||||
private readonly _linkifier: ILinkifier2,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@ICoreService private readonly _coreService: ICoreService,
|
||||
@IMouseService private readonly _mouseService: IMouseService,
|
||||
@@ -316,13 +318,22 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
* Selects word at the current mouse event coordinates.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _selectWordAtCursor(event: MouseEvent): void {
|
||||
private _selectWordAtCursor(event: MouseEvent, allowWhitespaceOnlySelection: boolean): boolean {
|
||||
// Check if there is a link under the cursor first and select that if so
|
||||
const range = this._linkifier.currentLink?.link?.range;
|
||||
if (range) {
|
||||
this._model.selectionStart = [range.start.x - 1, range.start.y - 1];
|
||||
this._model.selectionEnd = [range.end.x, range.end.y - 1];
|
||||
return true;
|
||||
}
|
||||
|
||||
const coords = this._getMouseBufferCoords(event);
|
||||
if (coords) {
|
||||
this._selectWordAt(coords, false);
|
||||
this._selectWordAt(coords, allowWhitespaceOnlySelection);
|
||||
this._model.selectionEnd = undefined;
|
||||
this.refresh(true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -527,14 +538,12 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a double click, selecting the current work.
|
||||
* Performs a double click, selecting the current word.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _onDoubleClick(event: MouseEvent): void {
|
||||
const coords = this._getMouseBufferCoords(event);
|
||||
if (coords) {
|
||||
if (this._selectWordAtCursor(event, true)) {
|
||||
this._activeSelectionMode = SelectionMode.WORD;
|
||||
this._selectWordAt(coords, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -764,7 +773,9 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
|
||||
public rightClickSelect(ev: MouseEvent): void {
|
||||
if (!this._isClickInSelection(ev)) {
|
||||
this._selectWordAtCursor(ev);
|
||||
if (this._selectWordAtCursor(ev, false)) {
|
||||
this.refresh(true);
|
||||
}
|
||||
this._fireEventIfSelectionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
+30
-97
@@ -27,7 +27,7 @@ import { InstantiationService } from 'common/services/InstantiationService';
|
||||
import { LogService } from 'common/services/LogService';
|
||||
import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService';
|
||||
import { OptionsService } from 'common/services/OptionsService';
|
||||
import { ITerminalOptions, IDisposable, IBufferLine, IAttributeData, ICoreTerminal } from 'common/Types';
|
||||
import { ITerminalOptions, IDisposable, IBufferLine, IAttributeData, ICoreTerminal, IKeyboardEvent, IScrollEvent, ScrollSource } from 'common/Types';
|
||||
import { CoreService } from 'common/services/CoreService';
|
||||
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
|
||||
import { CoreMouseService } from 'common/services/CoreMouseService';
|
||||
@@ -41,7 +41,7 @@ import { InputHandler } from 'common/InputHandler';
|
||||
import { WriteBuffer } from 'common/input/WriteBuffer';
|
||||
|
||||
// Only trigger this warning a single time per session
|
||||
let hasWriteSyncWarnHappened: boolean = false;
|
||||
let hasWriteSyncWarnHappened = false;
|
||||
|
||||
export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
protected readonly _instantiationService: IInstantiationService;
|
||||
@@ -58,8 +58,6 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
protected _inputHandler: InputHandler;
|
||||
private _writeBuffer: WriteBuffer;
|
||||
private _windowsMode: IDisposable | undefined;
|
||||
/** An IBufferline to clone/copy from for new blank lines */
|
||||
private _cachedBlankLine: IBufferLine | undefined;
|
||||
|
||||
private _onBinary = new EventEmitter<string>();
|
||||
public get onBinary(): IEvent<string> { return this._onBinary.event; }
|
||||
@@ -69,8 +67,21 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
public get onLineFeed(): IEvent<void> { return this._onLineFeed.event; }
|
||||
private _onResize = new EventEmitter<{ cols: number, rows: number }>();
|
||||
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
|
||||
protected _onScroll = new EventEmitter<number>();
|
||||
public get onScroll(): IEvent<number> { return this._onScroll.event; }
|
||||
protected _onScroll = new EventEmitter<IScrollEvent, void>();
|
||||
/**
|
||||
* Internally we track the source of the scroll but this is meaningless outside the library so
|
||||
* it's filtered out.
|
||||
*/
|
||||
protected _onScrollApi?: EventEmitter<number, void>;
|
||||
public get onScroll(): IEvent<number, void> {
|
||||
if (!this._onScrollApi) {
|
||||
this._onScrollApi = new EventEmitter<number, void>();
|
||||
this.register(this._onScroll.event(ev => {
|
||||
this._onScrollApi?.fire(ev.position);
|
||||
}));
|
||||
}
|
||||
return this._onScrollApi.event;
|
||||
}
|
||||
|
||||
public get cols(): number { return this._bufferService.cols; }
|
||||
public get rows(): number { return this._bufferService.rows; }
|
||||
@@ -110,6 +121,10 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
this.register(forwardEvent(this._coreService.onData, this._onData));
|
||||
this.register(forwardEvent(this._coreService.onBinary, this._onBinary));
|
||||
this.register(this.optionsService.onOptionChange(key => this._updateOptions(key)));
|
||||
this.register(this._bufferService.onScroll(event => {
|
||||
this._onScroll.fire({position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL});
|
||||
this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
|
||||
}));
|
||||
|
||||
// Setup WriteBuffer
|
||||
this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult));
|
||||
@@ -137,12 +152,12 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
*
|
||||
* @deprecated Unreliable, will be removed soon.
|
||||
*/
|
||||
public writeSync(data: string | Uint8Array): void {
|
||||
public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void {
|
||||
if (this._logService.logLevel <= LogLevelEnum.WARN && !hasWriteSyncWarnHappened) {
|
||||
this._logService.warn('writeSync is unreliable and will be removed soon.');
|
||||
hasWriteSyncWarnHappened = true;
|
||||
}
|
||||
this._writeBuffer.writeSync(data);
|
||||
this._writeBuffer.writeSync(data, maxSubsequentCalls);
|
||||
}
|
||||
|
||||
public resize(x: number, y: number): void {
|
||||
@@ -161,66 +176,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
* @param isWrapped Whether the new line is wrapped from the previous line.
|
||||
*/
|
||||
public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {
|
||||
const buffer = this._bufferService.buffer;
|
||||
|
||||
let newLine: IBufferLine | undefined;
|
||||
newLine = this._cachedBlankLine;
|
||||
if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) {
|
||||
newLine = buffer.getBlankLine(eraseAttr, isWrapped);
|
||||
this._cachedBlankLine = newLine;
|
||||
}
|
||||
newLine.isWrapped = isWrapped;
|
||||
|
||||
const topRow = buffer.ybase + buffer.scrollTop;
|
||||
const bottomRow = buffer.ybase + buffer.scrollBottom;
|
||||
|
||||
if (buffer.scrollTop === 0) {
|
||||
// Determine whether the buffer is going to be trimmed after insertion.
|
||||
const willBufferBeTrimmed = buffer.lines.isFull;
|
||||
|
||||
// Insert the line using the fastest method
|
||||
if (bottomRow === buffer.lines.length - 1) {
|
||||
if (willBufferBeTrimmed) {
|
||||
buffer.lines.recycle().copyFrom(newLine);
|
||||
} else {
|
||||
buffer.lines.push(newLine.clone());
|
||||
}
|
||||
} else {
|
||||
buffer.lines.splice(bottomRow + 1, 0, newLine.clone());
|
||||
}
|
||||
|
||||
// Only adjust ybase and ydisp when the buffer is not trimmed
|
||||
if (!willBufferBeTrimmed) {
|
||||
buffer.ybase++;
|
||||
// Only scroll the ydisp with ybase if the user has not scrolled up
|
||||
if (!this._bufferService.isUserScrolling) {
|
||||
buffer.ydisp++;
|
||||
}
|
||||
} else {
|
||||
// When the buffer is full and the user has scrolled up, keep the text
|
||||
// stable unless ydisp is right at the top
|
||||
if (this._bufferService.isUserScrolling) {
|
||||
buffer.ydisp = Math.max(buffer.ydisp - 1, 0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// scrollTop is non-zero which means no line will be going to the
|
||||
// scrollback, instead we can just shift them in-place.
|
||||
const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */;
|
||||
buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1);
|
||||
buffer.lines.set(bottomRow, newLine.clone());
|
||||
}
|
||||
|
||||
// Move the viewport to the bottom of the buffer unless the user is
|
||||
// scrolling.
|
||||
if (!this._bufferService.isUserScrolling) {
|
||||
buffer.ydisp = buffer.ybase;
|
||||
}
|
||||
|
||||
// Flag rows that need updating
|
||||
this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom);
|
||||
|
||||
this._onScroll.fire(buffer.ydisp);
|
||||
this._bufferService.scroll(eraseAttr, isWrapped);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,28 +186,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
* to avoid unwanted events being handled by the viewport when the event was triggered from the
|
||||
* viewport originally.
|
||||
*/
|
||||
public scrollLines(disp: number, suppressScrollEvent?: boolean): void {
|
||||
const buffer = this._bufferService.buffer;
|
||||
if (disp < 0) {
|
||||
if (buffer.ydisp === 0) {
|
||||
return;
|
||||
}
|
||||
this._bufferService.isUserScrolling = true;
|
||||
} else if (disp + buffer.ydisp >= buffer.ybase) {
|
||||
this._bufferService.isUserScrolling = false;
|
||||
}
|
||||
|
||||
const oldYdisp = buffer.ydisp;
|
||||
buffer.ydisp = Math.max(Math.min(buffer.ydisp + disp, buffer.ybase), 0);
|
||||
|
||||
// No change occurred, don't trigger scroll/refresh
|
||||
if (oldYdisp === buffer.ydisp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!suppressScrollEvent) {
|
||||
this._onScroll.fire(buffer.ydisp);
|
||||
}
|
||||
public scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void {
|
||||
this._bufferService.scrollLines(disp, suppressScrollEvent, source);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,28 +195,25 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
* @param pageCount The number of pages to scroll (negative scrolls up).
|
||||
*/
|
||||
public scrollPages(pageCount: number): void {
|
||||
this.scrollLines(pageCount * (this.rows - 1));
|
||||
this._bufferService.scrollPages(pageCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the display of the terminal to the top.
|
||||
*/
|
||||
public scrollToTop(): void {
|
||||
this.scrollLines(-this._bufferService.buffer.ydisp);
|
||||
this._bufferService.scrollToTop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the display of the terminal to the bottom.
|
||||
*/
|
||||
public scrollToBottom(): void {
|
||||
this.scrollLines(this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp);
|
||||
this._bufferService.scrollToBottom();
|
||||
}
|
||||
|
||||
public scrollToLine(line: number): void {
|
||||
const scrollAmount = line - this._bufferService.buffer.ydisp;
|
||||
if (scrollAmount !== 0) {
|
||||
this.scrollLines(scrollAmount);
|
||||
}
|
||||
this._bufferService.scrollToLine(line);
|
||||
}
|
||||
|
||||
/** Add handler for ESC escape sequence. See xterm.d.ts for details. */
|
||||
|
||||
@@ -66,11 +66,117 @@ describe('InputHandler', () => {
|
||||
optionsService = new MockOptionsService();
|
||||
bufferService = new BufferService(optionsService);
|
||||
bufferService.resize(80, 30);
|
||||
coreService = new CoreService(() => {}, bufferService, new MockLogService(), optionsService);
|
||||
coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService);
|
||||
|
||||
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService());
|
||||
});
|
||||
|
||||
describe('SL/SR/DECIC/DECDC', () => {
|
||||
beforeEach(() => {
|
||||
bufferService.resize(5, 5);
|
||||
optionsService.options.scrollback = 1;
|
||||
bufferService.reset();
|
||||
});
|
||||
it('SL (scrollLeft)', async () => {
|
||||
inputHandler.parseP('12345'.repeat(6));
|
||||
inputHandler.parseP('\x1b[ @');
|
||||
assert.deepEqual(getLines(bufferService, 6), ['12345', '2345', '2345', '2345', '2345', '2345']);
|
||||
inputHandler.parseP('\x1b[0 @');
|
||||
assert.deepEqual(getLines(bufferService, 6), ['12345', '345', '345', '345', '345', '345']);
|
||||
inputHandler.parseP('\x1b[2 @');
|
||||
assert.deepEqual(getLines(bufferService, 6), ['12345', '5', '5', '5', '5', '5']);
|
||||
});
|
||||
it('SR (scrollRight)', async () => {
|
||||
inputHandler.parseP('12345'.repeat(6));
|
||||
inputHandler.parseP('\x1b[ A');
|
||||
assert.deepEqual(getLines(bufferService, 6), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']);
|
||||
inputHandler.parseP('\x1b[0 A');
|
||||
assert.deepEqual(getLines(bufferService, 6), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']);
|
||||
inputHandler.parseP('\x1b[2 A');
|
||||
assert.deepEqual(getLines(bufferService, 6), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']);
|
||||
});
|
||||
it('insertColumns (DECIC)', async () => {
|
||||
inputHandler.parseP('12345'.repeat(6));
|
||||
inputHandler.parseP('\x1b[3;3H');
|
||||
inputHandler.parseP('\x1b[\'}');
|
||||
assert.deepEqual(getLines(bufferService, 6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
|
||||
bufferService.reset();
|
||||
inputHandler.parseP('12345'.repeat(6));
|
||||
inputHandler.parseP('\x1b[3;3H');
|
||||
inputHandler.parseP('\x1b[1\'}');
|
||||
assert.deepEqual(getLines(bufferService, 6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
|
||||
bufferService.reset();
|
||||
inputHandler.parseP('12345'.repeat(6));
|
||||
inputHandler.parseP('\x1b[3;3H');
|
||||
inputHandler.parseP('\x1b[2\'}');
|
||||
assert.deepEqual(getLines(bufferService, 6), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']);
|
||||
});
|
||||
it('deleteColumns (DECDC)', async () => {
|
||||
inputHandler.parseP('12345'.repeat(6));
|
||||
inputHandler.parseP('\x1b[3;3H');
|
||||
inputHandler.parseP('\x1b[\'~');
|
||||
assert.deepEqual(getLines(bufferService, 6), ['12345', '1245', '1245', '1245', '1245', '1245']);
|
||||
bufferService.reset();
|
||||
inputHandler.parseP('12345'.repeat(6));
|
||||
inputHandler.parseP('\x1b[3;3H');
|
||||
inputHandler.parseP('\x1b[1\'~');
|
||||
assert.deepEqual(getLines(bufferService, 6), ['12345', '1245', '1245', '1245', '1245', '1245']);
|
||||
bufferService.reset();
|
||||
inputHandler.parseP('12345'.repeat(6));
|
||||
inputHandler.parseP('\x1b[3;3H');
|
||||
inputHandler.parseP('\x1b[2\'~');
|
||||
assert.deepEqual(getLines(bufferService, 6), ['12345', '125', '125', '125', '125', '125']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BS with reverseWraparound set/unset', () => {
|
||||
const ttyBS = '\x08 \x08'; // tty ICANON sends <BS SP BS> on pressing BS
|
||||
beforeEach(() => {
|
||||
bufferService.resize(5, 5);
|
||||
optionsService.options.scrollback = 1;
|
||||
bufferService.reset();
|
||||
});
|
||||
describe('reverseWraparound set', () => {
|
||||
it('should not reverse outside of scroll margins', async () => {
|
||||
// prepare buffer content
|
||||
inputHandler.parseP('#####abcdefghijklmnopqrstuvwxy');
|
||||
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', 'uvwxy']);
|
||||
assert.equal(bufferService.buffers.active.ydisp, 1);
|
||||
assert.equal(bufferService.buffers.active.x, 5);
|
||||
assert.equal(bufferService.buffers.active.y, 4);
|
||||
inputHandler.parseP(ttyBS.repeat(100));
|
||||
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' y']);
|
||||
|
||||
inputHandler.parseP('\x1b[?45h');
|
||||
inputHandler.parseP('uvwxy');
|
||||
|
||||
// set top/bottom to 1/3 (0-based)
|
||||
inputHandler.parseP('\x1b[2;4r');
|
||||
// place cursor below scroll bottom
|
||||
bufferService.buffers.active.x = 5;
|
||||
bufferService.buffers.active.y = 4;
|
||||
inputHandler.parseP(ttyBS.repeat(100));
|
||||
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' ']);
|
||||
|
||||
inputHandler.parseP('uvwxy');
|
||||
// place cursor within scroll margins
|
||||
bufferService.buffers.active.x = 5;
|
||||
bufferService.buffers.active.y = 3;
|
||||
inputHandler.parseP(ttyBS.repeat(100));
|
||||
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', ' ', ' ', ' ', 'uvwxy']);
|
||||
assert.equal(bufferService.buffers.active.x, 0);
|
||||
assert.equal(bufferService.buffers.active.y, bufferService.buffers.active.scrollTop); // stops at 0, scrollTop
|
||||
|
||||
inputHandler.parseP('fghijklmnopqrst');
|
||||
// place cursor above scroll top
|
||||
bufferService.buffers.active.x = 5;
|
||||
bufferService.buffers.active.y = 0;
|
||||
inputHandler.parseP(ttyBS.repeat(100));
|
||||
assert.deepEqual(getLines(bufferService, 6), ['#####', ' ', 'fghij', 'klmno', 'pqrst', 'uvwxy']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('save and restore cursor', () => {
|
||||
bufferService.buffer.x = 1;
|
||||
bufferService.buffer.y = 2;
|
||||
@@ -140,7 +246,7 @@ describe('InputHandler', () => {
|
||||
assert.equal(coreService.decPrivateModes.bracketedPasteMode, false);
|
||||
});
|
||||
});
|
||||
describe('regression tests', function(): void {
|
||||
describe('regression tests', function (): void {
|
||||
function termContent(bufferService: IBufferService, trim: boolean): string[] {
|
||||
const result = [];
|
||||
for (let i = 0; i < bufferService.rows; ++i) result.push(bufferService.buffer.lines.get(i)!.translateToString(trim));
|
||||
@@ -1240,7 +1346,7 @@ describe('InputHandler', () => {
|
||||
await inputHandler.parseP('\x1b[6H\x1b[2Mm');
|
||||
assert.deepEqual(getLines(bufferService), ['0', '1', '2', '3', '4', 'm', '6', '7', '8', '9']);
|
||||
await inputHandler.parseP('\x1b[3H\x1b[2Mn');
|
||||
assert.deepEqual(getLines(bufferService), ['0', '1', 'n', 'm', '', '', '6', '7', '8', '9']);
|
||||
assert.deepEqual(getLines(bufferService), ['0', '1', 'n', 'm', '', '', '6', '7', '8', '9']);
|
||||
});
|
||||
});
|
||||
it('should parse big chunks in smaller subchunks', async () => {
|
||||
@@ -1759,7 +1865,8 @@ describe('InputHandler', () => {
|
||||
|
||||
assert.isNotNull(event);
|
||||
assert.deepEqual(event!.colors[0], { colorIndex: 19, red: 0xa1, green: 0xb2, blue: 0xc3 });
|
||||
}),
|
||||
});
|
||||
|
||||
it('4: should ignore incorrect Ansi color change data', () => {
|
||||
// this is testing a private method
|
||||
assert.isNull(inputHandler.parseAnsiColorChange('17;rgb:a/b/c'));
|
||||
@@ -1767,6 +1874,7 @@ describe('InputHandler', () => {
|
||||
assert.isNull(inputHandler.parseAnsiColorChange('17;rgba:aa/bb/cc'));
|
||||
assert.isNull(inputHandler.parseAnsiColorChange('rgb:aa/bb/cc'));
|
||||
});
|
||||
|
||||
it('4: should parse a list of Ansi color changes', () => {
|
||||
// this is testing a private method
|
||||
const event = inputHandler.parseAnsiColorChange('19;rgb:a1/b2/c3;17;rgb:00/11/22;255;rgb:01/ef/2d');
|
||||
@@ -1818,7 +1926,7 @@ describe('InputHandler - async handlers', () => {
|
||||
optionsService = new MockOptionsService();
|
||||
bufferService = new BufferService(optionsService);
|
||||
bufferService.resize(80, 30);
|
||||
coreService = new CoreService(() => {}, bufferService, new MockLogService(), optionsService);
|
||||
coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService);
|
||||
coreService.onData(data => { console.log(data); });
|
||||
|
||||
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService());
|
||||
@@ -1827,7 +1935,7 @@ describe('InputHandler - async handlers', () => {
|
||||
it('async CUP with CPR check', async () => {
|
||||
const cup: number[][] = [];
|
||||
const cpr: number[][] = [];
|
||||
inputHandler.registerCsiHandler({final: 'H'}, async params => {
|
||||
inputHandler.registerCsiHandler({ final: 'H' }, async params => {
|
||||
cup.push(params.toArray() as number[]);
|
||||
await new Promise(res => setTimeout(res, 50));
|
||||
// late call of real repositioning
|
||||
@@ -1853,7 +1961,7 @@ describe('InputHandler - async handlers', () => {
|
||||
assert.deepEqual(getLines(bufferService, 2), ['hello world!', 'second line']);
|
||||
});
|
||||
it('async DCS between', async () => {
|
||||
inputHandler.registerDcsHandler({final: 'a'}, async (data, params) => {
|
||||
inputHandler.registerDcsHandler({ final: 'a' }, async (data, params) => {
|
||||
await new Promise(res => setTimeout(res, 50));
|
||||
assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']);
|
||||
assert.equal(data, 'some data');
|
||||
|
||||
@@ -240,8 +240,6 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
public get onRequestRefreshRows(): IEvent<number, number> { return this._onRequestRefreshRows.event; }
|
||||
private _onRequestReset = new EventEmitter<void>();
|
||||
public get onRequestReset(): IEvent<void> { return this._onRequestReset.event; }
|
||||
private _onRequestScroll = new EventEmitter<IAttributeData, boolean | void>();
|
||||
public get onRequestScroll(): IEvent<IAttributeData, boolean | void> { return this._onRequestScroll.event; }
|
||||
private _onRequestSyncScrollBar = new EventEmitter<void>();
|
||||
public get onRequestSyncScrollBar(): IEvent<void> { return this._onRequestSyncScrollBar.event; }
|
||||
private _onRequestWindowsOptionsReport = new EventEmitter<WindowsOptionsReportType>();
|
||||
@@ -651,7 +649,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
buffer.y++;
|
||||
if (buffer.y === buffer.scrollBottom + 1) {
|
||||
buffer.y--;
|
||||
this._onRequestScroll.fire(this._eraseAttrData(), true);
|
||||
this._bufferService.scroll(this._eraseAttrData(), true);
|
||||
} else {
|
||||
if (buffer.y >= this._bufferService.rows) {
|
||||
buffer.y = this._bufferService.rows - 1;
|
||||
@@ -791,7 +789,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
buffer.y++;
|
||||
if (buffer.y === buffer.scrollBottom + 1) {
|
||||
buffer.y--;
|
||||
this._onRequestScroll.fire(this._eraseAttrData());
|
||||
this._bufferService.scroll(this._eraseAttrData());
|
||||
} else if (buffer.y >= this._bufferService.rows) {
|
||||
buffer.y = this._bufferService.rows - 1;
|
||||
}
|
||||
@@ -2856,7 +2854,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
protected _parseAnsiColorChange(data: string): IAnsiColorChangeEvent | null {
|
||||
const result: IAnsiColorChangeEvent = { colors: [] };
|
||||
// example data: 5;rgb:aa/bb/cc
|
||||
const regex = /(\d+);rgb:([0-9a-f]{2})\/([0-9a-f]{2})\/([0-9a-f]{2})/gi;
|
||||
const regex = /(\d+);rgb:([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})/gi;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(data)) !== null) {
|
||||
@@ -2987,7 +2985,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._bufferService.buffer.y++;
|
||||
if (buffer.y === buffer.scrollBottom + 1) {
|
||||
buffer.y--;
|
||||
this._onRequestScroll.fire(this._eraseAttrData());
|
||||
this._bufferService.scroll(this._eraseAttrData());
|
||||
} else if (buffer.y >= this._bufferService.rows) {
|
||||
buffer.y = this._bufferService.rows - 1;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { clone } from 'common/Clone';
|
||||
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
|
||||
import { IBufferSet, IBuffer } from 'common/buffer/Types';
|
||||
import { BufferSet } from 'common/buffer/BufferSet';
|
||||
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes } from 'common/Types';
|
||||
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData } from 'common/Types';
|
||||
import { UnicodeV6 } from 'common/input/UnicodeV6';
|
||||
|
||||
export class MockBufferService implements IBufferService {
|
||||
@@ -17,6 +17,7 @@ export class MockBufferService implements IBufferService {
|
||||
public get buffer(): IBuffer { return this.buffers.active; }
|
||||
public buffers: IBufferSet = {} as any;
|
||||
public onResize: IEvent<{ cols: number, rows: number }> = new EventEmitter<{ cols: number, rows: number }>().event;
|
||||
public onScroll: IEvent<number> = new EventEmitter<number>().event;
|
||||
public isUserScrolling: boolean = false;
|
||||
constructor(
|
||||
public cols: number,
|
||||
@@ -25,23 +26,41 @@ export class MockBufferService implements IBufferService {
|
||||
) {
|
||||
this.buffers = new BufferSet(optionsService, this);
|
||||
}
|
||||
public scrollPages(pageCount: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public scrollToTop(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public scrollToLine(line: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public scroll(eraseAttr: IAttributeData, isWrapped: boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public scrollToBottom(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public scrollLines(disp: number, suppressScrollEvent?: boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public resize(cols: number, rows: number): void {
|
||||
this.cols = cols;
|
||||
this.rows = rows;
|
||||
}
|
||||
public reset(): void {}
|
||||
public reset(): void { }
|
||||
}
|
||||
|
||||
export class MockCoreMouseService implements ICoreMouseService {
|
||||
public areMouseEventsActive: boolean = false;
|
||||
public activeEncoding: string = '';
|
||||
public activeProtocol: string = '';
|
||||
public addEncoding(name: string): void {}
|
||||
public addProtocol(name: string): void {}
|
||||
public reset(): void {}
|
||||
public addEncoding(name: string): void { }
|
||||
public addProtocol(name: string): void { }
|
||||
public reset(): void { }
|
||||
public triggerMouseEvent(event: ICoreMouseEvent): boolean { return false; }
|
||||
public onProtocolChange: IEvent<CoreMouseEventType> = new EventEmitter<CoreMouseEventType>().event;
|
||||
public explainEvents(events: CoreMouseEventType): {[event: string]: boolean} {
|
||||
public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
@@ -50,9 +69,9 @@ export class MockCharsetService implements ICharsetService {
|
||||
public serviceBrand: any;
|
||||
public charset: ICharset | undefined;
|
||||
public glevel: number = 0;
|
||||
public reset(): void {}
|
||||
public setgLevel(g: number): void {}
|
||||
public setgCharset(g: number, charset: ICharset): void {}
|
||||
public reset(): void { }
|
||||
public setgLevel(g: number): void { }
|
||||
public setgCharset(g: number, charset: ICharset): void { }
|
||||
}
|
||||
|
||||
export class MockCoreService implements ICoreService {
|
||||
@@ -75,28 +94,28 @@ export class MockCoreService implements ICoreService {
|
||||
public onData: IEvent<string> = new EventEmitter<string>().event;
|
||||
public onUserInput: IEvent<void> = new EventEmitter<void>().event;
|
||||
public onBinary: IEvent<string> = new EventEmitter<string>().event;
|
||||
public reset(): void {}
|
||||
public triggerDataEvent(data: string, wasUserInput?: boolean): void {}
|
||||
public triggerBinaryEvent(data: string): void {}
|
||||
public reset(): void { }
|
||||
public triggerDataEvent(data: string, wasUserInput?: boolean): void { }
|
||||
public triggerBinaryEvent(data: string): void { }
|
||||
}
|
||||
|
||||
export class MockDirtyRowService implements IDirtyRowService {
|
||||
public serviceBrand: any;
|
||||
public start: number = 0;
|
||||
public end: number = 0;
|
||||
public clearRange(): void {}
|
||||
public markDirty(y: number): void {}
|
||||
public markRangeDirty(y1: number, y2: number): void {}
|
||||
public markAllDirty(): void {}
|
||||
public clearRange(): void { }
|
||||
public markDirty(y: number): void { }
|
||||
public markRangeDirty(y1: number, y2: number): void { }
|
||||
public markAllDirty(): void { }
|
||||
}
|
||||
|
||||
export class MockLogService implements ILogService {
|
||||
public serviceBrand: any;
|
||||
public logLevel = LogLevelEnum.DEBUG;
|
||||
public debug(message: any, ...optionalParams: any[]): void {}
|
||||
public info(message: any, ...optionalParams: any[]): void {}
|
||||
public warn(message: any, ...optionalParams: any[]): void {}
|
||||
public error(message: any, ...optionalParams: any[]): void {}
|
||||
public debug(message: any, ...optionalParams: any[]): void { }
|
||||
public info(message: any, ...optionalParams: any[]): void { }
|
||||
public warn(message: any, ...optionalParams: any[]): void { }
|
||||
public error(message: any, ...optionalParams: any[]): void { }
|
||||
}
|
||||
|
||||
export class MockOptionsService implements IOptionsService {
|
||||
|
||||
Vendored
+10
-1
@@ -42,6 +42,16 @@ export interface IKeyboardEvent {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface IScrollEvent {
|
||||
position: number;
|
||||
source: ScrollSource;
|
||||
}
|
||||
|
||||
export const enum ScrollSource {
|
||||
TERMINAL,
|
||||
VIEWPORT,
|
||||
}
|
||||
|
||||
export interface ICircularList<T> {
|
||||
length: number;
|
||||
maxLength: number;
|
||||
@@ -347,7 +357,6 @@ export interface IAnsiColorChangeEvent {
|
||||
*/
|
||||
export interface IInputHandler {
|
||||
onTitleChange: IEvent<string>;
|
||||
onRequestScroll: IEvent<IAttributeData, boolean | void>;
|
||||
|
||||
parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise<boolean>;
|
||||
print(data: Uint32Array, start: number, end: number): void;
|
||||
|
||||
@@ -85,5 +85,26 @@ describe('WriteBuffer', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('writeSync called from action does not overflow callstack - issue #3265', () => {
|
||||
wb = new WriteBuffer(data => {
|
||||
const num = parseInt(data as string);
|
||||
if (num < 1000000) {
|
||||
wb.writeSync('' + (num + 1));
|
||||
}
|
||||
});
|
||||
wb.writeSync('1');
|
||||
});
|
||||
it('writeSync maxSubsequentCalls argument', () => {
|
||||
let last: string = '';
|
||||
wb = new WriteBuffer(data => {
|
||||
last = data as string;
|
||||
const num = parseInt(data as string);
|
||||
if (num < 1000000) {
|
||||
wb.writeSync('' + (num + 1), 10);
|
||||
}
|
||||
});
|
||||
wb.writeSync('1', 10);
|
||||
assert.equal(last, '11'); // 1 + 10 sub calls = 11
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,31 +42,55 @@ export class WriteBuffer {
|
||||
private _callbacks: ((() => void) | undefined)[] = [];
|
||||
private _pendingData = 0;
|
||||
private _bufferOffset = 0;
|
||||
private _isSyncWriting = false;
|
||||
private _syncCalls = 0;
|
||||
|
||||
constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise<boolean>) { }
|
||||
|
||||
/**
|
||||
* @deprecated Unreliable, to be removed soon.
|
||||
*/
|
||||
public writeSync(data: string | Uint8Array): void {
|
||||
public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void {
|
||||
// stop writeSync recursions with maxSubsequentCalls argument
|
||||
// This is dangerous to use as it will lose the current data chunk
|
||||
// and return immediately.
|
||||
if (maxSubsequentCalls !== undefined && this._syncCalls > maxSubsequentCalls) {
|
||||
// comment next line if a whole loop block should only contain x `writeSync` calls
|
||||
// (total flat vs. deep nested limit)
|
||||
this._syncCalls = 0;
|
||||
return;
|
||||
}
|
||||
// append chunk to buffer
|
||||
this._pendingData += data.length;
|
||||
this._writeBuffer.push(data);
|
||||
this._callbacks.push(undefined);
|
||||
|
||||
// increase recursion counter
|
||||
this._syncCalls++;
|
||||
// exit early if another writeSync loop is active
|
||||
if (this._isSyncWriting) {
|
||||
return;
|
||||
}
|
||||
this._isSyncWriting = true;
|
||||
|
||||
// force sync processing on pending data chunks to avoid in-band data scrambling
|
||||
// does the same as innerWrite but without event loop
|
||||
if (this._writeBuffer.length) {
|
||||
for (let i = this._bufferOffset; i < this._writeBuffer.length; ++i) {
|
||||
const data = this._writeBuffer[i];
|
||||
const cb = this._callbacks[i];
|
||||
this._action(data);
|
||||
if (cb) cb();
|
||||
}
|
||||
// reset all to avoid reprocessing of chunks with scheduled innerWrite call
|
||||
this._writeBuffer = [];
|
||||
this._callbacks = [];
|
||||
this._pendingData = 0;
|
||||
// stop scheduled innerWrite by offset > length condition
|
||||
this._bufferOffset = 0x7FFFFFFF;
|
||||
// we have to do it here as single loop steps to not corrupt loop subject
|
||||
// by another writeSync call triggered from _action
|
||||
let chunk: string | Uint8Array | undefined;
|
||||
while (chunk = this._writeBuffer.shift()) {
|
||||
this._action(chunk);
|
||||
const cb = this._callbacks.shift();
|
||||
if (cb) cb();
|
||||
}
|
||||
// handle current data chunk
|
||||
this._action(data);
|
||||
// reset to avoid reprocessing of chunks with scheduled innerWrite call
|
||||
// stopping scheduled innerWrite by offset > length condition
|
||||
this._pendingData = 0;
|
||||
this._bufferOffset = 0x7FFFFFFF;
|
||||
|
||||
// allow another writeSync to loop
|
||||
this._isSyncWriting = false;
|
||||
this._syncCalls = 0;
|
||||
}
|
||||
|
||||
public write(data: string | Uint8Array, callback?: () => void): void {
|
||||
@@ -191,8 +215,8 @@ export class WriteBuffer {
|
||||
}
|
||||
setTimeout(() => this._innerWrite());
|
||||
} else {
|
||||
this._writeBuffer = [];
|
||||
this._callbacks = [];
|
||||
this._writeBuffer.length = 0;
|
||||
this._callbacks.length = 0;
|
||||
this._pendingData = 0;
|
||||
this._bufferOffset = 0;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { BufferSet } from 'common/buffer/BufferSet';
|
||||
import { IBufferSet, IBuffer } from 'common/buffer/Types';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IAttributeData, IBufferLine, ScrollSource } from 'common/Types';
|
||||
|
||||
export const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars
|
||||
export const MINIMUM_ROWS = 1;
|
||||
@@ -23,9 +24,14 @@ export class BufferService extends Disposable implements IBufferService {
|
||||
|
||||
private _onResize = new EventEmitter<{ cols: number, rows: number }>();
|
||||
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
|
||||
private _onScroll = new EventEmitter<number>();
|
||||
public get onScroll(): IEvent<number> { return this._onScroll.event; }
|
||||
|
||||
public get buffer(): IBuffer { return this.buffers.active; }
|
||||
|
||||
/** An IBufferline to clone/copy from for new blank lines */
|
||||
private _cachedBlankLine: IBufferLine | undefined;
|
||||
|
||||
constructor(
|
||||
@IOptionsService private _optionsService: IOptionsService
|
||||
) {
|
||||
@@ -52,4 +58,128 @@ export class BufferService extends Disposable implements IBufferService {
|
||||
this.buffers.reset();
|
||||
this.isUserScrolling = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the terminal down 1 row, creating a blank line.
|
||||
* @param isWrapped Whether the new line is wrapped from the previous line.
|
||||
*/
|
||||
public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {
|
||||
const buffer = this.buffer;
|
||||
|
||||
let newLine: IBufferLine | undefined;
|
||||
newLine = this._cachedBlankLine;
|
||||
if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) {
|
||||
newLine = buffer.getBlankLine(eraseAttr, isWrapped);
|
||||
this._cachedBlankLine = newLine;
|
||||
}
|
||||
newLine.isWrapped = isWrapped;
|
||||
|
||||
const topRow = buffer.ybase + buffer.scrollTop;
|
||||
const bottomRow = buffer.ybase + buffer.scrollBottom;
|
||||
|
||||
if (buffer.scrollTop === 0) {
|
||||
// Determine whether the buffer is going to be trimmed after insertion.
|
||||
const willBufferBeTrimmed = buffer.lines.isFull;
|
||||
|
||||
// Insert the line using the fastest method
|
||||
if (bottomRow === buffer.lines.length - 1) {
|
||||
if (willBufferBeTrimmed) {
|
||||
buffer.lines.recycle().copyFrom(newLine);
|
||||
} else {
|
||||
buffer.lines.push(newLine.clone());
|
||||
}
|
||||
} else {
|
||||
buffer.lines.splice(bottomRow + 1, 0, newLine.clone());
|
||||
}
|
||||
|
||||
// Only adjust ybase and ydisp when the buffer is not trimmed
|
||||
if (!willBufferBeTrimmed) {
|
||||
buffer.ybase++;
|
||||
// Only scroll the ydisp with ybase if the user has not scrolled up
|
||||
if (!this.isUserScrolling) {
|
||||
buffer.ydisp++;
|
||||
}
|
||||
} else {
|
||||
// When the buffer is full and the user has scrolled up, keep the text
|
||||
// stable unless ydisp is right at the top
|
||||
if (this.isUserScrolling) {
|
||||
buffer.ydisp = Math.max(buffer.ydisp - 1, 0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// scrollTop is non-zero which means no line will be going to the
|
||||
// scrollback, instead we can just shift them in-place.
|
||||
const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */;
|
||||
buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1);
|
||||
buffer.lines.set(bottomRow, newLine.clone());
|
||||
}
|
||||
|
||||
// Move the viewport to the bottom of the buffer unless the user is
|
||||
// scrolling.
|
||||
if (!this.isUserScrolling) {
|
||||
buffer.ydisp = buffer.ybase;
|
||||
}
|
||||
|
||||
this._onScroll.fire(buffer.ydisp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the display of the terminal
|
||||
* @param disp The number of lines to scroll down (negative scroll up).
|
||||
* @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used
|
||||
* to avoid unwanted events being handled by the viewport when the event was triggered from the
|
||||
* viewport originally.
|
||||
*/
|
||||
public scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void {
|
||||
const buffer = this.buffer;
|
||||
if (disp < 0) {
|
||||
if (buffer.ydisp === 0) {
|
||||
return;
|
||||
}
|
||||
this.isUserScrolling = true;
|
||||
} else if (disp + buffer.ydisp >= buffer.ybase) {
|
||||
this.isUserScrolling = false;
|
||||
}
|
||||
|
||||
const oldYdisp = buffer.ydisp;
|
||||
buffer.ydisp = Math.max(Math.min(buffer.ydisp + disp, buffer.ybase), 0);
|
||||
|
||||
// No change occurred, don't trigger scroll/refresh
|
||||
if (oldYdisp === buffer.ydisp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!suppressScrollEvent) {
|
||||
this._onScroll.fire(buffer.ydisp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the display of the terminal by a number of pages.
|
||||
* @param pageCount The number of pages to scroll (negative scrolls up).
|
||||
*/
|
||||
public scrollPages(pageCount: number): void {
|
||||
this.scrollLines(pageCount * (this.rows - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the display of the terminal to the top.
|
||||
*/
|
||||
public scrollToTop(): void {
|
||||
this.scrollLines(-this.buffer.ydisp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the display of the terminal to the bottom.
|
||||
*/
|
||||
public scrollToBottom(): void {
|
||||
this.scrollLines(this.buffer.ybase - this.buffer.ydisp);
|
||||
}
|
||||
|
||||
public scrollToLine(line: number): void {
|
||||
const scrollAmount = line - this.buffer.ydisp;
|
||||
if (scrollAmount !== 0) {
|
||||
this.scrollLines(scrollAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
import { IBuffer, IBufferSet } from 'common/buffer/Types';
|
||||
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes } from 'common/Types';
|
||||
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource } from 'common/Types';
|
||||
import { createDecorator } from 'common/services/ServiceRegistry';
|
||||
|
||||
export const IBufferService = createDecorator<IBufferService>('BufferService');
|
||||
@@ -17,9 +17,14 @@ export interface IBufferService {
|
||||
readonly buffer: IBuffer;
|
||||
readonly buffers: IBufferSet;
|
||||
isUserScrolling: boolean;
|
||||
|
||||
onResize: IEvent<{ cols: number, rows: number }>;
|
||||
|
||||
onScroll: IEvent<number>;
|
||||
scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void;
|
||||
scrollToBottom(): void;
|
||||
scrollToTop(): void;
|
||||
scrollToLine(line: number): void;
|
||||
scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void;
|
||||
scrollPages(pageCount: number): void;
|
||||
resize(cols: number, rows: number): void;
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
@@ -4820,9 +4820,9 @@ xterm-benchmark@^0.1.3:
|
||||
typescript "^3.5.1"
|
||||
|
||||
y18n@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
|
||||
integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"
|
||||
integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==
|
||||
|
||||
yargs-parser@13.1.2, yargs-parser@^13.1.2:
|
||||
version "13.1.2"
|
||||
|
||||
Reference in New Issue
Block a user