From 14b02b9ad5d73450186b07be5e3e5bfc0982888b Mon Sep 17 00:00:00 2001 From: Svante Boberg Date: Mon, 1 Aug 2022 10:17:10 +0200 Subject: [PATCH 01/36] Bump minimum target to es2015 Now that we no longer support IE we can raise the minimum ES target a bit. Changing to es2015 shaves off ~17% from the main bundle size, also it prevents generators from being transpiled into regular functions etc. so I would guess it has the potential to give some general performance improvements too. For fun I also tried to raise it to es2022, this gave a slightly bigger bundle than es2015 so I think es2015 could be an ok target unless we need something newer for some reason. --- addons/xterm-addon-attach/src/tsconfig.json | 2 +- addons/xterm-addon-attach/test/tsconfig.json | 2 +- addons/xterm-addon-canvas/src/tsconfig.json | 2 +- addons/xterm-addon-fit/src/tsconfig.json | 2 +- addons/xterm-addon-fit/test/tsconfig.json | 2 +- addons/xterm-addon-search/src/tsconfig.json | 2 +- addons/xterm-addon-search/test/tsconfig.json | 2 +- addons/xterm-addon-serialize/src/tsconfig.json | 2 +- addons/xterm-addon-serialize/test/tsconfig.json | 2 +- addons/xterm-addon-web-links/src/tsconfig.json | 2 +- addons/xterm-addon-web-links/test/tsconfig.json | 2 +- addons/xterm-addon-webgl/src/tsconfig.json | 2 +- addons/xterm-addon-webgl/test/tsconfig.json | 2 +- src/tsconfig-base.json | 4 ++-- 14 files changed, 15 insertions(+), 15 deletions(-) diff --git a/addons/xterm-addon-attach/src/tsconfig.json b/addons/xterm-addon-attach/src/tsconfig.json index 54bfc38b..f3e409d1 100644 --- a/addons/xterm-addon-attach/src/tsconfig.json +++ b/addons/xterm-addon-attach/src/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es2015", "lib": [ "dom", "es2015" diff --git a/addons/xterm-addon-attach/test/tsconfig.json b/addons/xterm-addon-attach/test/tsconfig.json index 1c772984..fa67b169 100644 --- a/addons/xterm-addon-attach/test/tsconfig.json +++ b/addons/xterm-addon-attach/test/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es2015", "lib": [ "es2015" ], diff --git a/addons/xterm-addon-canvas/src/tsconfig.json b/addons/xterm-addon-canvas/src/tsconfig.json index 206d52ae..f752dc5b 100644 --- a/addons/xterm-addon-canvas/src/tsconfig.json +++ b/addons/xterm-addon-canvas/src/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es2015", "lib": [ "dom", "es6" diff --git a/addons/xterm-addon-fit/src/tsconfig.json b/addons/xterm-addon-fit/src/tsconfig.json index 54bfc38b..f3e409d1 100644 --- a/addons/xterm-addon-fit/src/tsconfig.json +++ b/addons/xterm-addon-fit/src/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es2015", "lib": [ "dom", "es2015" diff --git a/addons/xterm-addon-fit/test/tsconfig.json b/addons/xterm-addon-fit/test/tsconfig.json index 1c772984..fa67b169 100644 --- a/addons/xterm-addon-fit/test/tsconfig.json +++ b/addons/xterm-addon-fit/test/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es2015", "lib": [ "es2015" ], diff --git a/addons/xterm-addon-search/src/tsconfig.json b/addons/xterm-addon-search/src/tsconfig.json index 5a5e671f..9ef27b5c 100644 --- a/addons/xterm-addon-search/src/tsconfig.json +++ b/addons/xterm-addon-search/src/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es2015", "lib": [ "dom", "es6", diff --git a/addons/xterm-addon-search/test/tsconfig.json b/addons/xterm-addon-search/test/tsconfig.json index df82a457..052fac43 100644 --- a/addons/xterm-addon-search/test/tsconfig.json +++ b/addons/xterm-addon-search/test/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es2015", "lib": [ "es6", ], diff --git a/addons/xterm-addon-serialize/src/tsconfig.json b/addons/xterm-addon-serialize/src/tsconfig.json index 38ef6a7f..ba26f22e 100644 --- a/addons/xterm-addon-serialize/src/tsconfig.json +++ b/addons/xterm-addon-serialize/src/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es2015", "lib": [ "dom", "es2015" diff --git a/addons/xterm-addon-serialize/test/tsconfig.json b/addons/xterm-addon-serialize/test/tsconfig.json index 7afbc858..971f3e92 100644 --- a/addons/xterm-addon-serialize/test/tsconfig.json +++ b/addons/xterm-addon-serialize/test/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es2015", "lib": [ "es2015" ], diff --git a/addons/xterm-addon-web-links/src/tsconfig.json b/addons/xterm-addon-web-links/src/tsconfig.json index 54bfc38b..f3e409d1 100644 --- a/addons/xterm-addon-web-links/src/tsconfig.json +++ b/addons/xterm-addon-web-links/src/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es2015", "lib": [ "dom", "es2015" diff --git a/addons/xterm-addon-web-links/test/tsconfig.json b/addons/xterm-addon-web-links/test/tsconfig.json index 9f4d23df..48975764 100644 --- a/addons/xterm-addon-web-links/test/tsconfig.json +++ b/addons/xterm-addon-web-links/test/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es2015", "lib": [ "es2015" ], diff --git a/addons/xterm-addon-webgl/src/tsconfig.json b/addons/xterm-addon-webgl/src/tsconfig.json index 206d52ae..f752dc5b 100644 --- a/addons/xterm-addon-webgl/src/tsconfig.json +++ b/addons/xterm-addon-webgl/src/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es2015", "lib": [ "dom", "es6" diff --git a/addons/xterm-addon-webgl/test/tsconfig.json b/addons/xterm-addon-webgl/test/tsconfig.json index 9c7f602f..1ff9217e 100644 --- a/addons/xterm-addon-webgl/test/tsconfig.json +++ b/addons/xterm-addon-webgl/test/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es2015", "lib": [ "es6", ], diff --git a/src/tsconfig-base.json b/src/tsconfig-base.json index 0cd951a7..cf4454a4 100644 --- a/src/tsconfig-base.json +++ b/src/tsconfig-base.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "target": "es5", - "lib": [ "es5" ], + "module": "commonjs", + "target": "es2015", "rootDir": ".", "sourceMap": true, From a1385f589b25b67f86a15485a0f3a71e37c2a6cb Mon Sep 17 00:00:00 2001 From: Simon Lamon Date: Fri, 5 Aug 2022 16:16:12 +0000 Subject: [PATCH 02/36] Required options --- .../src/atlas/CharAtlasCache.ts | 4 +- .../src/atlas/CharAtlasUtils.ts | 4 +- addons/xterm-addon-webgl/src/WebglRenderer.ts | 6 +- .../src/atlas/CharAtlasUtils.ts | 20 +++--- .../src/renderLayer/CursorRenderLayer.ts | 2 +- demo/client.ts | 2 +- src/browser/TestUtils.test.ts | 2 +- src/browser/Types.d.ts | 2 +- src/browser/public/Terminal.ts | 4 +- src/common/CoreTerminal.ts | 2 +- src/common/TestUtils.test.ts | 4 +- src/common/services/OptionsService.ts | 8 +-- src/common/services/Services.ts | 72 +++++++++---------- src/headless/Terminal.ts | 2 +- src/headless/public/Terminal.ts | 4 +- typings/xterm-headless.d.ts | 13 ++-- typings/xterm.d.ts | 15 ++-- 17 files changed, 84 insertions(+), 82 deletions(-) diff --git a/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts b/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts index 7d020dcc..0c78808e 100644 --- a/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts +++ b/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts @@ -8,7 +8,7 @@ import { BaseCharAtlas } from './BaseCharAtlas'; import { DynamicCharAtlas } from './DynamicCharAtlas'; import { ICharAtlasConfig } from './Types'; import { IColorSet } from 'browser/Types'; -import { ITerminalOptions } from 'common/services/Services'; +import { ITerminalOptions } from 'xterm'; interface ICharAtlasCacheEntry { atlas: BaseCharAtlas; @@ -25,7 +25,7 @@ const charAtlasCache: ICharAtlasCacheEntry[] = []; * one that is in use by another terminal. */ export function acquireCharAtlas( - options: ITerminalOptions, + options: Required, rendererId: number, colors: IColorSet, scaledCharWidth: number, diff --git a/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts index 5d44b859..b5674304 100644 --- a/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts @@ -6,9 +6,9 @@ import { ICharAtlasConfig } from './Types'; import { DEFAULT_COLOR } from 'common/buffer/Constants'; import { IColorSet, IPartialColorSet } from 'browser/Types'; -import { ITerminalOptions } from 'common/services/Services'; +import { ITerminalOptions } from 'xterm'; -export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, options: ITerminalOptions, colors: IColorSet): ICharAtlasConfig { +export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, options: Required, colors: IColorSet): ICharAtlasConfig { // null out some fields that don't matter const clonedColors: IPartialColorSet = { foreground: colors.foreground, diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 9e90df02..4848162d 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -552,18 +552,18 @@ export class WebglRenderer extends Disposable implements IRenderer { // Calculate the scaled cell height, if lineHeight is _not_ 1, the resulting value will be // floored since lineHeight can never be lower then 1, this guarentees the scaled cell height // will always be larger than scaled char height. - this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight!); + this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); // Calculate the y offset within a cell that glyph should draw at in order for it to be centered // correctly within the cell. this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); // Calculate the scaled cell width, taking the letterSpacing into account. - this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing!); + this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing); // Calculate the x offset with a cell that text should draw from in order for it to be centered // correctly within the cell. - this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing! / 2); + this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing / 2); // Recalculate the canvas dimensions, the scaled dimensions define the actual number of pixel in // the canvas diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts index 86f7e5ab..18de5739 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts @@ -32,21 +32,21 @@ export function generateConfig(scaledCellWidth: number, scaledCellHeight: number contrastCache: colors.contrastCache }; return { - customGlyphs: terminal.options.customGlyphs!, + customGlyphs: terminal.options.customGlyphs, devicePixelRatio: window.devicePixelRatio, - letterSpacing: terminal.options.letterSpacing!, - lineHeight: terminal.options.lineHeight!, + letterSpacing: terminal.options.letterSpacing, + lineHeight: terminal.options.lineHeight, scaledCellWidth, scaledCellHeight, scaledCharWidth, scaledCharHeight, - fontFamily: terminal.options.fontFamily!, - fontSize: terminal.options.fontSize!, - fontWeight: terminal.options.fontWeight as FontWeight, - fontWeightBold: terminal.options.fontWeightBold as FontWeight, - allowTransparency: terminal.options.allowTransparency!, - drawBoldTextInBrightColors: terminal.options.drawBoldTextInBrightColors!, - minimumContrastRatio: terminal.options.minimumContrastRatio!, + fontFamily: terminal.options.fontFamily, + fontSize: terminal.options.fontSize, + fontWeight: terminal.options.fontWeight, + fontWeightBold: terminal.options.fontWeightBold, + allowTransparency: terminal.options.allowTransparency, + drawBoldTextInBrightColors: terminal.options.drawBoldTextInBrightColors, + minimumContrastRatio: terminal.options.minimumContrastRatio, colors: clonedColors }; } diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts index 04e2b387..3c804b3d 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts @@ -213,7 +213,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; - this._fillLeftLineAtCell(x, y, terminal.options.cursorWidth!); + this._fillLeftLineAtCell(x, y, terminal.options.cursorWidth); this._ctx.restore(); } diff --git a/demo/client.ts b/demo/client.ts index 64c694f5..175b61ba 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -343,7 +343,7 @@ function initOptions(term: TerminalType): void { ]; const stringOptions = { cursorStyle: ['block', 'underline', 'bar'], - fastScrollModifier: ['alt', 'ctrl', 'shift', undefined], + fastScrollModifier: ['none', 'alt', 'ctrl', 'shift'], fontFamily: null, fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index e09097a2..fa33802b 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -141,7 +141,7 @@ export class MockTerminal implements ITerminal { public renderer!: IRenderer; public linkifier2!: ILinkifier2; public isFocused!: boolean; - public options: ITerminalOptions = {}; + public options!: Required; public element!: HTMLElement; public screenElement!: HTMLElement; public rowContainer!: HTMLElement; diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index a3c27a88..48461a5e 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -16,7 +16,7 @@ export interface ITerminal extends IPublicTerminal, ICoreTerminal { browser: IBrowser; buffer: IBuffer; viewport: IViewport | undefined; - options: ITerminalOptions; + options: Required; linkifier2: ILinkifier2; onBlur: IEvent; diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 57efdbd0..daf064ac 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -24,7 +24,7 @@ export class Terminal implements ITerminalApi { private _addonManager: AddonManager; private _parser: IParser | undefined; private _buffer: BufferNamespaceApi | undefined; - private _publicOptions: ITerminalOptions; + private _publicOptions: Required; constructor(options?: ITerminalOptions) { this._core = new TerminalCore(options); @@ -123,7 +123,7 @@ export class Terminal implements ITerminalApi { wraparoundMode: m.wraparound }; } - public get options(): ITerminalOptions { + public get options(): Required { return this._publicOptions; } public set options(options: ITerminalOptions) { diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index af9ec3f9..20761732 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -88,7 +88,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { public get cols(): number { return this._bufferService.cols; } public get rows(): number { return this._bufferService.rows; } public get buffers(): IBufferSet { return this._bufferService.buffers; } - public get options(): ITerminalOptions { return this.optionsService.options; } + public get options(): Required { return this.optionsService.options; } public set options(options: ITerminalOptions) { for (const key in options) { this.optionsService.options[key] = options[key]; diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 48f3a69e..8f941fff 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -121,8 +121,8 @@ export class MockLogService implements ILogService { export class MockOptionsService implements IOptionsService { public serviceBrand: any; - public readonly rawOptions: ITerminalOptions = clone(DEFAULT_OPTIONS); - public options: ITerminalOptions = this.rawOptions; + public readonly rawOptions: Required = clone(DEFAULT_OPTIONS); + public options: Required = this.rawOptions; public onOptionChange: IEvent = new EventEmitter().event; constructor(testOptions?: Partial) { if (testOptions) { diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index ab9edfbf..1e4f53ce 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -8,7 +8,7 @@ import { EventEmitter, IEvent } from 'common/EventEmitter'; import { isMac } from 'common/Platform'; import { CursorStyle } from 'common/Types'; -export const DEFAULT_OPTIONS: Readonly = { +export const DEFAULT_OPTIONS: Readonly> = { cols: 80, rows: 24, cursorBlink: false, @@ -45,7 +45,7 @@ export const DEFAULT_OPTIONS: Readonly = { convertEol: false, termName: 'xterm', cancelEvents: false, - overviewRulerWidth: undefined + overviewRulerWidth: 0 }; const FONT_WEIGHT_OPTIONS: Extract[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']; @@ -53,8 +53,8 @@ const FONT_WEIGHT_OPTIONS: Extract[] = ['normal', 'bold', '1 export class OptionsService implements IOptionsService { public serviceBrand: any; - public readonly rawOptions: ITerminalOptions; - public options: ITerminalOptions; + public readonly rawOptions: Required; + public options: Required; private _onOptionChange = new EventEmitter(); public get onOptionChange(): IEvent { return this._onOptionChange.event; } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 585b29ac..ab21ecef 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -194,8 +194,8 @@ export interface IOptionsService { * single options without any validation as we trust TypeScript to enforce correct usage * internally. */ - readonly rawOptions: Readonly; - readonly options: ITerminalOptions; + readonly rawOptions: Required; + readonly options: Required; readonly onOptionChange: IEvent; } @@ -204,40 +204,40 @@ export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '50 export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off'; export interface ITerminalOptions { - allowProposedApi: boolean; - allowTransparency: boolean; - altClickMovesCursor: boolean; - cols: number; - convertEol: boolean; - cursorBlink: boolean; - cursorStyle: CursorStyle; - cursorWidth: number; - customGlyphs: boolean; - disableStdin: boolean; - drawBoldTextInBrightColors: boolean; - fastScrollModifier: 'alt' | 'ctrl' | 'shift' | undefined; - fastScrollSensitivity: number; - fontSize: number; - fontFamily: string; - fontWeight: FontWeight; - fontWeightBold: FontWeight; - letterSpacing: number; - lineHeight: number; - logLevel: LogLevel; - macOptionIsMeta: boolean; - macOptionClickForcesSelection: boolean; - minimumContrastRatio: number; - rightClickSelectsWord: boolean; - rows: number; - screenReaderMode: boolean; - scrollback: number; - scrollSensitivity: number; - smoothScrollDuration: number; - tabStopWidth: number; - theme: ITheme; - windowsMode: boolean; - windowOptions: IWindowOptions; - wordSeparator: string; + allowProposedApi?: boolean; + allowTransparency?: boolean; + altClickMovesCursor?: boolean; + cols?: number; + convertEol?: boolean; + cursorBlink?: boolean; + cursorStyle?: CursorStyle; + cursorWidth?: number; + customGlyphs?: boolean; + disableStdin?: boolean; + drawBoldTextInBrightColors?: boolean; + fastScrollModifier?: 'none' | 'alt' | 'ctrl' | 'shift'; + fastScrollSensitivity?: number; + fontSize?: number; + fontFamily?: string; + fontWeight?: FontWeight; + fontWeightBold?: FontWeight; + letterSpacing?: number; + lineHeight?: number; + logLevel?: LogLevel; + macOptionIsMeta?: boolean; + macOptionClickForcesSelection?: boolean; + minimumContrastRatio?: number; + rightClickSelectsWord?: boolean; + rows?: number; + screenReaderMode?: boolean; + scrollback?: number; + scrollSensitivity?: number; + smoothScrollDuration?: number; + tabStopWidth?: number; + theme?: ITheme; + windowsMode?: boolean; + windowOptions?: IWindowOptions; + wordSeparator?: string; overviewRulerWidth?: number; [key: string]: any; diff --git a/src/headless/Terminal.ts b/src/headless/Terminal.ts index 7f138ce1..1cad0ee2 100644 --- a/src/headless/Terminal.ts +++ b/src/headless/Terminal.ts @@ -30,7 +30,7 @@ import { IMarker, ITerminalOptions, ScrollSource } from 'common/Types'; export class Terminal extends CoreTerminal { // TODO: We should remove options once components adopt optionsService - public get options(): IInitializedTerminalOptions { return this.optionsService.options; } + public get options(): Required { return this.optionsService.options; } private _onBell = new EventEmitter(); public get onBell(): IEvent { return this._onBell.event; } diff --git a/src/headless/public/Terminal.ts b/src/headless/public/Terminal.ts index efc0a8a1..d4360c7b 100644 --- a/src/headless/public/Terminal.ts +++ b/src/headless/public/Terminal.ts @@ -21,7 +21,7 @@ export class Terminal implements ITerminalApi { private _addonManager: AddonManager; private _parser: IParser | undefined; private _buffer: BufferNamespaceApi | undefined; - private _publicOptions: ITerminalOptions; + private _publicOptions: Required; constructor(options?: ITerminalOptions & ITerminalInitOnlyOptions) { this._core = new TerminalCore(options); @@ -123,7 +123,7 @@ export class Terminal implements ITerminalApi { wraparoundMode: m.wraparound }; } - public get options(): ITerminalOptions { + public get options(): Required { return this._publicOptions; } public set options(options: ITerminalOptions) { diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index cda7ad1a..55114bf6 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -84,7 +84,7 @@ declare module 'xterm-headless' { /** * The modifier key hold to multiply scroll speed. */ - fastScrollModifier?: 'alt' | 'ctrl' | 'shift' | undefined; + fastScrollModifier?: 'none' | 'alt' | 'ctrl' | 'shift'; /** * The spacing in whole pixels between characters. @@ -538,11 +538,12 @@ declare module 'xterm-headless' { * ```typescript * console.log(terminal.options.fontSize); * ``` + */ + get options(): Required; + + /** + * Gets or sets the terminal options. This supports setting multiple options. * - * @example Set a single option - * ```typescript - * terminal.options.fontSize = 12; - * ``` * * @example Set multiple options * ```typescript @@ -552,7 +553,7 @@ declare module 'xterm-headless' { * }; * ``` */ - options: ITerminalOptions; + set options(options: ITerminalOptions); /** * Natural language strings that can be localized. diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 05058d04..e62c5e70 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -91,7 +91,7 @@ declare module 'xterm' { /** * The modifier key hold to multiply scroll speed. */ - fastScrollModifier?: 'alt' | 'ctrl' | 'shift' | undefined; + fastScrollModifier?: 'none' | 'alt' | 'ctrl' | 'shift'; /** * The scroll speed multiplier used for fast scrolling. @@ -700,11 +700,12 @@ declare module 'xterm' { * ```typescript * console.log(terminal.options.fontSize); * ``` + */ + get options(): Required; + + /** + * Gets or sets the terminal options. This supports setting multiple options. * - * @example Set a single option - * ```typescript - * terminal.options.fontSize = 12; - * ``` * * @example Set multiple options * ```typescript @@ -714,7 +715,7 @@ declare module 'xterm' { * }; * ``` */ - options: ITerminalOptions; + set options(options: ITerminalOptions); /** * Natural language strings that can be localized. @@ -1069,7 +1070,7 @@ declare module 'xterm' { /** * An object representing a range within the viewport of the terminal. */ - export interface IViewportRange { + export interface IViewportRange { /** * The start of the range. */ From 6e123e90eec4cd1f223c8b7182e75b2bfde856ce Mon Sep 17 00:00:00 2001 From: Simon Lamon Date: Fri, 5 Aug 2022 16:56:54 +0000 Subject: [PATCH 03/36] Add example back --- typings/xterm-headless.d.ts | 4 ++++ typings/xterm.d.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 55114bf6..1c0a986a 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -544,6 +544,10 @@ declare module 'xterm-headless' { /** * Gets or sets the terminal options. This supports setting multiple options. * + * @example Set a single option + * ```typescript + * terminal.options.fontSize = 12; + * ``` * * @example Set multiple options * ```typescript diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index e62c5e70..e989cdbc 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -706,6 +706,10 @@ declare module 'xterm' { /** * Gets or sets the terminal options. This supports setting multiple options. * + * @example Set a single option + * ```typescript + * terminal.options.fontSize = 12; + * ``` * * @example Set multiple options * ```typescript From 91d46dae509b1940404dc9973760844c027e9674 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 7 Aug 2022 05:08:13 -0700 Subject: [PATCH 04/36] OSC link progress --- .../src/atlas/WebglCharAtlas.ts | 2 +- src/browser/OscLinkProvider.ts | 72 +++++++++++++++++++ src/browser/Terminal.ts | 2 + src/common/CoreTerminal.ts | 6 +- src/common/InputHandler.ts | 71 +++++++++++++++++- src/common/Types.d.ts | 15 +++- src/common/buffer/AttributeData.ts | 48 ++++++++++--- src/common/services/OscLinkService.ts | 22 ++++++ src/common/services/Services.ts | 14 +++- 9 files changed, 237 insertions(+), 15 deletions(-) create mode 100644 src/browser/OscLinkProvider.ts create mode 100644 src/common/services/OscLinkService.ts diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 13dceae6..182b3b3e 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -356,7 +356,7 @@ export class WebglCharAtlas implements IDisposable { private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number): IRasterizedGlyph { const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars; - + console.log('_drawToCache', chars, ext); this.hasCanvasChanged = true; // Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used diff --git a/src/browser/OscLinkProvider.ts b/src/browser/OscLinkProvider.ts new file mode 100644 index 00000000..c7594479 --- /dev/null +++ b/src/browser/OscLinkProvider.ts @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ILink, ILinkProvider } from 'browser/Types'; +import { CellData } from 'common/buffer/CellData'; +import { IBufferService, IOscLinkService } from 'common/services/Services'; + +export class OscLinkProvider implements ILinkProvider { + constructor( + @IBufferService private readonly _bufferService: IBufferService, + @IOscLinkService private readonly _oscLinkService: IOscLinkService + ) { + } + + public provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void { + const line = this._bufferService.buffer.lines.get(y - 1); + if (!line) { + callback(undefined); + return; + } + + const result: ILink[] = []; + const cell = new CellData(); + const lineLength = line.getTrimmedLength(); + let currentLinkId = -1; + let currentStart = -1; + let finishLink = false; + for (let x = 0; x < lineLength; x++) { + if (!line.hasContent(x)) { + continue; + } + + line.loadCell(x, cell); + if (cell.extended.urlId) { + if (currentStart === -1) { + currentStart = x; + currentLinkId = cell.extended.urlId; + continue; + } else { + finishLink = cell.extended.urlId !== currentLinkId; + } + } else { + if (currentStart !== -1) { + finishLink = true; + } + } + + if (finishLink || (currentStart !== -1 && x === lineLength - 1)) { + const text = this._oscLinkService.getLinkData(currentLinkId)?.uri; + if (text) { + // OSC links always use underline and pointer decorations + result.push({ + text, + // These ranges are 1-based + range: { + start: { x: currentStart + 1, y }, + end: { x: x + 1, y } + }, + activate(e, text) { + console.log('activate!', text); + } + // TODO: Embedder API to handle hover + }); + } + } + } + // TODO: Handle fetching and returning other link ranges to underline other links with the same id + callback(result); + } +} diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 57099ac2..aacd6cf7 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -55,6 +55,7 @@ import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRe import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer'; import { DecorationService } from 'common/services/DecorationService'; import { IDecorationService } from 'common/services/Services'; +import { OscLinkProvider } from 'browser/OscLinkProvider'; // Let it work inside Node.js for automated testing purposes. const document: Document = (typeof window !== 'undefined') ? window.document : null as any; @@ -163,6 +164,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this._setup(); this.linkifier2 = this.register(this._instantiationService.createInstance(Linkifier2)); + this.linkifier2.registerLinkProvider(this._instantiationService.createInstance(OscLinkProvider)); this._decorationService = this._instantiationService.createInstance(DecorationService); this._instantiationService.setService(IDecorationService, this._decorationService); diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index af9ec3f9..4a1c99ff 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -22,7 +22,7 @@ */ import { Disposable } from 'common/Lifecycle'; -import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum, ITerminalOptions } from 'common/services/Services'; +import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services'; import { InstantiationService } from 'common/services/InstantiationService'; import { LogService } from 'common/services/LogService'; import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService'; @@ -39,6 +39,7 @@ import { IFunctionIdentifier, IParams } from 'common/parser/Types'; import { IBufferSet } from 'common/buffer/Types'; import { InputHandler } from 'common/InputHandler'; import { WriteBuffer } from 'common/input/WriteBuffer'; +import { OscLinkService } from 'common/services/OscLinkService'; // Only trigger this warning a single time per session let hasWriteSyncWarnHappened = false; @@ -49,6 +50,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { protected readonly _logService: ILogService; protected readonly _charsetService: ICharsetService; protected readonly _dirtyRowService: IDirtyRowService; + protected readonly _oscLinkService: IOscLinkService; public readonly coreMouseService: ICoreMouseService; public readonly coreService: ICoreService; @@ -118,6 +120,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._instantiationService.setService(IUnicodeService, this.unicodeService); this._charsetService = this._instantiationService.createInstance(CharsetService); this._instantiationService.setService(ICharsetService, this._charsetService); + this._oscLinkService = this._instantiationService.createInstance(OscLinkService); + this._instantiationService.setService(IOscLinkService, this._oscLinkService); // Register input handler and handle/forward events this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this.coreService, this._dirtyRowService, this._logService, this.optionsService, this.coreMouseService, this.unicodeService); diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index d5b8d948..b613a24a 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -4,7 +4,7 @@ * @license MIT */ -import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex, ColorRequestType } from 'common/Types'; +import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex, ColorRequestType, IOscLinkData } from 'common/Types'; import { C0, C1 } from 'common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets'; import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; @@ -214,8 +214,6 @@ class DECRQSS implements IDcsHandler { * @vt: #N DCS XTSETTCAP "Set Terminfo Data" "DCS + p Pt ST" "Set Terminfo Data." */ - - /** * The terminal's standard implementation of IInputHandler, this handles all * input from the Parser. @@ -230,6 +228,7 @@ export class InputHandler extends Disposable implements IInputHandler { private _workCell: CellData = new CellData(); private _windowTitle = ''; private _iconName = ''; + private _currentHyperlink?: IOscLinkData; protected _windowTitleStack: string[] = []; protected _iconNameStack: string[] = []; @@ -265,6 +264,10 @@ export class InputHandler extends Disposable implements IInputHandler { public get onTitleChange(): IEvent { return this._onTitleChange.event; } private _onColor = new EventEmitter(); public get onColor(): IEvent { return this._onColor.event; } + private _onStartHyperlink = new EventEmitter(); + public get onStartHyperlink(): IEvent { return this._onStartHyperlink.event; } + private _onFinishHyperlink = new EventEmitter(); + public get onFinishHyperlink(): IEvent { return this._onFinishHyperlink.event; } private _parseStack: IParseStack = { paused: false, @@ -403,6 +406,8 @@ export class InputHandler extends Disposable implements IInputHandler { // 5 - Change Special Color Number // 6 - Enable/disable Special Color Number c // 7 - current directory? (not in xterm spec, see https://gitlab.com/gnachman/iterm2/issues/3939) + // 8 - create hyperlink (not in xterm spec, see https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda) + this._parser.registerOscHandler(8, new OscHandler(data => this.setHyperlink(data))); // 10 - Change VT100 text foreground color to Pt. this._parser.registerOscHandler(10, new OscHandler(data => this.setOrReportFgColor(data))); // 11 - Change VT100 text background color to Pt. @@ -2889,6 +2894,66 @@ export class InputHandler extends Disposable implements IInputHandler { return true; } + /** + * OSC 8 ; ; ST - create hyperlink + * OSC 8 ; ; ST - finish hyperlink + * + * Test case: + * + * ```sh + * printf '\e]8;;http://example.com\e\\This is a link\e]8;;\e\\\n' + * ``` + * + * @vt: #Y OSC 8 "Create hyperlink" "OSC 8 ; params ; uri BEL" "Create a hyperlink to `uri` using `params`." + * `uri` is a hyperlink starting with `http://`, `https://`, `ftp://`, `file://` or `mailto://`. `params` is an + * optional list of key=value assignments, separated by the : character. Example: `id=xyz123:foo=bar:baz=quux`. + * Currently only the id key is defined. Cells that share the same ID and URI share hover feedback. + * Use `OSC 8 ; ; BEL` to finish the current hyperlink. + */ + public setHyperlink(data: string): boolean { + const args = data.split(';'); + console.log('hyperlink', args); + if (args.length < 2) { + return false; + } + if (args[1]) { + return this._createHyperlink(args[0], args[1]); + } + if (args[0]) { + return false; + } + return this._finishHyperlink(); + } + + private _createHyperlink(params: string, uri: string): boolean { + // It's legal to open a new hyperlink without explicitly finishing the previous one + if (this._currentHyperlink) { + this._finishHyperlink(); + } + const parsedParams = params.split(':'); + let id: string | undefined; + const idParamIndex = parsedParams.findIndex(e => e.startsWith('id=')); + if (idParamIndex !== -1) { + id = parsedParams[idParamIndex].slice(3) || undefined; + } + this._currentHyperlink = { id, uri }; + this._curAttrData.extended = this._curAttrData.extended.clone(); + this._curAttrData.extended.urlId = 1; + this._curAttrData.updateExtended(); + console.log('hasExtendedAttrs?', this._curAttrData.hasExtendedAttrs()); + this._onStartHyperlink.fire(this._currentHyperlink); + return true; + } + + private _finishHyperlink(): boolean { + this._curAttrData.extended = this._curAttrData.extended.clone(); + this._curAttrData.extended.urlId = 0; + this._curAttrData.updateExtended(); + this._onFinishHyperlink.fire(); + this._currentHyperlink = undefined; + return true; + } + // special colors - OSC 10 | 11 | 12 private _specialColors = [ColorIndex.FOREGROUND, ColorIndex.BACKGROUND, ColorIndex.CURSOR]; diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 56815da0..129f8e1b 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -9,6 +9,7 @@ import { IDeleteEvent, IInsertEvent } from 'common/CircularList'; import { IParams } from 'common/parser/Types'; import { ICoreMouseService, ICoreService, IOptionsService, IUnicodeService } from 'common/services/Services'; import { IBufferSet } from 'common/buffer/Types'; +import { UnderlineStyle } from 'common/buffer/Constants'; export interface ICoreTerminal { coreMouseService: ICoreMouseService; @@ -114,12 +115,24 @@ export type IColorRGB = [number, number, number]; export interface IExtendedAttrs { ext: number; - underlineStyle: number; + underlineStyle: UnderlineStyle; underlineColor: number; + urlId: number; clone(): IExtendedAttrs; isEmpty(): boolean; } +/** + * Tracks the current hyperlink. Since these are treated as extended attirbutes, these get passed on + * to the linkifier when anything is printed. Doing it this way ensures that even when the cursor + * moves around unexpectedly the link is tracked, as opposed to using a start position and + * finalizing it at the end. + */ +export interface IOscLinkData { + id?: string; + uri: string; +} + /** Attribute data */ export interface IAttributeData { fg: number; diff --git a/src/common/buffer/AttributeData.ts b/src/common/buffer/AttributeData.ts index b51f7ecb..aac6a33d 100644 --- a/src/common/buffer/AttributeData.ts +++ b/src/common/buffer/AttributeData.ts @@ -35,7 +35,12 @@ export class AttributeData implements IAttributeData { // flags public isInverse(): number { return this.fg & FgFlags.INVERSE; } public isBold(): number { return this.fg & FgFlags.BOLD; } - public isUnderline(): number { return this.fg & FgFlags.UNDERLINE; } + public isUnderline(): number { + if (this.hasExtendedAttrs() && this.extended.underlineStyle !== UnderlineStyle.NONE) { + return 1; + } + return this.fg & FgFlags.UNDERLINE; + } public isBlink(): number { return this.fg & FgFlags.BLINK; } public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; } public isItalic(): number { return this.bg & BgFlags.ITALIC; } @@ -128,10 +133,24 @@ export class AttributeData implements IAttributeData { */ export class ExtendedAttrs implements IExtendedAttrs { private _ext: number = 0; - public get ext(): number { return this._ext; } + public get ext(): number { + // TODO: How to handle previous underline style if link overrides it? + if (this._urlId) { + console.log('ext, has url'); + return ( + (this._ext & ~ExtFlags.UNDERLINE_STYLE) | + (this.underlineStyle << 26) + ); + } + return this._ext; + } public set ext(value: number) { this._ext = value; } public get underlineStyle(): UnderlineStyle { + // Always return the URL style if it has one + if (this._urlId) { + return UnderlineStyle.DASHED; + } return (this._ext & ExtFlags.UNDERLINE_STYLE) >> 26; } public set underlineStyle(value: UnderlineStyle) { @@ -140,6 +159,11 @@ export class ExtendedAttrs implements IExtendedAttrs { } public get underlineColor(): number { + // Always return the URL color if it has one + if (this._urlId) { + // TODO: fix + return 0; + } return this._ext & (Attributes.CM_MASK | Attributes.RGB_MASK); } public set underlineColor(value: number) { @@ -147,16 +171,24 @@ export class ExtendedAttrs implements IExtendedAttrs { this._ext |= value & (Attributes.CM_MASK | Attributes.RGB_MASK); } + private _urlId: number = 0; + public get urlId(): number { + return this._urlId; + } + public set urlId(value: number) { + this._urlId = value; + } + constructor( - underlineStyle: UnderlineStyle = UnderlineStyle.NONE, - underlineColor: number = Attributes.CM_DEFAULT + ext: number = 0, + urlId: number = 0 ) { - this.underlineStyle = underlineStyle; - this.underlineColor = underlineColor; + this._ext = ext; + this._urlId = urlId; } public clone(): IExtendedAttrs { - return new ExtendedAttrs(this.underlineStyle, this.underlineColor); + return new ExtendedAttrs(this._ext, this._urlId); } /** @@ -164,6 +196,6 @@ export class ExtendedAttrs implements IExtendedAttrs { * that needs to be persistant in the buffer. */ public isEmpty(): boolean { - return this.underlineStyle === UnderlineStyle.NONE; + return this.underlineStyle === UnderlineStyle.NONE && this._urlId === 0; } } diff --git a/src/common/services/OscLinkService.ts b/src/common/services/OscLinkService.ts new file mode 100644 index 00000000..58961c4f --- /dev/null +++ b/src/common/services/OscLinkService.ts @@ -0,0 +1,22 @@ +import { IBufferService, IOscLinkService } from 'common/services/Services'; +import { IOscLinkData } from 'common/Types'; + +export class OscLinkService implements IOscLinkService { + public serviceBrand: any; + + constructor( + @IBufferService private readonly _bufferService: IBufferService + ) { + } + + public registerLink(linkData: IOscLinkData): number { + // TODO: Add and return properly + return 1; + } + + public getLinkData(linkId: number): IOscLinkData | undefined { + return { + uri: 'https://github.com' + }; + } +} diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 585b29ac..5f97a487 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -5,7 +5,7 @@ import { IEvent, IEventEmitter } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColorRGB, IColor, CursorStyle } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColorRGB, IColor, CursorStyle, IOscLinkData } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; import { IDecorationOptions, IDecoration } from 'xterm'; @@ -272,6 +272,18 @@ export interface ITheme { extendedAnsi?: string[]; } +export const IOscLinkService = createDecorator('OscLinkService'); +export interface IOscLinkService { + serviceBrand: undefined; + /** + * Registers a link to the service, returning the link ID. The link data is managed by this + * service and will be freed when this current cursor position is trimmed off the buffer. + */ + registerLink(linkData: IOscLinkData): number; + /** Get the link data associated with a link ID. */ + getLinkData(linkId: number): IOscLinkData | undefined; +} + export const IUnicodeService = createDecorator('UnicodeService'); export interface IUnicodeService { serviceBrand: undefined; From 7970b5c1e35726103ede881f5840453ad954a3e5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 7 Aug 2022 06:05:42 -0700 Subject: [PATCH 05/36] Fix edge cases in link creation --- .../src/atlas/WebglCharAtlas.ts | 2 +- demo/client.ts | 14 +++++++++++++ demo/index.html | 1 + src/browser/OscLinkProvider.ts | 20 +++++++++++++++---- src/common/InputHandler.ts | 3 ++- 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 182b3b3e..13dceae6 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -356,7 +356,7 @@ export class WebglCharAtlas implements IDisposable { private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number): IRasterizedGlyph { const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars; - console.log('_drawToCache', chars, ext); + this.hasCanvasChanged = true; // Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used diff --git a/demo/client.ts b/demo/client.ts index 64c694f5..8f858b39 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -189,6 +189,7 @@ if (document.location.pathname === '/test') { document.getElementById('powerline-symbol-test').addEventListener('click', powerlineSymbolTest); document.getElementById('underline-test').addEventListener('click', underlineTest); document.getElementById('ansi-colors').addEventListener('click', ansiColorsTest); + document.getElementById('osc-hyperlinks').addEventListener('click', addAnsiHyperlink); document.getElementById('add-decoration').addEventListener('click', addDecoration); document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler); } @@ -827,6 +828,19 @@ function ansiColorsTest() { } } +function addAnsiHyperlink() { + term.write('\n\n\r'); + term.writeln(`Regular link with no id:`); + term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;\x07'); + term.writeln('\x1b]8;;https://xtermjs.org\x07https://xtermjs.org\x1b]8;;\x07\x1b[C<- null cell'); + term.writeln(`\nShared ID links:`); + term.writeln('╔════╗ ╔════╗'); + term.writeln('║\x1b]8;;https://github.com\x07GitH\x1b]8;;\x07║ ║ ║'); + term.writeln('║\x1b]8;;https://github.com\x07ub\x1b]8;;\x07 ║ ║ ║'); + term.writeln('╚════╝ ╚════╝'); + term.write('\x1b[3A\x1b[8C\x1b]8;;https://xtermjs.org\x07xter\x1b[B\x1b[4Dm.js\x1b]8;;\x07\x1b[2B\x1b[12D'); +} + function addDecoration() { term.options['overviewRulerWidth'] = 15; const marker = term.registerMarker(1); diff --git a/demo/index.html b/demo/index.html index 836084b3..c38cb007 100644 --- a/demo/index.html +++ b/demo/index.html @@ -79,6 +79,7 @@
+
Decorations
diff --git a/src/browser/OscLinkProvider.ts b/src/browser/OscLinkProvider.ts index c7594479..7fee62af 100644 --- a/src/browser/OscLinkProvider.ts +++ b/src/browser/OscLinkProvider.ts @@ -28,12 +28,14 @@ export class OscLinkProvider implements ILinkProvider { let currentStart = -1; let finishLink = false; for (let x = 0; x < lineLength; x++) { - if (!line.hasContent(x)) { + // Minor optimization, only check for content if there isn't a link in case the link ends with + // a null cell + if (currentStart === -1 && !line.hasContent(x)) { continue; } line.loadCell(x, cell); - if (cell.extended.urlId) { + if (cell.hasExtendedAttrs() && cell.extended.urlId) { if (currentStart === -1) { currentStart = x; currentLinkId = cell.extended.urlId; @@ -55,8 +57,15 @@ export class OscLinkProvider implements ILinkProvider { text, // These ranges are 1-based range: { - start: { x: currentStart + 1, y }, - end: { x: x + 1, y } + start: { + x: currentStart + 1, + y + }, + end: { + // Offset end x if it's a link that ends on the last cell in the line + x: x + (!finishLink && x === lineLength - 1 ? 1 : 0), + y + } }, activate(e, text) { console.log('activate!', text); @@ -64,6 +73,9 @@ export class OscLinkProvider implements ILinkProvider { // TODO: Embedder API to handle hover }); } + currentStart = -1; + currentLinkId = -1; + finishLink = false; } } // TODO: Handle fetching and returning other link ranges to underline other links with the same id diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index b613a24a..e6d154b9 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -2937,15 +2937,16 @@ export class InputHandler extends Disposable implements IInputHandler { id = parsedParams[idParamIndex].slice(3) || undefined; } this._currentHyperlink = { id, uri }; + console.log('start hyperlink'); this._curAttrData.extended = this._curAttrData.extended.clone(); this._curAttrData.extended.urlId = 1; this._curAttrData.updateExtended(); - console.log('hasExtendedAttrs?', this._curAttrData.hasExtendedAttrs()); this._onStartHyperlink.fire(this._currentHyperlink); return true; } private _finishHyperlink(): boolean { + console.log('finish hyperlink'); this._curAttrData.extended = this._curAttrData.extended.clone(); this._curAttrData.extended.urlId = 0; this._curAttrData.updateExtended(); From fd79100d95daa54c727681a6b987df37180db7fd Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 7 Aug 2022 06:10:27 -0700 Subject: [PATCH 06/36] Fix test service injection --- src/common/CoreTerminal.ts | 2 +- src/common/InputHandler.test.ts | 18 ++++++++++++------ src/common/InputHandler.ts | 11 +++-------- src/common/TestUtils.test.ts | 14 ++++++++++++-- src/common/services/OscLinkService.ts | 1 + 5 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 4a1c99ff..6e318ce7 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -124,7 +124,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._instantiationService.setService(IOscLinkService, this._oscLinkService); // Register input handler and handle/forward events - this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this.coreService, this._dirtyRowService, this._logService, this.optionsService, this.coreMouseService, this.unicodeService); + this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this.coreService, this._dirtyRowService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService); this.register(forwardEvent(this._inputHandler.onLineFeed, this._onLineFeed)); this.register(this._inputHandler); diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index eac41052..2fa4ac2f 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -11,7 +11,7 @@ import { CellData } from 'common/buffer/CellData'; import { Attributes, UnderlineStyle } from 'common/buffer/Constants'; import { AttributeData } from 'common/buffer/AttributeData'; import { Params } from 'common/parser/Params'; -import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService } from 'common/TestUtils.test'; +import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test'; import { IBufferService, ICoreService } from 'common/services/Services'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { clone } from 'common/Clone'; @@ -67,7 +67,7 @@ describe('InputHandler', () => { bufferService.resize(80, 30); coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService); - inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService()); + inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); }); describe('SL/SR/DECIC/DECDC', () => { @@ -236,7 +236,7 @@ describe('InputHandler', () => { describe('setMode', () => { it('should toggle bracketedPasteMode', () => { const coreService = new MockCoreService(); - const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); // Set bracketed paste mode inputHandler.setModePrivate(Params.fromArray([2004])); assert.equal(coreService.decPrivateModes.bracketedPasteMode, true); @@ -261,6 +261,7 @@ describe('InputHandler', () => { new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), + new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService() ); @@ -307,6 +308,7 @@ describe('InputHandler', () => { new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), + new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService() ); @@ -357,6 +359,7 @@ describe('InputHandler', () => { new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), + new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService() ); @@ -394,6 +397,7 @@ describe('InputHandler', () => { new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), + new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService() ); @@ -444,6 +448,7 @@ describe('InputHandler', () => { new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), + new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService() ); @@ -570,6 +575,7 @@ describe('InputHandler', () => { new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), + new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService() ); @@ -593,7 +599,7 @@ describe('InputHandler', () => { beforeEach(() => { bufferService = new MockBufferService(80, 30); - handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); }); it('should handle DECSET/DECRST 47 (alt screen buffer)', async () => { await handler.parseP('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST'); @@ -790,7 +796,7 @@ describe('InputHandler', () => { describe('colon notation', () => { let inputHandler2: TestInputHandler; beforeEach(() => { - inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService()); + inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); }); describe('should equal to semicolon', () => { it('CSI 38:2::50:100:150 m', async () => { @@ -2156,7 +2162,7 @@ describe('InputHandler - async handlers', () => { 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()); + inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); }); it('async CUP with CPR check', async () => { diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index e6d154b9..76d35fd4 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -17,7 +17,7 @@ import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionId import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; -import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum } from 'common/services/Services'; +import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services'; import { OscHandler } from 'common/parser/OscParser'; import { DcsHandler } from 'common/parser/DcsParser'; import { IBuffer } from 'common/buffer/Types'; @@ -264,10 +264,6 @@ export class InputHandler extends Disposable implements IInputHandler { public get onTitleChange(): IEvent { return this._onTitleChange.event; } private _onColor = new EventEmitter(); public get onColor(): IEvent { return this._onColor.event; } - private _onStartHyperlink = new EventEmitter(); - public get onStartHyperlink(): IEvent { return this._onStartHyperlink.event; } - private _onFinishHyperlink = new EventEmitter(); - public get onFinishHyperlink(): IEvent { return this._onFinishHyperlink.event; } private _parseStack: IParseStack = { paused: false, @@ -284,6 +280,7 @@ export class InputHandler extends Disposable implements IInputHandler { private readonly _dirtyRowService: IDirtyRowService, private readonly _logService: ILogService, private readonly _optionsService: IOptionsService, + private readonly _oscLinkService: IOscLinkService, private readonly _coreMouseService: ICoreMouseService, private readonly _unicodeService: IUnicodeService, private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser() @@ -2937,11 +2934,10 @@ export class InputHandler extends Disposable implements IInputHandler { id = parsedParams[idParamIndex].slice(3) || undefined; } this._currentHyperlink = { id, uri }; - console.log('start hyperlink'); + this._oscLinkService.registerLink(this._currentHyperlink); this._curAttrData.extended = this._curAttrData.extended.clone(); this._curAttrData.extended.urlId = 1; this._curAttrData.updateExtended(); - this._onStartHyperlink.fire(this._currentHyperlink); return true; } @@ -2950,7 +2946,6 @@ export class InputHandler extends Disposable implements IInputHandler { this._curAttrData.extended = this._curAttrData.extended.clone(); this._curAttrData.extended.urlId = 0; this._curAttrData.updateExtended(); - this._onFinishHyperlink.fire(); this._currentHyperlink = undefined; return true; } diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 48f3a69e..1d353b91 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -3,13 +3,13 @@ * @license MIT */ -import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration } from 'common/services/Services'; +import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService } from 'common/services/Services'; import { IEvent, EventEmitter } from 'common/EventEmitter'; 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, IAttributeData } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData, IOscLinkData } from 'common/Types'; import { UnicodeV6 } from 'common/input/UnicodeV6'; import { IDecorationOptions, IDecoration } from 'xterm'; @@ -138,6 +138,16 @@ export class MockOptionsService implements IOptionsService { } } +export class MockOscLinkService implements IOscLinkService { + public serviceBrand: any; + public registerLink(linkData: IOscLinkData): number { + return 1; + } + public getLinkData(linkId: number): IOscLinkData | undefined { + return undefined; + } +} + // defaults to V6 always to keep tests passing export class MockUnicodeService implements IUnicodeService { public serviceBrand: any; diff --git a/src/common/services/OscLinkService.ts b/src/common/services/OscLinkService.ts index 58961c4f..b67b9846 100644 --- a/src/common/services/OscLinkService.ts +++ b/src/common/services/OscLinkService.ts @@ -10,6 +10,7 @@ export class OscLinkService implements IOscLinkService { } public registerLink(linkData: IOscLinkData): number { + console.log('register link'); // TODO: Add and return properly return 1; } From ace99d125db2ac7d1cdc782ede16fcfd8315aaf7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 7 Aug 2022 07:02:42 -0700 Subject: [PATCH 07/36] Progress on osc link service --- demo/client.ts | 19 ++++-- src/common/InputHandler.ts | 15 +++-- src/common/TestUtils.test.ts | 2 + src/common/services/OscLinkService.ts | 85 ++++++++++++++++++++++++--- src/common/services/Services.ts | 4 ++ 5 files changed, 105 insertions(+), 20 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 8f858b39..96f6b335 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -833,12 +833,19 @@ function addAnsiHyperlink() { term.writeln(`Regular link with no id:`); term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;\x07'); term.writeln('\x1b]8;;https://xtermjs.org\x07https://xtermjs.org\x1b]8;;\x07\x1b[C<- null cell'); - term.writeln(`\nShared ID links:`); - term.writeln('╔════╗ ╔════╗'); - term.writeln('║\x1b]8;;https://github.com\x07GitH\x1b]8;;\x07║ ║ ║'); - term.writeln('║\x1b]8;;https://github.com\x07ub\x1b]8;;\x07 ║ ║ ║'); - term.writeln('╚════╝ ╚════╝'); - term.write('\x1b[3A\x1b[8C\x1b]8;;https://xtermjs.org\x07xter\x1b[B\x1b[4Dm.js\x1b]8;;\x07\x1b[2B\x1b[12D'); + term.writeln(`\nAdjacent links:`); + term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;https://xtermjs.org\x07xterm.js\x1b]8;;\x07'); + term.writeln(`\nShared ID link:`); + term.writeln('╔════╗'); + term.writeln('║\x1b]8;id=testid;https://github.com\x07GitH\x1b]8;;\x07║'); + term.writeln('║\x1b]8;id=testid;https://github.com\x07ub\x1b]8;;\x07 ║'); + term.writeln('╚════╝'); + term.writeln(`\nWrapped link with no ID (not meant to share underline):`); + term.writeln('╔════╗'); + term.writeln('║ ║'); + term.writeln('║ ║'); + term.writeln('╚════╝'); + term.write('\x1b[3A\x1b[1C\x1b]8;;https://xtermjs.org\x07xter\x1b[B\x1b[4Dm.js\x1b]8;;\x07\x1b[2B\x1b[5D'); } function addDecoration() { diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 76d35fd4..37a0a127 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -228,7 +228,7 @@ export class InputHandler extends Disposable implements IInputHandler { private _workCell: CellData = new CellData(); private _windowTitle = ''; private _iconName = ''; - private _currentHyperlink?: IOscLinkData; + private _currentLinkId?: number; protected _windowTitleStack: string[] = []; protected _iconNameStack: string[] = []; @@ -639,6 +639,9 @@ export class InputHandler extends Disposable implements IInputHandler { if (screenReaderMode) { this._onA11yChar.fire(stringFromCodePoint(code)); } + if (this._currentLinkId !== undefined) { + this._oscLinkService.addLineToLink(this._currentLinkId, this._activeBuffer.ybase + this._activeBuffer.y); + } // insert combining char at last cursor position // this._activeBuffer.x should never be 0 for a combining char @@ -2924,7 +2927,7 @@ export class InputHandler extends Disposable implements IInputHandler { private _createHyperlink(params: string, uri: string): boolean { // It's legal to open a new hyperlink without explicitly finishing the previous one - if (this._currentHyperlink) { + if (this._currentLinkId !== undefined) { this._finishHyperlink(); } const parsedParams = params.split(':'); @@ -2933,10 +2936,10 @@ export class InputHandler extends Disposable implements IInputHandler { if (idParamIndex !== -1) { id = parsedParams[idParamIndex].slice(3) || undefined; } - this._currentHyperlink = { id, uri }; - this._oscLinkService.registerLink(this._currentHyperlink); this._curAttrData.extended = this._curAttrData.extended.clone(); - this._curAttrData.extended.urlId = 1; + this._currentLinkId = this._oscLinkService.registerLink({ id, uri }); + this._curAttrData.extended.urlId = this._currentLinkId; + console.log('register', uri, `id=${this._curAttrData.extended.urlId}`); this._curAttrData.updateExtended(); return true; } @@ -2946,7 +2949,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._curAttrData.extended = this._curAttrData.extended.clone(); this._curAttrData.extended.urlId = 0; this._curAttrData.updateExtended(); - this._currentHyperlink = undefined; + this._currentLinkId = undefined; return true; } diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 1d353b91..215481af 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -146,6 +146,8 @@ export class MockOscLinkService implements IOscLinkService { public getLinkData(linkId: number): IOscLinkData | undefined { return undefined; } + public addLineToLink(linkId: number, y: number): void { + } } // defaults to V6 always to keep tests passing diff --git a/src/common/services/OscLinkService.ts b/src/common/services/OscLinkService.ts index b67b9846..a3f8af1d 100644 --- a/src/common/services/OscLinkService.ts +++ b/src/common/services/OscLinkService.ts @@ -1,23 +1,92 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ import { IBufferService, IOscLinkService } from 'common/services/Services'; -import { IOscLinkData } from 'common/Types'; +import { IMarker, IOscLinkData } from 'common/Types'; export class OscLinkService implements IOscLinkService { public serviceBrand: any; + private _nextId = 1; + + // TODO: Evict on marker dispose + private _entriesNoId: IOscLinkEntryNoId[] = []; + private _entriesWithId: Map = new Map(); + + // The "link id" (number) which is the numberic representation of a unique link should not be + // confused with "id" (string) which comes in with "id=" in the OSC link's properties + private _dataByLinkId: Map = new Map(); + constructor( @IBufferService private readonly _bufferService: IBufferService ) { } - public registerLink(linkData: IOscLinkData): number { - console.log('register link'); - // TODO: Add and return properly - return 1; + public registerLink(data: IOscLinkData): number { + // TODO: Extend range where appropriate + const buffer = this._bufferService.buffer; + + // Links with no id will only ever be registered a single time + if (data.id === undefined) { + const entry: IOscLinkEntryNoId = { + data, + id: this._nextId++, + lines: [buffer.addMarker(buffer.ybase + buffer.y)] + }; + this._entriesNoId.push(entry); + this._dataByLinkId.set(entry.id, entry); + return entry.id; + } + + const castData = data as Required; + const key = this._getEntryIdKey(castData); + const match = this._entriesWithId.get(key); + if (match) { + this.addLineToLink(match.id, buffer.ybase + buffer.y); + return match.id; + } + + const entry: IOscLinkEntryWithId = { + id: this._nextId++, + key: this._getEntryIdKey(castData), + data: castData, + lines: [buffer.addMarker(buffer.ybase + buffer.y)] + }; + this._entriesWithId.set(entry.key, entry); + this._dataByLinkId.set(entry.id, entry); + return entry.id; + } + + public addLineToLink(linkId: number, y: number): void { + const link = this._dataByLinkId.get(linkId); + if (!link) { + return; + } + if (link.lines.every(e => e.line !== y)) { + console.log(' add new line', y); + link.lines.push(this._bufferService.buffer.addMarker(y)); + } } public getLinkData(linkId: number): IOscLinkData | undefined { - return { - uri: 'https://github.com' - }; + return this._dataByLinkId.get(linkId)?.data; + } + + private _getEntryIdKey(linkData: Required): string { + return `${linkData.id};;${linkData.uri}`; } } + +interface IOscLinkEntry { + data: T; + id: number; + lines: IMarker[]; +} + +interface IOscLinkEntryNoId extends IOscLinkEntry { +} + +interface IOscLinkEntryWithId extends IOscLinkEntry> { + key: string; +} diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 5f97a487..88a21252 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -280,6 +280,10 @@ export interface IOscLinkService { * service and will be freed when this current cursor position is trimmed off the buffer. */ registerLink(linkData: IOscLinkData): number; + /** + * Adds a line to a link if needed. + */ + addLineToLink(linkId: number, y: number): void; /** Get the link data associated with a link ID. */ getLinkData(linkId: number): IOscLinkData | undefined; } From d9b8c6a838ed2e03972f418065858f2494ee5310 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 7 Aug 2022 07:35:46 -0700 Subject: [PATCH 08/36] linkHandler option and default handler --- demo/client.ts | 4 +-- src/browser/OscLinkProvider.ts | 34 +++++++++++++++++++++---- src/common/services/OptionsService.ts | 1 + src/common/services/OscLinkService.ts | 4 +-- src/common/services/Services.ts | 3 ++- typings/xterm.d.ts | 36 +++++++++++++++++++++++++++ 6 files changed, 72 insertions(+), 10 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 96f6b335..cfebf384 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -835,12 +835,12 @@ function addAnsiHyperlink() { term.writeln('\x1b]8;;https://xtermjs.org\x07https://xtermjs.org\x1b]8;;\x07\x1b[C<- null cell'); term.writeln(`\nAdjacent links:`); term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;https://xtermjs.org\x07xterm.js\x1b]8;;\x07'); - term.writeln(`\nShared ID link:`); + term.writeln(`\nShared ID link (underline should be shared):`); term.writeln('╔════╗'); term.writeln('║\x1b]8;id=testid;https://github.com\x07GitH\x1b]8;;\x07║'); term.writeln('║\x1b]8;id=testid;https://github.com\x07ub\x1b]8;;\x07 ║'); term.writeln('╚════╝'); - term.writeln(`\nWrapped link with no ID (not meant to share underline):`); + term.writeln(`\nWrapped link with no ID (not necessarily meant to share underline):`); term.writeln('╔════╗'); term.writeln('║ ║'); term.writeln('║ ║'); diff --git a/src/browser/OscLinkProvider.ts b/src/browser/OscLinkProvider.ts index 7fee62af..96a2c395 100644 --- a/src/browser/OscLinkProvider.ts +++ b/src/browser/OscLinkProvider.ts @@ -5,16 +5,22 @@ import { ILink, ILinkProvider } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; -import { IBufferService, IOscLinkService } from 'common/services/Services'; +import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services'; export class OscLinkProvider implements ILinkProvider { constructor( @IBufferService private readonly _bufferService: IBufferService, + @IOptionsService private readonly _optionsService: IOptionsService, @IOscLinkService private readonly _oscLinkService: IOscLinkService ) { } public provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void { + // OSC links only work when a link handler is set + // if (this._optionsService.rawOptions.linkHandler === null) { + // return; + // } + const line = this._bufferService.buffer.lines.get(y - 1); if (!line) { callback(undefined); @@ -52,12 +58,14 @@ export class OscLinkProvider implements ILinkProvider { if (finishLink || (currentStart !== -1 && x === lineLength - 1)) { const text = this._oscLinkService.getLinkData(currentLinkId)?.uri; if (text) { + const linkHandler = this._optionsService.rawOptions.linkHandler; // OSC links always use underline and pointer decorations result.push({ text, // These ranges are 1-based range: { start: { + // TODO: Adjacent links aren't working correctly x: currentStart + 1, y }, @@ -67,10 +75,9 @@ export class OscLinkProvider implements ILinkProvider { y } }, - activate(e, text) { - console.log('activate!', text); - } - // TODO: Embedder API to handle hover + activate: linkHandler?.activate || defaultActivate, + hover: linkHandler?.hover, + leave: linkHandler?.leave }); } currentStart = -1; @@ -82,3 +89,20 @@ export class OscLinkProvider implements ILinkProvider { callback(result); } } + +function defaultActivate(e: MouseEvent, uri: string): void { + const answer = confirm(`Do you want to navigate to ${uri}?`); + if (answer) { + const newWindow = window.open(); + if (newWindow) { + try { + newWindow.opener = null; + } catch { + // no-op, Electron can throw + } + newWindow.location.href = uri; + } else { + console.warn('Opening link blocked as opener could not be cleared'); + } + } +} diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index ab9edfbf..744903f1 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -24,6 +24,7 @@ export const DEFAULT_OPTIONS: Readonly = { fontWeightBold: 'bold', lineHeight: 1.0, letterSpacing: 0, + linkHandler: null, logLevel: 'info', scrollback: 1000, scrollSensitivity: 1, diff --git a/src/common/services/OscLinkService.ts b/src/common/services/OscLinkService.ts index a3f8af1d..d3744229 100644 --- a/src/common/services/OscLinkService.ts +++ b/src/common/services/OscLinkService.ts @@ -24,7 +24,6 @@ export class OscLinkService implements IOscLinkService { } public registerLink(data: IOscLinkData): number { - // TODO: Extend range where appropriate const buffer = this._bufferService.buffer; // Links with no id will only ever be registered a single time @@ -39,6 +38,7 @@ export class OscLinkService implements IOscLinkService { return entry.id; } + // Add the line to the link if it already exists const castData = data as Required; const key = this._getEntryIdKey(castData); const match = this._entriesWithId.get(key); @@ -47,6 +47,7 @@ export class OscLinkService implements IOscLinkService { return match.id; } + // Create the link const entry: IOscLinkEntryWithId = { id: this._nextId++, key: this._getEntryIdKey(castData), @@ -64,7 +65,6 @@ export class OscLinkService implements IOscLinkService { return; } if (link.lines.every(e => e.line !== y)) { - console.log(' add new line', y); link.lines.push(this._bufferService.buffer.addMarker(y)); } } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 88a21252..63543c56 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -7,7 +7,7 @@ import { IEvent, IEventEmitter } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColorRGB, IColor, CursorStyle, IOscLinkData } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; -import { IDecorationOptions, IDecoration } from 'xterm'; +import { IDecorationOptions, IDecoration, ILinkHandler } from 'xterm'; export const IBufferService = createDecorator('BufferService'); export interface IBufferService { @@ -223,6 +223,7 @@ export interface ITerminalOptions { fontWeightBold: FontWeight; letterSpacing: number; lineHeight: number; + linkHandler: ILinkHandler | null; logLevel: LogLevel; macOptionIsMeta: boolean; macOptionClickForcesSelection: boolean; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 05058d04..2fa17421 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -128,6 +128,14 @@ declare module 'xterm' { */ lineHeight?: number; + /** + * The handler for OSC 8 hyperlinks. Links will use the `confirm` browser + * API if no link handler is set. Consider the security of users when using + * this, there should be some tooltip or prompt when hovering or activating + * the link. + */ + linkHandler?: ILinkHandler | null; + /** * What log level to use, this will log for all levels below and including * what is set: @@ -1101,6 +1109,34 @@ declare module 'xterm' { y: number; } + /** + * A link handler for OSC 8 hyperlinks. + */ + interface ILinkHandler { + /** + * Calls when the link is activated. + * @param event The mouse event triggering the callback. + * @param text The text of the link. + */ + activate(event: MouseEvent, text: string): void; + + /** + * Called when the mouse hovers the link. To use this to create a DOM-based hover tooltip, + * create the hover element within `Terminal.element` and add the `xterm-hover` class to it, + * that will cause mouse events to not fall through and activate other links. + * @param event The mouse event triggering the callback. + * @param text The text of the link. + */ + hover?(event: MouseEvent, text: string): void; + + /** + * Called when the mouse leaves the link. + * @param event The mouse event triggering the callback. + * @param text The text of the link. + */ + leave?(event: MouseEvent, text: string): void; + } + /** * A custom link provider. */ From c39d2351683008e22c5766458ec066da337ba4b8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 7 Aug 2022 07:42:21 -0700 Subject: [PATCH 09/36] Fix adjacent link edge case --- demo/client.ts | 2 +- src/browser/OscLinkProvider.ts | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index cfebf384..507663a8 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -834,7 +834,7 @@ function addAnsiHyperlink() { term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;\x07'); term.writeln('\x1b]8;;https://xtermjs.org\x07https://xtermjs.org\x1b]8;;\x07\x1b[C<- null cell'); term.writeln(`\nAdjacent links:`); - term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;https://xtermjs.org\x07xterm.js\x1b]8;;\x07'); + term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;https://xtermjs.org\x07\x1b[32mxterm.js\x1b[0m\x1b]8;;\x07'); term.writeln(`\nShared ID link (underline should be shared):`); term.writeln('╔════╗'); term.writeln('║\x1b]8;id=testid;https://github.com\x07GitH\x1b]8;;\x07║'); diff --git a/src/browser/OscLinkProvider.ts b/src/browser/OscLinkProvider.ts index 96a2c395..8d3977c6 100644 --- a/src/browser/OscLinkProvider.ts +++ b/src/browser/OscLinkProvider.ts @@ -65,7 +65,6 @@ export class OscLinkProvider implements ILinkProvider { // These ranges are 1-based range: { start: { - // TODO: Adjacent links aren't working correctly x: currentStart + 1, y }, @@ -80,12 +79,20 @@ export class OscLinkProvider implements ILinkProvider { leave: linkHandler?.leave }); } - currentStart = -1; - currentLinkId = -1; finishLink = false; + + // Clear link or start a new link if one starts immediately + if (cell.hasExtendedAttrs() && cell.extended.urlId) { + currentStart = x; + currentLinkId = cell.extended.urlId; + } else { + currentStart = -1; + currentLinkId = -1; + } } } // TODO: Handle fetching and returning other link ranges to underline other links with the same id + console.log('result', result); callback(result); } } From 9c9ff2b38c6e8f3f0b4693f11527a08339414789 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 7 Aug 2022 07:46:16 -0700 Subject: [PATCH 10/36] Clean up --- src/browser/OscLinkProvider.ts | 9 ++------- src/common/InputHandler.ts | 3 --- src/common/buffer/AttributeData.ts | 7 ------- 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/browser/OscLinkProvider.ts b/src/browser/OscLinkProvider.ts index 8d3977c6..38c07106 100644 --- a/src/browser/OscLinkProvider.ts +++ b/src/browser/OscLinkProvider.ts @@ -16,11 +16,6 @@ export class OscLinkProvider implements ILinkProvider { } public provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void { - // OSC links only work when a link handler is set - // if (this._optionsService.rawOptions.linkHandler === null) { - // return; - // } - const line = this._bufferService.buffer.lines.get(y - 1); if (!line) { callback(undefined); @@ -28,6 +23,7 @@ export class OscLinkProvider implements ILinkProvider { } const result: ILink[] = []; + const linkHandler = this._optionsService.rawOptions.linkHandler; const cell = new CellData(); const lineLength = line.getTrimmedLength(); let currentLinkId = -1; @@ -58,7 +54,6 @@ export class OscLinkProvider implements ILinkProvider { if (finishLink || (currentStart !== -1 && x === lineLength - 1)) { const text = this._oscLinkService.getLinkData(currentLinkId)?.uri; if (text) { - const linkHandler = this._optionsService.rawOptions.linkHandler; // OSC links always use underline and pointer decorations result.push({ text, @@ -91,8 +86,8 @@ export class OscLinkProvider implements ILinkProvider { } } } + // TODO: Handle fetching and returning other link ranges to underline other links with the same id - console.log('result', result); callback(result); } } diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 37a0a127..34c99f84 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -2912,7 +2912,6 @@ export class InputHandler extends Disposable implements IInputHandler { */ public setHyperlink(data: string): boolean { const args = data.split(';'); - console.log('hyperlink', args); if (args.length < 2) { return false; } @@ -2939,13 +2938,11 @@ export class InputHandler extends Disposable implements IInputHandler { this._curAttrData.extended = this._curAttrData.extended.clone(); this._currentLinkId = this._oscLinkService.registerLink({ id, uri }); this._curAttrData.extended.urlId = this._currentLinkId; - console.log('register', uri, `id=${this._curAttrData.extended.urlId}`); this._curAttrData.updateExtended(); return true; } private _finishHyperlink(): boolean { - console.log('finish hyperlink'); this._curAttrData.extended = this._curAttrData.extended.clone(); this._curAttrData.extended.urlId = 0; this._curAttrData.updateExtended(); diff --git a/src/common/buffer/AttributeData.ts b/src/common/buffer/AttributeData.ts index aac6a33d..3af3d293 100644 --- a/src/common/buffer/AttributeData.ts +++ b/src/common/buffer/AttributeData.ts @@ -134,9 +134,7 @@ export class AttributeData implements IAttributeData { export class ExtendedAttrs implements IExtendedAttrs { private _ext: number = 0; public get ext(): number { - // TODO: How to handle previous underline style if link overrides it? if (this._urlId) { - console.log('ext, has url'); return ( (this._ext & ~ExtFlags.UNDERLINE_STYLE) | (this.underlineStyle << 26) @@ -159,11 +157,6 @@ export class ExtendedAttrs implements IExtendedAttrs { } public get underlineColor(): number { - // Always return the URL color if it has one - if (this._urlId) { - // TODO: fix - return 0; - } return this._ext & (Attributes.CM_MASK | Attributes.RGB_MASK); } public set underlineColor(value: number) { From 8191483fbd9b5a68c47cad2b6d66738adc5fa66b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 7 Aug 2022 08:05:42 -0700 Subject: [PATCH 11/36] Evict links from maps when markers are disposed --- src/common/services/OscLinkService.ts | 45 ++++++++++++++++++++------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/common/services/OscLinkService.ts b/src/common/services/OscLinkService.ts index d3744229..13bd8aa4 100644 --- a/src/common/services/OscLinkService.ts +++ b/src/common/services/OscLinkService.ts @@ -10,12 +10,16 @@ export class OscLinkService implements IOscLinkService { private _nextId = 1; - // TODO: Evict on marker dispose - private _entriesNoId: IOscLinkEntryNoId[] = []; + /** + * A map of the link key to link entry. This is used to add additional lines to links with ids. + */ private _entriesWithId: Map = new Map(); - // The "link id" (number) which is the numberic representation of a unique link should not be - // confused with "id" (string) which comes in with "id=" in the OSC link's properties + /** + * A map of the link id to the link entry. The "link id" (number) which is the numberic + * representation of a unique link should not be confused with "id" (string) which comes in with + * `id=` in the OSC link's properties. + */ private _dataByLinkId: Map = new Map(); constructor( @@ -28,12 +32,13 @@ export class OscLinkService implements IOscLinkService { // Links with no id will only ever be registered a single time if (data.id === undefined) { + const marker = buffer.addMarker(buffer.ybase + buffer.y); const entry: IOscLinkEntryNoId = { data, id: this._nextId++, - lines: [buffer.addMarker(buffer.ybase + buffer.y)] + lines: [marker] }; - this._entriesNoId.push(entry); + marker.onDispose(() => this._removeMarkerFromLink(entry, marker)); this._dataByLinkId.set(entry.id, entry); return entry.id; } @@ -48,24 +53,28 @@ export class OscLinkService implements IOscLinkService { } // Create the link + const marker = buffer.addMarker(buffer.ybase + buffer.y); const entry: IOscLinkEntryWithId = { id: this._nextId++, key: this._getEntryIdKey(castData), data: castData, - lines: [buffer.addMarker(buffer.ybase + buffer.y)] + lines: [marker] }; + marker.onDispose(() => this._removeMarkerFromLink(entry, marker)); this._entriesWithId.set(entry.key, entry); this._dataByLinkId.set(entry.id, entry); return entry.id; } public addLineToLink(linkId: number, y: number): void { - const link = this._dataByLinkId.get(linkId); - if (!link) { + const entry = this._dataByLinkId.get(linkId); + if (!entry) { return; } - if (link.lines.every(e => e.line !== y)) { - link.lines.push(this._bufferService.buffer.addMarker(y)); + if (entry.lines.every(e => e.line !== y)) { + const marker = this._bufferService.buffer.addMarker(y); + entry.lines.push(marker); + marker.onDispose(() => this._removeMarkerFromLink(entry, marker)); } } @@ -76,6 +85,20 @@ export class OscLinkService implements IOscLinkService { private _getEntryIdKey(linkData: Required): string { return `${linkData.id};;${linkData.uri}`; } + + private _removeMarkerFromLink(entry: IOscLinkEntryNoId | IOscLinkEntryWithId, marker: IMarker): void { + const index = entry.lines.indexOf(marker); + if (index === -1) { + return; + } + entry.lines.splice(index, 1); + if (entry.lines.length === 0) { + if (entry.data.id !== undefined) { + this._entriesWithId.delete((entry as IOscLinkEntryWithId).key); + } + this._dataByLinkId.delete(entry.id); + } + } } interface IOscLinkEntry { From e87e15e87bc6a7f8dda32b3047e9ce7c0b996b60 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 7 Aug 2022 10:36:42 -0700 Subject: [PATCH 12/36] Only override AttributeData.isUnderline when urlId is set --- src/common/buffer/AttributeData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/buffer/AttributeData.ts b/src/common/buffer/AttributeData.ts index 3af3d293..e4f464cd 100644 --- a/src/common/buffer/AttributeData.ts +++ b/src/common/buffer/AttributeData.ts @@ -36,7 +36,7 @@ export class AttributeData implements IAttributeData { public isInverse(): number { return this.fg & FgFlags.INVERSE; } public isBold(): number { return this.fg & FgFlags.BOLD; } public isUnderline(): number { - if (this.hasExtendedAttrs() && this.extended.underlineStyle !== UnderlineStyle.NONE) { + if (this.hasExtendedAttrs() && this.extended.urlId) { return 1; } return this.fg & FgFlags.UNDERLINE; From 2cfbee413ff2289506f4a6135865273d0700b539 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 7 Aug 2022 11:30:08 -0700 Subject: [PATCH 13/36] Add some OscLinkService unit tests --- src/common/services/BufferService.ts | 10 ++--- src/common/services/OscLinkService.test.ts | 44 ++++++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 src/common/services/OscLinkService.test.ts diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index bba60dd8..e3b7dcd8 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -32,13 +32,11 @@ export class BufferService extends Disposable implements IBufferService { /** An IBufferline to clone/copy from for new blank lines */ private _cachedBlankLine: IBufferLine | undefined; - constructor( - @IOptionsService private _optionsService: IOptionsService - ) { + constructor(@IOptionsService optionsService: IOptionsService) { super(); - this.cols = Math.max(_optionsService.rawOptions.cols || 0, MINIMUM_COLS); - this.rows = Math.max(_optionsService.rawOptions.rows || 0, MINIMUM_ROWS); - this.buffers = new BufferSet(_optionsService, this); + this.cols = Math.max(optionsService.rawOptions.cols || 0, MINIMUM_COLS); + this.rows = Math.max(optionsService.rawOptions.rows || 0, MINIMUM_ROWS); + this.buffers = new BufferSet(optionsService, this); } public dispose(): void { diff --git a/src/common/services/OscLinkService.test.ts b/src/common/services/OscLinkService.test.ts new file mode 100644 index 00000000..5000e8e2 --- /dev/null +++ b/src/common/services/OscLinkService.test.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2020 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { AttributeData } from 'common/buffer/AttributeData'; +import { BufferService } from 'common/services/BufferService'; +import { OptionsService } from 'common/services/OptionsService'; +import { OscLinkService } from 'common/services/OscLinkService'; +import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services'; + +describe('OscLinkService', () => { + describe('constructor', () => { + let bufferService: IBufferService; + let optionsService: IOptionsService; + let oscLinkService: IOscLinkService; + beforeEach(() => { + optionsService = new OptionsService({ rows: 3, cols: 10 }); + bufferService = new BufferService(optionsService); + oscLinkService = new OscLinkService(bufferService); + }); + + it('link IDs are created and fetched consistently', () => { + const linkId = oscLinkService.registerLink({ id: 'foo', uri: 'bar' }); + assert.ok(linkId); + assert.equal(oscLinkService.registerLink({ id: 'foo', uri: 'bar' }), linkId); + }); + + it('should dispose the link ID when the last marker is trimmed from the buffer', () => { + // Activate the alt buffer to get 0 scrollback + bufferService.buffers.activateAltBuffer(); + const linkId = oscLinkService.registerLink({ id: 'foo', uri: 'bar' }); + assert.ok(linkId); + bufferService.scroll(new AttributeData()); + assert.notStrictEqual(oscLinkService.registerLink({ id: 'foo', uri: 'bar' }), linkId); + }); + + it('should fetch link data from link id', () => { + const linkId = oscLinkService.registerLink({ id: 'foo', uri: 'bar' }); + assert.deepStrictEqual(oscLinkService.getLinkData(linkId), { id: 'foo', uri: 'bar' }); + }); + }); +}); From bc528cd870e52c75787137e06568343de1029155 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 7 Aug 2022 11:51:52 -0700 Subject: [PATCH 14/36] Ensure extended underline style is cleared on SGR 24 --- src/common/InputHandler.ts | 1 + src/common/buffer/AttributeData.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 34c99f84..5a0725a4 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -2500,6 +2500,7 @@ export class InputHandler extends Disposable implements IInputHandler { } else if (p === 24) { // not underlined attr.fg &= ~FgFlags.UNDERLINE; + this._processUnderline(UnderlineStyle.NONE, attr); } else if (p === 25) { // not blink attr.fg &= ~FgFlags.BLINK; diff --git a/src/common/buffer/AttributeData.ts b/src/common/buffer/AttributeData.ts index e4f464cd..3af3d293 100644 --- a/src/common/buffer/AttributeData.ts +++ b/src/common/buffer/AttributeData.ts @@ -36,7 +36,7 @@ export class AttributeData implements IAttributeData { public isInverse(): number { return this.fg & FgFlags.INVERSE; } public isBold(): number { return this.fg & FgFlags.BOLD; } public isUnderline(): number { - if (this.hasExtendedAttrs() && this.extended.urlId) { + if (this.hasExtendedAttrs() && this.extended.underlineStyle !== UnderlineStyle.NONE) { return 1; } return this.fg & FgFlags.UNDERLINE; From 5c6d16ef0a805469f2e2628b2a428dc9285eae38 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 28 Aug 2022 13:33:09 -0700 Subject: [PATCH 15/36] Only remove padding from restricted powerline chars --- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index c17c54a5..d4e58c9f 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -404,7 +404,7 @@ export class WebglCharAtlas implements IDisposable { this._tmpCtx.fillStyle = foregroundColor.css; // For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129) - const padding = powerlineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING * 2; + const padding = restrictedPowerlineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING * 2; // Draw custom characters if applicable let customGlyph = false; From 22cca3495ae7658460a2df22c8b11d52aff152a7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 30 Aug 2022 07:00:33 -0700 Subject: [PATCH 16/36] Expose range on link handler This is needed for proper tooltip positioning Part of #1134 --- src/browser/OscLinkProvider.ts | 32 +++++++++++++++++--------------- typings/xterm.d.ts | 9 ++++++--- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/browser/OscLinkProvider.ts b/src/browser/OscLinkProvider.ts index 38c07106..fdae9b0e 100644 --- a/src/browser/OscLinkProvider.ts +++ b/src/browser/OscLinkProvider.ts @@ -6,6 +6,7 @@ import { ILink, ILinkProvider } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services'; +import { IBufferRange } from 'xterm'; export class OscLinkProvider implements ILinkProvider { constructor( @@ -54,24 +55,25 @@ export class OscLinkProvider implements ILinkProvider { if (finishLink || (currentStart !== -1 && x === lineLength - 1)) { const text = this._oscLinkService.getLinkData(currentLinkId)?.uri; if (text) { + // These ranges are 1-based + const range: IBufferRange = { + start: { + x: currentStart + 1, + y + }, + end: { + // Offset end x if it's a link that ends on the last cell in the line + x: x + (!finishLink && x === lineLength - 1 ? 1 : 0), + y + } + }; // OSC links always use underline and pointer decorations result.push({ text, - // These ranges are 1-based - range: { - start: { - x: currentStart + 1, - y - }, - end: { - // Offset end x if it's a link that ends on the last cell in the line - x: x + (!finishLink && x === lineLength - 1 ? 1 : 0), - y - } - }, - activate: linkHandler?.activate || defaultActivate, - hover: linkHandler?.hover, - leave: linkHandler?.leave + range, + activate: (e, text) => (linkHandler?.activate(e, text, range) || defaultActivate(e, text)), + hover: (e, text) => linkHandler?.hover?.(e, text, range), + leave: (e, text) => linkHandler?.leave?.(e, text, range) }); } finishLink = false; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 2fa17421..bd48afd4 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -1117,8 +1117,9 @@ declare module 'xterm' { * Calls when the link is activated. * @param event The mouse event triggering the callback. * @param text The text of the link. + * @param range The buffer range of the link. */ - activate(event: MouseEvent, text: string): void; + activate(event: MouseEvent, text: string, range: IBufferRange): void; /** * Called when the mouse hovers the link. To use this to create a DOM-based hover tooltip, @@ -1126,15 +1127,17 @@ declare module 'xterm' { * that will cause mouse events to not fall through and activate other links. * @param event The mouse event triggering the callback. * @param text The text of the link. + * @param range The buffer range of the link. */ - hover?(event: MouseEvent, text: string): void; + hover?(event: MouseEvent, text: string, range: IBufferRange): void; /** * Called when the mouse leaves the link. * @param event The mouse event triggering the callback. * @param text The text of the link. + * @param range The buffer range of the link. */ - leave?(event: MouseEvent, text: string): void; + leave?(event: MouseEvent, text: string, range: IBufferRange): void; } /** From c5861d512c41b179fe944c1c032ad13d3a070dc0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 30 Aug 2022 07:01:23 -0700 Subject: [PATCH 17/36] Import from Types --- src/browser/OscLinkProvider.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/browser/OscLinkProvider.ts b/src/browser/OscLinkProvider.ts index fdae9b0e..9af0cf6f 100644 --- a/src/browser/OscLinkProvider.ts +++ b/src/browser/OscLinkProvider.ts @@ -3,10 +3,9 @@ * @license MIT */ -import { ILink, ILinkProvider } from 'browser/Types'; +import { IBufferRange, ILink, ILinkProvider } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services'; -import { IBufferRange } from 'xterm'; export class OscLinkProvider implements ILinkProvider { constructor( From 284d9f98933c615e24f528bee027e98df5f1a898 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 30 Aug 2022 07:51:32 -0700 Subject: [PATCH 18/36] Fix ternary in activate call Part of #1134 --- src/browser/OscLinkProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/OscLinkProvider.ts b/src/browser/OscLinkProvider.ts index 9af0cf6f..3ca93b09 100644 --- a/src/browser/OscLinkProvider.ts +++ b/src/browser/OscLinkProvider.ts @@ -70,7 +70,7 @@ export class OscLinkProvider implements ILinkProvider { result.push({ text, range, - activate: (e, text) => (linkHandler?.activate(e, text, range) || defaultActivate(e, text)), + activate: (e, text) => (linkHandler ? linkHandler.activate(e, text, range) : defaultActivate(e, text)), hover: (e, text) => linkHandler?.hover?.(e, text, range), leave: (e, text) => linkHandler?.leave?.(e, text, range) }); From 4a6e7bee211de163438adab78a18668d721475ab Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 30 Aug 2022 14:09:39 -0700 Subject: [PATCH 19/36] Fix lgtm recommendations --- .../src/SelectionRenderLayer.ts | 3 +- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 2 +- addons/xterm-addon-webgl/src/Types.d.ts | 4 --- .../src/atlas/WebglCharAtlas.ts | 3 +- .../src/renderLayer/CursorRenderLayer.ts | 2 +- bin/extract_vtfeatures.js | 2 +- demo/index.html | 4 +-- src/browser/Terminal.ts | 30 +++++++++---------- src/browser/TestUtils.test.ts | 2 +- .../decorations/OverviewRulerRenderer.ts | 2 +- src/browser/public/Terminal.ts | 2 +- src/browser/renderer/dom/DomRenderer.ts | 2 +- src/browser/services/RenderService.ts | 2 +- src/browser/services/Services.ts | 4 +-- src/common/CoreTerminal.ts | 2 +- src/common/services/OptionsService.ts | 1 - src/common/services/Services.ts | 2 +- 17 files changed, 29 insertions(+), 40 deletions(-) diff --git a/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts b/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts index 61fc4783..82aa056b 100644 --- a/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts @@ -3,12 +3,11 @@ * @license MIT */ -import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; +import { IRenderDimensions } from 'browser/renderer/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; import { IColorSet } from 'browser/Types'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; import { ICoreBrowserService } from 'browser/services/Services'; -import { IEventEmitter } from 'common/EventEmitter'; interface ISelectionState { start?: [number, number]; diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 5d3fb9ac..85ef25f6 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -8,7 +8,7 @@ import { WebglCharAtlas } from './atlas/WebglCharAtlas'; import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types'; import { fill } from 'common/TypedArrayUtils'; import { NULL_CELL_CODE } from 'common/buffer/Constants'; -import { Terminal, IBufferLine } from 'xterm'; +import { Terminal } from 'xterm'; import { IColorSet } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; import { Disposable, toDisposable } from 'common/Lifecycle'; diff --git a/addons/xterm-addon-webgl/src/Types.d.ts b/addons/xterm-addon-webgl/src/Types.d.ts index c803d3e4..bcfa11c8 100644 --- a/addons/xterm-addon-webgl/src/Types.d.ts +++ b/addons/xterm-addon-webgl/src/Types.d.ts @@ -3,10 +3,6 @@ * @license MIT */ -export interface IRasterizedGlyphSet { - [bg: number]: { [fg: number]: { [ext: number]: IRasterizedGlyph } } | undefined; -} - /** * Represents a rasterized glyph within a texture atlas. Some numbers are * tracked in CSS pixels as well in order to reduce calculations during the diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index d4e58c9f..e7dbfaa2 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -5,7 +5,7 @@ import { ICharAtlasConfig } from './Types'; import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/Constants'; -import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types'; +import { IRasterizedGlyph, IBoundingBox } from '../Types'; import { DEFAULT_COLOR, Attributes, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants'; import { throwIfFalsy } from '../WebglUtils'; import { IColor } from 'common/Types'; @@ -450,7 +450,6 @@ export class WebglCharAtlas implements IDisposable { // Underline style/stroke this._tmpCtx.beginPath(); const xLeft = padding; - const xRight = padding + this._config.scaledCellWidth * chWidth; const yTop = Math.ceil(padding + this._config.scaledCharHeight) - yOffset; const yMid = padding + this._config.scaledCharHeight + lineWidth - yOffset; const yBot = Math.ceil(padding + this._config.scaledCharHeight + lineWidth * 2) - yOffset; diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts index 04e2b387..8a16a387 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts @@ -7,7 +7,7 @@ import { Terminal } from 'xterm'; import { BaseRenderLayer } from './BaseRenderLayer'; import { ICellData } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; -import { IColorSet, ITerminal } from 'browser/Types'; +import { IColorSet } from 'browser/Types'; import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; import { IEventEmitter } from 'common/EventEmitter'; import { ICoreBrowserService } from 'browser/services/Services'; diff --git a/bin/extract_vtfeatures.js b/bin/extract_vtfeatures.js index 9b5ab898..1ccfb636 100644 --- a/bin/extract_vtfeatures.js +++ b/bin/extract_vtfeatures.js @@ -362,7 +362,7 @@ function* parseMultiLineGen(filename, s) { if (!s.includes('@vt:')) { return; } - const lines = s.split('\n').map(el => el.trim().replace(/\*/, '').replace(/\s/, '')); + const lines = s.split('\n').map(el => el.trim().replace(/\*/, '').replace(/\s/, '')); // lgtm [js/incomplete-sanitization] let grabLine = false; let longDescription = []; let feature = undefined; diff --git a/demo/index.html b/demo/index.html index c38cb007..d6c20851 100644 --- a/demo/index.html +++ b/demo/index.html @@ -10,8 +10,8 @@ - - + +

xterm.js: A terminal for the web

diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index aacd6cf7..e45d2355 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -213,22 +213,20 @@ export class Terminal extends CoreTerminal implements ITerminal { acc = 'ansi'; ident = '4;' + req.index; } - if (acc) { - switch (req.type) { - case ColorRequestType.REPORT: - const channels = color.toColorRGB(acc === 'ansi' - ? this._colorManager.colors.ansi[req.index] - : this._colorManager.colors[acc]); - this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C1_ESCAPED.ST}`); - break; - case ColorRequestType.SET: - if (acc === 'ansi') this._colorManager.colors.ansi[req.index] = rgba.toColor(...req.color); - else this._colorManager.colors[acc] = rgba.toColor(...req.color); - break; - case ColorRequestType.RESTORE: - this._colorManager.restoreColor(req.index); - break; - } + switch (req.type) { + case ColorRequestType.REPORT: + const channels = color.toColorRGB(acc === 'ansi' + ? this._colorManager.colors.ansi[req.index] + : this._colorManager.colors[acc]); + this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C1_ESCAPED.ST}`); + break; + case ColorRequestType.SET: + if (acc === 'ansi') this._colorManager.colors.ansi[req.index] = rgba.toColor(...req.color); + else this._colorManager.colors[acc] = rgba.toColor(...req.color); + break; + case ColorRequestType.RESTORE: + this._colorManager.restoreColor(req.index); + break; } } this._renderService?.setColors(this._colorManager.colors); diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index e09097a2..a3b5fdb0 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -7,7 +7,7 @@ import { IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration } import { IEvent, EventEmitter } from 'common/EventEmitter'; import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types'; -import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IRenderDebouncer, IBufferRange } from 'browser/Types'; +import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IBufferRange } from 'browser/Types'; import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types'; import { Buffer } from 'common/buffer/Buffer'; diff --git a/src/browser/decorations/OverviewRulerRenderer.ts b/src/browser/decorations/OverviewRulerRenderer.ts index 22f3ddf5..7c284a61 100644 --- a/src/browser/decorations/OverviewRulerRenderer.ts +++ b/src/browser/decorations/OverviewRulerRenderer.ts @@ -7,7 +7,7 @@ import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations import { addDisposableDomListener } from 'browser/Lifecycle'; import { IRenderService } from 'browser/services/Services'; import { Disposable } from 'common/Lifecycle'; -import { IBufferService, IDecorationService, IInternalDecoration, IOptionsService } from 'common/services/Services'; +import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; // Helper objects to avoid excessive calculation and garbage collection during rendering. These are // static values for each render and can be accessed using the decoration position as the key. diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 57efdbd0..2e39cd63 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal as ITerminalApi, IMarker, IDisposable, ILocalizableStrings, ITerminalAddon, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, FontWeight, IModes, IDecorationOptions, IDecoration } from 'xterm'; +import { Terminal as ITerminalApi, IMarker, IDisposable, ILocalizableStrings, ITerminalAddon, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, IModes, IDecorationOptions, IDecoration } from 'xterm'; import { IBufferRange, ITerminal } from 'browser/Types'; import { Terminal as TerminalCore } from 'browser/Terminal'; import * as Strings from 'browser/LocalizableStrings'; diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index 050f9396..ec9b835c 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -9,7 +9,7 @@ import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; import { Disposable } from 'common/Lifecycle'; import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types'; import { ICharSizeService } from 'browser/services/Services'; -import { IOptionsService, IBufferService, IInstantiationService, IDecorationService } from 'common/services/Services'; +import { IOptionsService, IBufferService, IInstantiationService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { color } from 'common/Color'; import { removeElementFromParent } from 'browser/Dom'; diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 78bd7f56..13cdaf9c 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -9,7 +9,7 @@ import { EventEmitter, IEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; import { addDisposableDomListener } from 'browser/Lifecycle'; -import { IColorSet, IRenderDebouncer, IRenderDebouncerWithCallback } from 'browser/Types'; +import { IColorSet, IRenderDebouncerWithCallback } from 'browser/Types'; import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; import { ICharSizeService, IRenderService } from 'browser/services/Services'; diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 9f226338..165d0c7e 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -5,12 +5,10 @@ import { IEvent } from 'common/EventEmitter'; import { IRenderDimensions, IRenderer } from 'browser/renderer/Types'; -import { IColorSet, IRenderDebouncer } from 'browser/Types'; +import { IColorSet } from 'browser/Types'; import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; import { IDisposable } from 'common/Types'; -import { IDecorationOptions, IDecoration } from 'xterm'; -import { IBufferService } from 'common/services/Services'; export const ICharSizeService = createDecorator('CharSizeService'); export interface ICharSizeService { diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 6e318ce7..7960858d 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -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 { IDisposable, IBufferLine, IAttributeData, ICoreTerminal, IKeyboardEvent, IScrollEvent, ScrollSource, ITerminalOptions as IPublicTerminalOptions } from 'common/Types'; +import { IDisposable, IAttributeData, ICoreTerminal, IScrollEvent, ScrollSource } from 'common/Types'; import { CoreService } from 'common/services/CoreService'; import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter'; import { CoreMouseService } from 'common/services/CoreMouseService'; diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 744903f1..46e5919c 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -120,7 +120,6 @@ export class OptionsService implements IOptionsService { throw new Error(`"${value}" is not a valid value for ${key}`); } break; - case 'cursorStyle': case 'wordSeparator': if (!value) { value = DEFAULT_OPTIONS[key]; diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 82ad7355..9312cd46 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -5,7 +5,7 @@ import { IEvent, IEventEmitter } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColorRGB, IColor, CursorStyle, IOscLinkData } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColor, CursorStyle, IOscLinkData } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; import { IDecorationOptions, IDecoration, ILinkHandler } from 'xterm'; From 9459553665e94713bb8dd3415b8638c0f61eabdb Mon Sep 17 00:00:00 2001 From: Kenneth Russell Date: Fri, 26 Aug 2022 16:50:42 -0700 Subject: [PATCH 20/36] Handle WebGL context loss/restore events in WebGL renderer. If the context is restored within a brief period of time (3 seconds), recreate members dependent on WebGL state, and fire onRequestRedraw to the renderer's listeners. Handle changes to the char atlas better in the demo client. Tested in Chrome Canary on macOS by running the demo in one window, and visiting the internal URL about:gpucrash in another window. The terminal now recovers and redraws properly. --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 52 ++++++++++++++++--- demo/client.ts | 2 +- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 43de1c98..23c763a2 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -46,8 +46,8 @@ export class WebglRenderer extends Disposable implements IRenderer { private _canvas: HTMLCanvasElement; private _gl: IWebGL2RenderingContext; - private _rectangleRenderer: RectangleRenderer; - private _glyphRenderer: GlyphRenderer; + private _rectangleRenderer!: RectangleRenderer; + private _glyphRenderer!: GlyphRenderer; public dimensions: IRenderDimensions; @@ -62,6 +62,8 @@ export class WebglRenderer extends Disposable implements IRenderer { private _onContextLoss = new EventEmitter(); public get onContextLoss(): IEvent { return this._onContextLoss.event; } + private _contextRestorationTimeout: number | undefined; + constructor( private _terminal: Terminal, private _colors: IColorSet, @@ -108,16 +110,35 @@ export class WebglRenderer extends Disposable implements IRenderer { throw new Error('WebGL2 not supported ' + this._gl); } - this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => { this._onContextLoss.fire(e); })); + this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => { + console.log('webglcontextlost event received'); + // Prevent the default behavior in order to enable WebGL context restoration. + e.preventDefault(); + // Wait a few seconds to see if the 'webglcontextrestored' event is fired. + // If not, dispatch the onContextLoss notification to observers. + this._contextRestorationTimeout = setTimeout(() => { + if (this._contextRestorationTimeout !== 0) { + console.log('webgl context not restored; firing onContextLoss'); + this._onContextLoss.fire(e); + } + }, 3000 /* ms */); + })); + this.register(addDisposableDomListener(this._canvas, 'webglcontextrestored', (e) => { + console.log('webglcontextrestored event received'); + clearTimeout(this._contextRestorationTimeout); + this._contextRestorationTimeout = 0; + // The texture atlas and glyph renderer must be fully reinitialized + // because their contents have been lost. + removeTerminalFromCache(this._terminal); + this._initializeWebGLState(); + this._requestRedrawViewport(); + })); + this.register(observeDevicePixelDimensions(this._canvas, (w, h) => this._setCanvasDevicePixelDimensions(w, h))); this._core.screenElement!.appendChild(this._canvas); - this._rectangleRenderer = this.register(new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions)); - this._glyphRenderer = this.register(new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions)); - - // Update dimensions and acquire char atlas - this.onCharSizeChanged(); + this._initializeWebGLState(); this._isAttached = document.body.contains(this._core.screenElement!); } @@ -235,6 +256,21 @@ export class WebglRenderer extends Disposable implements IRenderer { this._refreshCharAtlas(); } + /** + * Initializes members dependent on WebGL context state. + */ + private _initializeWebGLState(): void { + // Dispose any previous rectangle and glyph renderers before creating new ones. + this._rectangleRenderer?.dispose(); + this._glyphRenderer?.dispose(); + + this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions); + this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions); + + // Update dimensions and acquire char atlas + this.onCharSizeChanged(); + } + /** * Refreshes the char atlas, aquiring a new one if necessary. * @param terminal The terminal. diff --git a/demo/client.ts b/demo/client.ts index 3593004a..2557fb44 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -606,7 +606,7 @@ function htmlSerializeButtonHandler(): void { } function addTextureAtlas(e: HTMLCanvasElement) { - document.querySelector('#texture-atlas').appendChild(e); + document.querySelector('#texture-atlas').replaceChildren(e); } function writeCustomGlyphHandler() { From f309affff2d7482188a67cd3c56cabdcb69fe543 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 31 Aug 2022 05:44:48 -0700 Subject: [PATCH 21/36] Tweak handling of _contextRestorationTimeout --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 23c763a2..6dcd3061 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -53,6 +53,7 @@ export class WebglRenderer extends Disposable implements IRenderer { private _core: ITerminal; private _isAttached: boolean; + private _contextRestorationTimeout: number | undefined; private _onChangeTextureAtlas = new EventEmitter(); public get onChangeTextureAtlas(): IEvent { return this._onChangeTextureAtlas.event; } @@ -62,8 +63,6 @@ export class WebglRenderer extends Disposable implements IRenderer { private _onContextLoss = new EventEmitter(); public get onContextLoss(): IEvent { return this._onContextLoss.event; } - private _contextRestorationTimeout: number | undefined; - constructor( private _terminal: Terminal, private _colors: IColorSet, @@ -117,16 +116,15 @@ export class WebglRenderer extends Disposable implements IRenderer { // Wait a few seconds to see if the 'webglcontextrestored' event is fired. // If not, dispatch the onContextLoss notification to observers. this._contextRestorationTimeout = setTimeout(() => { - if (this._contextRestorationTimeout !== 0) { - console.log('webgl context not restored; firing onContextLoss'); - this._onContextLoss.fire(e); - } + this._contextRestorationTimeout = undefined; + console.warn('webgl context not restored; firing onContextLoss'); + this._onContextLoss.fire(e); }, 3000 /* ms */); })); this.register(addDisposableDomListener(this._canvas, 'webglcontextrestored', (e) => { - console.log('webglcontextrestored event received'); + console.warn('webglcontextrestored event received'); clearTimeout(this._contextRestorationTimeout); - this._contextRestorationTimeout = 0; + this._contextRestorationTimeout = undefined; // The texture atlas and glyph renderer must be fully reinitialized // because their contents have been lost. removeTerminalFromCache(this._terminal); From 6d2d4555e7cbb64dbd61ee64a38f2eb5e4f443c9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 31 Aug 2022 06:28:58 -0700 Subject: [PATCH 22/36] Create codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 72 +++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..9afca6e8 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,72 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "master" ] + schedule: + - cron: '41 17 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript', 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 From c5a6b997d155988ecebc8acad11e919beb7fefb9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 31 Aug 2022 07:25:03 -0700 Subject: [PATCH 23/36] Remove lgtm comments in favor of codeql dismissable alerts --- bin/extract_vtfeatures.js | 2 +- demo/server.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bin/extract_vtfeatures.js b/bin/extract_vtfeatures.js index 1ccfb636..9b5ab898 100644 --- a/bin/extract_vtfeatures.js +++ b/bin/extract_vtfeatures.js @@ -362,7 +362,7 @@ function* parseMultiLineGen(filename, s) { if (!s.includes('@vt:')) { return; } - const lines = s.split('\n').map(el => el.trim().replace(/\*/, '').replace(/\s/, '')); // lgtm [js/incomplete-sanitization] + const lines = s.split('\n').map(el => el.trim().replace(/\*/, '').replace(/\s/, '')); let grabLine = false; let longDescription = []; let feature = undefined; diff --git a/demo/server.js b/demo/server.js index 71a9d36a..8d295942 100644 --- a/demo/server.js +++ b/demo/server.js @@ -20,19 +20,19 @@ function startServer() { logs = {}; app.use('/xterm.css', express.static(__dirname + '/../css/xterm.css')); - app.get('/logo.png', (req, res) => { // lgtm [js/missing-rate-limiting] + app.get('/logo.png', (req, res) => { res.sendFile(__dirname + '/logo.png'); }); - app.get('/', (req, res) => { // lgtm [js/missing-rate-limiting] + app.get('/', (req, res) => { res.sendFile(__dirname + '/index.html'); }); - app.get('/test', (req, res) => { // lgtm [js/missing-rate-limiting] + app.get('/test', (req, res) => { res.sendFile(__dirname + '/test.html'); }); - app.get('/style.css', (req, res) => { // lgtm [js/missing-rate-limiting] + app.get('/style.css', (req, res) => { res.sendFile(__dirname + '/style.css'); }); From ab8ce61e56945a834ceb2cf086dfc61a88c352f9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 31 Aug 2022 07:26:23 -0700 Subject: [PATCH 24/36] Remove python scanning --- .github/workflows/codeql-analysis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9afca6e8..f8c8362a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -32,7 +32,7 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'javascript', 'python' ] + language: [ 'javascript' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support @@ -48,11 +48,11 @@ jobs: # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. - + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality - + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild @@ -61,7 +61,7 @@ jobs: # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - # If the Autobuild fails above, remove it and uncomment the following three lines. + # If the Autobuild fails above, remove it and uncomment the following three lines. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # - run: | From 06d6e08347b051955a29b3378c1a47b37acf372a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 1 Sep 2022 00:50:11 +0200 Subject: [PATCH 25/36] sgr_pixels mouse report --- bin/test_mousemodes.js | 56 +++++--- src/browser/Terminal.ts | 8 +- src/browser/TestUtils.test.ts | 2 +- src/browser/input/Mouse.ts | 14 -- src/browser/services/MouseService.ts | 31 ++++- src/browser/services/Services.ts | 2 +- src/common/InputHandler.ts | 6 + src/common/Types.d.ts | 3 + src/common/services/CoreMouseService.test.ts | 132 ++++++++++--------- src/common/services/CoreMouseService.ts | 34 +++-- 10 files changed, 172 insertions(+), 116 deletions(-) diff --git a/bin/test_mousemodes.js b/bin/test_mousemodes.js index 6a547ab1..b73cd455 100644 --- a/bin/test_mousemodes.js +++ b/bin/test_mousemodes.js @@ -123,19 +123,19 @@ const ENC = { row: report[5] - 32 }) ], - 'UTF8' : [ - '\x1b[?1005h', - // format: CSI M