Compare commits

...
Author SHA1 Message Date
Anthony Kim c9beccb3d1 Prevent image jitter when resizing in demo 2026-02-11 22:03:02 -08:00
Anthony Kim f0d291f645 Merge pull request #5694 from xtermjs/anthonykim1/cleanDemoClientTypes
Correct alphabetical ordering in demo/client/types.ts
2026-02-11 09:45:22 -08:00
Daniel Imms f6b98291cf Merge pull request #5692 from xtermjs/anthonykim1/bumpWasmIIPHandlerChanges
Update wasm decoder to 0.3.0 changes in IIPHandler.ts
2026-02-11 05:43:44 -08:00
Anthony Kim f7118b19c6 Correct alphabetical ordering in demo/client/types.ts 2026-02-10 19:08:12 -08:00
Anthony Kim d9b5d150ae Put the const integer into local const enum, save indirection time at runtime 2026-02-10 16:31:17 -08:00
Anthony Kim a25db1c76d Update wasm to 0.3.0 changes in IIPHandler.ts 2026-02-10 14:35:34 -08:00
Daniel Imms c6a572fe21 Merge pull request #5688 from Tyriar/5687
Fix kitty modifier events when only flag 2 is used
2026-02-10 01:31:15 -08:00
Daniel Imms b9fa53169f Fix kitty modifier events when only flag 2 is used
Fixes #5687
2026-02-10 01:17:44 -08:00
Daniel Imms e14746e6e9 Merge pull request #5682 from Tyriar/3440
Add attributesEqual and underline diff APIs
2026-02-07 06:37:19 -08:00
Daniel Imms 82333b9414 Remove unnecessary cast 2026-02-07 06:29:33 -08:00
Daniel Imms 9eeb2799c1 Remove internal details about underline cell APIs 2026-02-07 06:24:47 -08:00
Daniel Imms 5630471480 Merge pull request #5684 from Tyriar/5681
Make 0x1FB98 and 0x1FB99 perfectly tile
2026-02-07 06:24:05 -08:00
Daniel Imms e0c59e68b2 Make 0x1FB98 and 0x1FB99 perfectly tile
Fixes #5681
2026-02-07 06:12:47 -08:00
Daniel Imms cc8e680790 Add attributesEqual and underline diff APIs
Fixes #3440
2026-02-06 16:21:25 -08:00
Daniel Imms 452ee50c81 Merge pull request #5669 from Tyriar/custom_glyph_alignment
Use offset variants for some custom glyphs
2026-02-06 12:58:20 -08:00
Daniel Imms f41e53a0f0 Use correct non-inverse shade char in testWindow 2026-02-06 12:50:28 -08:00
Daniel Imms b287397c4f Merge pull request #5680 from Tyriar/5678
Move overviewRuler API under scrollbar
2026-02-05 09:29:16 -08:00
Daniel Imms aebbe81252 Add overviewRuler options to demo 2026-02-05 09:19:26 -08:00
Daniel Imms d155bb6767 Move overviewRuler API under scrollbar
Fixes #5678
2026-02-05 09:14:58 -08:00
Daniel Imms 3a147e8e10 Merge pull request #5679 from Tyriar/scrollbar
Add scrollbar.showScrollbar
2026-02-05 07:28:45 -08:00
Daniel Imms c2db7d4c8a Add scrollbar.showScrollbar
Fixes #5676
2026-02-05 07:20:23 -08:00
Daniel Imms 456bc73bae Merge pull request #5677 from Tyriar/log_task
Use ILogService in TaskQueue
2026-02-05 06:49:05 -08:00
Daniel Imms 2e776c0820 Use ILogService in TaskQueue 2026-02-05 06:40:50 -08:00
Daniel Imms 6f163beb97 Merge pull request #5674 from xtermjs/anthonykim1/addScreenElementAPI
Add api to expose screenElement
2026-02-04 04:40:37 -08:00
Daniel Imms b0d0bf8187 Update typings/xterm.d.ts 2026-02-04 04:32:37 -08:00
Anthony Kim 32e77c3b81 Lint 2026-02-03 23:24:57 -08:00
Anthony Kim cba6e3d191 Add api to expose screenElement 2026-02-03 15:49:07 -08:00
Daniel Imms 5741e450e6 Merge pull request #5665 from Tyriar/de_vs
Remove vs/ module
2026-02-03 07:14:08 -08:00
Daniel Imms 6ef653e589 Merge remote-tracking branch 'upstream/master' into de_vs 2026-02-03 07:05:50 -08:00
Daniel Imms a4c0e4690e Make arrows a little less fat 2026-02-03 07:00:33 -08:00
Daniel Imms a31f78bc8b Prefix all scrollbar classes with xterm- 2026-02-03 06:57:50 -08:00
Daniel Imms ca8e8546b5 Remove unused methods on scrollable element 2026-02-03 06:00:24 -08:00
Daniel Imms 3d473b52b6 Further merge and reduce
- Remove unused keyboardEvent module
- Trim unused browserEvent in OnStopCallback
- Inline single-use helpers to reduce module surface
- Remove createFastDomNode wrapper
2026-02-03 05:21:03 -08:00
Daniel Imms a2bec2aeab Merge pull request #5672 from Tyriar/blink_opt
Prevent blink interval from running unless needed
2026-02-03 05:15:45 -08:00
Daniel Imms 205cf6089d Prevent blink interval from running unless needed
Part of #944
2026-02-03 05:07:40 -08:00
Daniel Imms 8b0a6f9785 Enumerate all block pattern codepoints 2026-02-03 05:04:03 -08:00
Daniel Imms db0226be5c Add triangular shade chars fill tests 2026-02-03 04:59:10 -08:00
Daniel Imms ab5c06cf17 Merge abstract and smooth scrollable elements 2026-02-03 04:55:32 -08:00
Daniel Imms 9a778f0c98 Move back to CellColorResolver 2026-02-03 04:53:02 -08:00
Daniel Imms c102b4af50 Merge pull request #5671 from Tyriar/944
Implement blink rendering
2026-02-03 04:03:20 -08:00
Daniel Imms f3a7cac49f Handle options within blink state manager 2026-02-03 03:55:25 -08:00
Daniel Imms 0d2a58c9d0 Clean up lifecycle of blink manager 2026-02-03 03:48:28 -08:00
Daniel Imms 8a00898533 Implement blink rendering
Fixes #944
2026-02-03 03:46:21 -08:00
Daniel Imms edb0e1ceb3 Simplify, reduce
- Merge ScrollableElement into SmoothScrollableElement
- Prune unused helpers FastDomNode, Widget LinkedList
- Make internal exports non-exported
- Remove redundant guards/aliases
2026-02-03 03:42:19 -08:00
Daniel Imms 93c1650446 Move variant handling out of CellColorResolver.ts 2026-02-03 03:37:57 -08:00
Daniel Imms 77695cb405 Make arrows configurable via API 2026-02-02 12:33:33 -08:00
Daniel Imms dc4ebfd6e3 Use offset variants for some custom glyphs
Part of #5668
2026-02-02 08:07:04 -08:00
Daniel Imms fae6f03f90 Consolidate dom and browser helpers, remove unused 2026-02-02 07:09:33 -08:00
Daniel Imms d36af624cf Remove vs/, fix lint and build 2026-02-02 06:10:43 -08:00
Daniel Imms a105d3e457 Lint and remove more parts 2026-02-01 22:36:14 -08:00
Daniel Imms d5900c7eae Remove more parts 2026-02-01 22:22:29 -08:00
Daniel Imms beebe8b5dc Reduce size of scrollable impl 2026-02-01 21:28:18 -08:00
Daniel Imms f666135881 Move all impl to scrollable/ 2026-02-01 06:25:12 -08:00
Daniel Imms eea03cf0a9 Use lightweight versions of most vs/ imports 2026-01-31 22:52:19 -08:00
160 changed files with 4587 additions and 19907 deletions
+3 -2
View File
@@ -69,9 +69,10 @@ export class FitAddon implements ITerminalAddon, IFitApi {
return undefined;
}
const scrollbarWidth = (this._terminal.options.scrollback === 0
const showScrollbar = this._terminal.options.scrollbar?.showScrollbar ?? true;
const scrollbarWidth = (this._terminal.options.scrollback === 0 || !showScrollbar
? 0
: (this._terminal.options.overviewRuler?.width || ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH));
: (this._terminal.options.scrollbar?.width ?? ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH));
const parentElementStyle = _getComputedStyle(this._terminal.element.parentElement);
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
+1 -1
View File
@@ -25,6 +25,6 @@
},
"devDependencies": {
"sixel": "^0.16.0",
"xterm-wasm-parts": "^0.1.0"
"xterm-wasm-parts": "^0.3.0"
}
}
+27 -14
View File
@@ -9,8 +9,15 @@ import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
// limit hold memory in base64 decoder
const KEEP_DATA = 4194304;
// Local const enum mirror - esbuild can't inline const enums from external packages
const enum DecoderConst {
// Limit held memory in base64 decoder (encoded bytes).
KEEP_DATA = 4194304,
// Initial buffer allocation for the decoder.
INITIAL_DATA = 1048576,
// Local mirror of const enum (esbuild can't inline const enums from external packages)
OK = 0
}
// default IIP header values
const DEFAULT_HEADER: IHeaderFields = {
@@ -27,7 +34,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
private _aborted = false;
private _hp = new HeaderParser();
private _header: IHeaderFields = DEFAULT_HEADER;
private _dec = new Base64Decoder(KEEP_DATA);
private _dec: Base64Decoder;
private _metrics = UNSUPPORTED_TYPE;
constructor(
@@ -35,7 +42,11 @@ export class IIPHandler implements IOscHandler, IResetHandler {
private readonly _renderer: ImageRenderer,
private readonly _storage: ImageStorage,
private readonly _coreTerminal: ITerminalExt
) {}
) {
const maxEncodedBytes = Math.ceil(this._opts.iipSizeLimit * 4 / 3);
const initialBytes = Math.min(DecoderConst.INITIAL_DATA, maxEncodedBytes);
this._dec = new Base64Decoder(DecoderConst.KEEP_DATA, maxEncodedBytes, initialBytes);
}
public reset(): void {}
@@ -50,7 +61,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
if (this._aborted) return;
if (this._hp.state === HeaderState.END) {
if (this._dec.put(data, start, end)) {
if ((this._dec.put(data.subarray(start, end)) as number) !== DecoderConst.OK) {
this._dec.release();
this._aborted = true;
}
@@ -66,8 +77,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
this._aborted = true;
return;
}
this._dec.init(this._header.size);
if (this._dec.put(data, dataPos, end)) {
this._dec.init();
if ((this._dec.put(data.subarray(dataPos, end)) as number) !== DecoderConst.OK) {
this._dec.release();
this._aborted = true;
}
@@ -85,13 +96,15 @@ export class IIPHandler implements IOscHandler, IResetHandler {
let cond: number | boolean = true;
if (cond = success) {
if (cond = !this._dec.end()) {
this._metrics = imageType(this._dec.data8);
if (cond = this._metrics.mime !== 'unsupported') {
w = this._metrics.width;
h = this._metrics.height;
if (cond = w && h && w * h < this._opts.pixelLimit) {
[w, h] = this._resize(w, h).map(Math.floor);
cond = w && h && w * h < this._opts.pixelLimit;
if (cond = this._dec.data8.length === this._header.size) {
this._metrics = imageType(this._dec.data8);
if (cond = this._metrics.mime !== 'unsupported') {
w = this._metrics.width;
h = this._metrics.height;
if (cond = w && h && w * h < this._opts.pixelLimit) {
[w, h] = this._resize(w, h).map(Math.floor);
cond = w && h && w * h < this._opts.pixelLimit;
}
}
}
}
+7 -9
View File
@@ -6,7 +6,7 @@
import { toRGBA8888 } from 'sixel/lib/Colors';
import { IDisposable } from '@xterm/xterm';
import { ICellSize, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
const PLACEHOLDER_LENGTH = 4096;
const PLACEHOLDER_HEIGHT = 24;
@@ -183,14 +183,12 @@ export class ImageRenderer extends Disposable implements IDisposable {
const finalWidth = count * width + sx > img.width ? img.width - sx : count * width;
const finalHeight = sy + height > img.height ? img.height - sy : height;
// Floor all pixel offsets to get stable tile mapping without any overflows.
// Note: For not pixel perfect aligned cells like in the DOM renderer
// this will move a tile slightly to the top/left (subpixel range, thus ignore it).
// FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
// Let the canvas handle sub-pixel coordinates natively to avoid
// inconsistent rounding that causes tile gaps/overlaps during resize.
this._ctx.drawImage(
img,
Math.floor(sx), Math.floor(sy), Math.ceil(finalWidth), Math.ceil(finalHeight),
Math.floor(dx), Math.floor(dy), Math.ceil(finalWidth), Math.ceil(finalHeight)
sx, sy, finalWidth, finalHeight,
dx, dy, finalWidth, finalHeight
);
}
@@ -216,8 +214,8 @@ export class ImageRenderer extends Disposable implements IDisposable {
if (ctx) {
ctx.drawImage(
img,
Math.floor(sx), Math.floor(sy), Math.floor(finalWidth), Math.floor(finalHeight),
0, 0, Math.floor(finalWidth), Math.floor(finalHeight)
sx, sy, finalWidth, finalHeight,
0, 0, canvas.width, canvas.height
);
return canvas;
}
+1 -3
View File
@@ -15,7 +15,6 @@
"paths": {
"browser/*": [ "../../../src/browser/*" ],
"common/*": [ "../../../src/common/*" ],
"vs/*": [ "../../../src/vs/*" ],
"@xterm/addon-image": [ "../typings/addon-image.d.ts" ]
}
},
@@ -25,7 +24,6 @@
],
"references": [
{ "path": "../../../src/browser" },
{ "path": "../../../src/common" },
{ "path": "../../../src/vs" }
{ "path": "../../../src/common" }
]
}
+2 -2
View File
@@ -5,7 +5,7 @@
import type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm';
import type { ProgressAddon as IProgressApi, IProgressState } from '@xterm/addon-progress';
import type { Emitter, Event } from 'vs/base/common/event';
import type { Emitter, IEvent } from 'common/Event';
const enum ProgressType {
@@ -39,7 +39,7 @@ export class ProgressAddon implements ITerminalAddon, IProgressApi {
private _pr = 0;
// HACK: This uses ! to align with the API, this should be fixed when 5283 is resolved
private _onChange!: Emitter<IProgressState>;
public onChange!: Event<IProgressState>;
public onChange!: IEvent<IProgressState>;
public dispose(): void {
this._seqHandler?.dispose();
+4 -4
View File
@@ -15,12 +15,12 @@
"../../../node_modules/@types/mocha"
],
"paths": {
"common/*": [
"../../../src/common/*"
],
"browser/*": [
"../../../src/browser/*"
],
"vs/*": [
"../../../src/vs/*"
],
"@xterm/addon-progress": [
"../typings/addon-progress.d.ts"
]
@@ -35,7 +35,7 @@
"path": "../../../src/browser"
},
{
"path": "../../../src/vs"
"path": "../../../src/common"
}
]
}
+1 -1
View File
@@ -5,7 +5,7 @@
import type { Terminal, IDisposable, IDecoration } from '@xterm/xterm';
import type { ISearchDecorationOptions } from '@xterm/addon-search';
import { dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { dispose, Disposable, toDisposable } from 'common/Lifecycle';
import type { ISearchResult } from './SearchEngine';
/**
+4 -4
View File
@@ -5,9 +5,9 @@
import type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm';
import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent, ISearchDecorationOptions } from '@xterm/addon-search';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { disposableTimeout } from 'vs/base/common/async';
import { Emitter, type IEvent } from 'common/Event';
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
import { disposableTimeout } from 'common/Async';
import { SearchLineCache } from './SearchLineCache';
import { SearchState } from './SearchState';
import { SearchEngine, type ISearchResult } from './SearchEngine';
@@ -47,7 +47,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
private readonly _onBeforeSearch = this._register(new Emitter<void>());
public readonly onBeforeSearch = this._onBeforeSearch.event;
public get onDidChangeResults(): Event<ISearchResultChangeEvent> {
public get onDidChangeResults(): IEvent<ISearchResultChangeEvent> {
return this._resultTracker.onDidChangeResults;
}
+1 -1
View File
@@ -7,7 +7,7 @@ import { SearchEngine } from './SearchEngine';
import { SearchLineCache } from './SearchLineCache';
import { Terminal } from 'browser/public/Terminal';
import type { ISearchOptions } from '@xterm/addon-search';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { DisposableStore } from 'common/Lifecycle';
function writeP(terminal: Terminal, data: string): Promise<void> {
return new Promise(r => terminal.write(data, r));
@@ -5,7 +5,7 @@
import { assert } from 'chai';
import { SearchLineCache, LineCacheEntry } from './SearchLineCache';
import { Terminal } from 'browser/public/Terminal';
import { timeout } from 'vs/base/common/async';
import { timeout } from 'common/Async';
function writeP(terminal: Terminal, data: string): Promise<void> {
return new Promise(r => terminal.write(data, r));
+2 -2
View File
@@ -4,8 +4,8 @@
*/
import type { Terminal } from '@xterm/xterm';
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { disposableTimeout } from 'vs/base/common/async';
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
import { disposableTimeout } from 'common/Async';
export type LineCacheEntry = [
/**
@@ -5,8 +5,8 @@
import type { ISearchResultChangeEvent } from '@xterm/addon-search';
import type { IDisposable } from '@xterm/xterm';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { Emitter, type IEvent } from 'common/Event';
import { Disposable } from 'common/Lifecycle';
import type { ISearchResult } from './SearchEngine';
/**
@@ -25,7 +25,7 @@ export class SearchResultTracker extends Disposable {
private _selectedDecoration: ISelectedDecoration | undefined;
private readonly _onDidChangeResults = this._register(new Emitter<ISearchResultChangeEvent>());
public get onDidChangeResults(): Event<ISearchResultChangeEvent> { return this._onDidChangeResults.event; }
public get onDidChangeResults(): IEvent<ISearchResultChangeEvent> { return this._onDidChangeResults.event; }
/**
* Gets the current search results.
-6
View File
@@ -21,9 +21,6 @@
"browser/*": [
"../../../src/browser/*"
],
"vs/*": [
"../../../src/vs/*"
],
"@xterm/addon-search" : [
"../typings/addon-search.d.ts"
]
@@ -39,9 +36,6 @@
},
{
"path": "../../../src/browser"
},
{
"path": "../../../src/vs"
}
]
}
@@ -116,7 +116,7 @@ describe('SerializeAddon', () => {
describe('underline styles', () => {
it('should serialize single underline with style', async () => {
await writeP(terminal, sgr('4:1') + 'test' + sgr('24'));
assert.equal(serializeAddon.serialize(), '\u001b[4:1mtest\u001b[0m');
assert.equal(serializeAddon.serialize(), '\u001b[4mtest\u001b[0m');
});
it('should serialize double underline', async () => {
+40 -16
View File
@@ -89,11 +89,19 @@ function equalUnderline(cell1: IBufferCell | IAttributeData, cell2: IBufferCell)
if (!cell1.isUnderline() && !cell2.isUnderline()) {
return true;
}
const cell1Data = cell1 as unknown as IAttributeData;
const cell2Data = cell2 as unknown as IAttributeData;
return cell1Data.getUnderlineStyle() === cell2Data.getUnderlineStyle()
&& cell1Data.getUnderlineColor() === cell2Data.getUnderlineColor()
&& cell1Data.getUnderlineColorMode() === cell2Data.getUnderlineColorMode();
if (cell1.getUnderlineStyle() !== cell2.getUnderlineStyle()) {
return false;
}
const cell1Default = cell1.isUnderlineColorDefault();
const cell2Default = cell2.isUnderlineColorDefault();
if (cell1Default && cell2Default) {
return true;
}
if (cell1Default !== cell2Default) {
return false;
}
return cell1.getUnderlineColor() === cell2.getUnderlineColor()
&& cell1.getUnderlineColorMode() === cell2.getUnderlineColorMode();
}
function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
@@ -109,6 +117,16 @@ function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): bo
&& cell1.isStrikethrough() === cell2.isStrikethrough();
}
function attributesEquals(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
const cell1AsBufferCell = cell1 as IBufferCell;
if (typeof cell1AsBufferCell.attributesEquals === 'function') {
return cell1AsBufferCell.attributesEquals(cell2);
}
return equalFg(cell1, cell2)
&& equalBg(cell1, cell2)
&& equalFlags(cell1, cell2);
}
class StringSerializeHandler extends BaseSerializeHandler {
private _rowIndex: number = 0;
private _allRows: string[] = new Array<string>();
@@ -258,6 +276,9 @@ class StringSerializeHandler extends BaseSerializeHandler {
private _diffStyle(cell: IBufferCell | IAttributeData, oldCell: IBufferCell): number[] {
const sgrSeq: number[] = [];
if (attributesEquals(cell, oldCell)) {
return sgrSeq;
}
const fgChanged = !equalFg(cell, oldCell);
const bgChanged = !equalBg(cell, oldCell);
const flagsChanged = !equalFlags(cell, oldCell);
@@ -290,17 +311,18 @@ class StringSerializeHandler extends BaseSerializeHandler {
if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); }
if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); }
if (!equalUnderline(cell, oldCell)) {
const cellData = cell as unknown as IAttributeData;
const style = cellData.getUnderlineStyle();
const style = cell.getUnderlineStyle();
if (style === UnderlineStyle.NONE) {
sgrSeq.push(24);
} else if (style === UnderlineStyle.SINGLE && cell.isUnderlineColorDefault()) {
sgrSeq.push(4);
} else {
// Use SGR 4:X format for underline styles
sgrSeq.push('4:' + style as unknown as number);
// Handle underline color
if (!cellData.isUnderlineColorDefault()) {
const color = cellData.getUnderlineColor();
if (cellData.isUnderlineColorRGB()) {
if (!cell.isUnderlineColorDefault()) {
const color = cell.getUnderlineColor();
if (cell.isUnderlineColorRGB()) {
sgrSeq.push('58:2::' + ((color >>> 16) & 0xFF) + ':' + ((color >>> 8) & 0xFF) + ':' + (color & 0xFF) as unknown as number);
} else {
sgrSeq.push('58:5:' + color as unknown as number);
@@ -675,12 +697,11 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
}
private _getUnderlineColor(cell: IBufferCell): string | undefined {
const cellData = cell as unknown as IAttributeData;
if (cellData.isUnderlineColorDefault()) {
if (cell.isUnderlineColorDefault()) {
return undefined;
}
const color = cellData.getUnderlineColor();
if (cellData.isUnderlineColorRGB()) {
const color = cell.getUnderlineColor();
if (cell.isUnderlineColorRGB()) {
const rgb = [
(color >> 16) & 255,
(color >> 8) & 255,
@@ -693,8 +714,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
}
private _getUnderlineStyle(cell: IBufferCell): string {
const cellData = cell as unknown as IAttributeData;
switch (cellData.getUnderlineStyle()) {
switch (cell.getUnderlineStyle()) {
case UnderlineStyle.SINGLE:
return 'underline';
case UnderlineStyle.DOUBLE:
@@ -713,6 +733,10 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): string[] | undefined {
const content: string[] = [];
if (attributesEquals(cell, oldCell)) {
return undefined;
}
const fgChanged = !equalFg(cell, oldCell);
const bgChanged = !equalBg(cell, oldCell);
const flagsChanged = !equalFlags(cell, oldCell);
@@ -203,6 +203,46 @@ test.describe('SerializeAddon', () => {
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
});
test('buffer cell attributesEquals compares underline style and color', async () => {
await ctx.proxy.write(`${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}A${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}B${sgr(NORMAL)}`);
const sameAttributes = await ctx.page.evaluate(`(() => {
const line = window.term.buffer.active.getLine(0);
const cellA = line?.getCell(0);
const cellB = line?.getCell(1);
if (!cellA || !cellB) {
return undefined;
}
return cellA.attributesEquals(cellB);
})()`);
strictEqual(sameAttributes, true);
await ctx.page.evaluate(`window.term.reset()`);
await ctx.proxy.write(`${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}A${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_GREEN)}B${sgr(NORMAL)}`);
const differentColor = await ctx.page.evaluate(`(() => {
const line = window.term.buffer.active.getLine(0);
const cellA = line?.getCell(0);
const cellB = line?.getCell(1);
if (!cellA || !cellB) {
return undefined;
}
return cellA.attributesEquals(cellB);
})()`);
strictEqual(differentColor, false);
await ctx.page.evaluate(`window.term.reset()`);
await ctx.proxy.write(`${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}A${sgr(UNDERLINED, UNDERLINE_COLOR_RED)}B${sgr(NORMAL)}`);
const differentStyle = await ctx.page.evaluate(`(() => {
const line = window.term.buffer.active.getLine(0);
const cellA = line?.getCell(0);
const cellB = line?.getCell(1);
if (!cellA || !cellB) {
return undefined;
}
return cellA.attributesEquals(cellB);
})()`);
strictEqual(differentStyle, false);
});
test('serialize all rows of content with color256', async function(): Promise<any> {
const rows = 32;
const cols = 10;
@@ -602,6 +642,9 @@ const BOLD = '1';
const DIM = '2';
const ITALIC = '3';
const UNDERLINED = '4';
const UNDERLINE_DOUBLE = '4:2';
const UNDERLINE_COLOR_RED = '58;5;196';
const UNDERLINE_COLOR_GREEN = '58;5;46';
const BLINK = '5';
const INVERSE = '7';
const INVISIBLE = '8';
+5 -2
View File
@@ -7,6 +7,7 @@ import { ICellData } from 'common/Types';
import { Terminal } from '@xterm/xterm';
import { rgba } from 'common/Color';
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
import { blockPatternCodepoints } from './customGlyphs/CustomGlyphDefinitions';
// Work variables to avoid garbage collection
let $fg = 0;
@@ -42,7 +43,7 @@ export class CellColorResolver {
* Resolves colors for the cell, putting the result into the shared {@link result}. This resolves
* overrides, inverse and selection for the cell which can then be used to feed into the renderer.
*/
public resolve(cell: ICellData, x: number, y: number, deviceCellWidth: number): void {
public resolve(cell: ICellData, x: number, y: number, deviceCellWidth: number, deviceCellHeight: number): void {
this.result.bg = cell.bg;
this.result.fg = cell.fg;
this.result.ext = cell.bg & BgFlags.HAS_EXTENDED ? cell.extended.ext : 0;
@@ -63,7 +64,9 @@ export class CellColorResolver {
const lineWidth = Math.max(1, Math.floor(this._optionService.rawOptions.fontSize * this._coreBrowserService.dpr / 15));
$variantOffset = x * deviceCellWidth % (Math.round(lineWidth) * 2);
}
if ($variantOffset === 0 && blockPatternCodepoints.has(code)) {
$variantOffset = ((x * deviceCellWidth) % 2) * 2 + ((y * deviceCellHeight) % 2);
}
// Apply decorations on the bottom layer
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
if (d.backgroundColorRGB) {
+3 -1
View File
@@ -8,6 +8,7 @@ import { ITerminalOptions, Terminal } from '@xterm/xterm';
import { ITerminal, ReadonlyColorSet } from 'browser/Types';
import { ICharAtlasConfig, ITextureAtlas } from './Types';
import { generateConfig, configEquals } from './CharAtlasUtils';
import type { ILogService } from 'common/services/Services';
interface ITextureAtlasCacheEntry {
atlas: ITextureAtlas;
@@ -67,8 +68,9 @@ export function acquireTextureAtlas(
}
const core: ITerminal = (terminal as any)._core;
const logService = (core as any)._logService as ILogService;
const newEntry: ITextureAtlasCacheEntry = {
atlas: new TextureAtlas(document, newConfig, core.unicodeService),
atlas: new TextureAtlas(document, newConfig, core.unicodeService, logService),
config: newConfig,
ownedBy: [terminal]
};
+6 -4
View File
@@ -14,7 +14,7 @@ import { IdleTaskQueue } from 'common/TaskQueue';
import { IColor } from 'common/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { Attributes, DEFAULT_COLOR, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants';
import { IUnicodeService } from 'common/services/Services';
import { ILogService, IUnicodeService } from 'common/services/Services';
import { Emitter } from 'common/Event';
/**
@@ -88,7 +88,8 @@ export class TextureAtlas implements ITextureAtlas {
constructor(
private readonly _document: Document,
private readonly _config: ICharAtlasConfig,
private readonly _unicodeService: IUnicodeService
private readonly _unicodeService: IUnicodeService,
private readonly _logService: ILogService
) {
this._createNewPage();
this._tmpCanvas = createCanvas(
@@ -119,7 +120,7 @@ export class TextureAtlas implements ITextureAtlas {
private _doWarmUp(): void {
// Pre-fill with ASCII 33-126, this is not urgent and done in idle callbacks
const queue = new IdleTaskQueue();
const queue = new IdleTaskQueue(this._logService);
for (let i = 33; i < 126; i++) {
queue.enqueue(() => {
if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT)) {
@@ -525,7 +526,8 @@ export class TextureAtlas implements ITextureAtlas {
// Draw custom characters if applicable
let customGlyph = false;
if (this._config.customGlyphs !== false) {
customGlyph = tryDrawCustomGlyph(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.deviceCharWidth, this._config.deviceCharHeight, this._config.fontSize, this._config.devicePixelRatio, backgroundColor.css);
const variantOffset = this._workAttributeData.getUnderlineVariantOffset();
customGlyph = tryDrawCustomGlyph(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.deviceCharWidth, this._config.deviceCharHeight, this._config.fontSize, this._config.devicePixelRatio, backgroundColor.css, variantOffset);
}
// Whether to clear pixels based on a threshold difference between the glyph color and the
+50 -4
View File
@@ -13,7 +13,8 @@ import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IThemeS
import { CharData, IBufferLine, ICellData } from 'common/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { CellData } from 'common/buffer/CellData';
import { Attributes, Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
import { TextBlinkStateManager } from 'browser/renderer/shared/TextBlinkStateManager';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { Terminal } from '@xterm/xterm';
import { GlyphRenderer } from './GlyphRenderer';
@@ -23,13 +24,14 @@ import { IWebGL2RenderingContext, type ITextureAtlas } from './Types';
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
import { IRenderLayer } from './renderLayer/Types';
import { Emitter, EventUtils } from 'common/Event';
import { addDisposableListener } from 'vs/base/browser/dom';
import { addDisposableListener } from 'browser/Dom';
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
export class WebglRenderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
private _cursorBlinkStateManager: MutableDisposable<CursorBlinkStateManager> = new MutableDisposable();
private _textBlinkStateManager: TextBlinkStateManager;
private _charAtlasDisposable = this._register(new MutableDisposable());
private _charAtlas: ITextureAtlas | undefined;
private _devicePixelRatio: number;
@@ -37,8 +39,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _observerDisposable = this._register(new MutableDisposable());
private _model: RenderModel = new RenderModel();
private _rowHasBlinkingCells: boolean[] = [];
private _rowHasBlinkingCellsCount: number = 0;
private _workCell: ICellData = new CellData();
private _workCell2: ICellData = new CellData();
private _cellColorResolver: CellColorResolver;
private _canvas: HTMLCanvasElement;
@@ -105,6 +108,12 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._updateDimensions();
this._updateCursorBlink();
this._register(_optionsService.onOptionChange(() => this._handleOptionsChanged()));
this._textBlinkStateManager = this._register(new TextBlinkStateManager(
() => this._requestRedrawViewport(),
this._coreBrowserService,
this._optionsService
));
this._resetBlinkingRowState();
this._deviceMaxTextureSize = this._gl.getParameter(this._gl.MAX_TEXTURE_SIZE);
@@ -178,6 +187,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._updateDimensions();
this._model.resize(this._terminal.cols, this._terminal.rows);
this._resetBlinkingRowState();
// Resize all render layers
for (const l of this._renderLayers) {
@@ -231,6 +241,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._requestRedrawViewport();
}
public handleViewportVisibilityChange(isVisible: boolean): void {
this._textBlinkStateManager.setViewportVisible(isVisible);
}
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
for (const l of this._renderLayers) {
l.handleSelectionChanged(this._terminal, start, end, columnSelectMode);
@@ -323,6 +337,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
l.reset(this._terminal);
}
this._resetBlinkingRowState();
this._textBlinkStateManager.setNeedsBlinkInViewport(false);
this._cursorBlinkStateManager.value?.restartBlinkAnimation();
this._updateCursorBlink();
}
@@ -420,6 +437,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
for (y = start; y <= end; y++) {
row = y + terminal.buffer.ydisp;
line = terminal.buffer.lines.get(row)!;
let rowHasBlinkingCells = false;
this._model.lineLengths[y] = 0;
isCursorRow = cursorY === row;
skipJoinedCheckUntilX = 0;
@@ -477,8 +495,12 @@ export class WebglRenderer extends Disposable implements IRenderer {
code = cell.getCode();
i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
if (!rowHasBlinkingCells && cell.isBlink()) {
rowHasBlinkingCells = true;
}
// Load colors/resolve overrides into work colors
this._cellColorResolver.resolve(cell, x, row, this.dimensions.device.cell.width);
this._cellColorResolver.resolve(cell, x, row, this.dimensions.device.cell.width, this.dimensions.device.cell.height);
// Override colors for cursor cell
if (isCursorVisible && row === cursorY) {
@@ -506,6 +528,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
}
if (this._textBlinkStateManager.isEnabled && !this._textBlinkStateManager.isBlinkOn && cell.isBlink()) {
this._cellColorResolver.result.fg |= FgFlags.INVISIBLE;
}
if (code !== NULL_CELL_CODE) {
this._model.lineLengths[y] = x + 1;
}
@@ -552,11 +578,31 @@ export class WebglRenderer extends Disposable implements IRenderer {
x--; // Go back to the previous update cell for next iteration
}
}
this._setRowBlinkState(y, rowHasBlinkingCells);
}
if (modelUpdated) {
this._rectangleRenderer.value!.updateBackgrounds(this._model);
}
this._rectangleRenderer.value!.updateCursor(this._model);
this._updateTextBlinkState();
}
private _resetBlinkingRowState(): void {
this._rowHasBlinkingCells = new Array(this._terminal.rows).fill(false);
this._rowHasBlinkingCellsCount = 0;
}
private _setRowBlinkState(row: number, hasBlinkingCells: boolean): void {
const previous = this._rowHasBlinkingCells[row];
if (previous === hasBlinkingCells) {
return;
}
this._rowHasBlinkingCells[row] = hasBlinkingCells;
this._rowHasBlinkingCellsCount += hasBlinkingCells ? 1 : -1;
}
private _updateTextBlinkState(): void {
this._textBlinkStateManager.setNeedsBlinkInViewport(this._rowHasBlinkingCellsCount > 0);
}
/**
@@ -644,8 +644,8 @@ export const customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefi
] },
// Diagonal fill characters (1FB98-1FB99)
'\u{1FB98}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M0,0 L1,1 M0,.25 L.75,1 M0,.5 L.5,1 M0,.75 L.25,1 M.25,0 L1,.75 M.5,0 L1,.5 M.75,0 L1,.25', strokeWidth: 1 }, // UPPER LEFT TO LOWER RIGHT FILL
'\u{1FB99}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M0,.25 L.25,0 M0,.5 L.5,0 M0,.75 L.75,0 M0,1 L1,0 M.25,1 L1,.25 M.5,1 L1,.5 M.75,1 L1,.75', strokeWidth: 1 }, // UPPER RIGHT TO LOWER LEFT FILL
'\u{1FB98}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M-0.25,-0.25 L1.25,1.25 M-0.25,0 L1,1.25 M-0.25,0.25 L0.75,1.25 M-0.25,0.5 L0.5,1.25 M0,-0.25 L1.25,1 M0.25,-0.25 L1.25,0.75 M0.5,-0.25 L1.25,0.5 M-0.25,0.75 L0.25,1.25 M0.75,-0.25 L1.25,0.25', strokeWidth: 1 }, // UPPER LEFT TO LOWER RIGHT FILL
'\u{1FB99}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M-0.25,0.5 L0.5,-0.25 M-0.25,0.75 L0.75,-0.25 M-0.25,1 L1,-0.25 M-0.25,1.25 L1.25,-0.25 M0,1.25 L1.25,0 M0.25,1.25 L1.25,0.25 M0.5,1.25 L1.25,0.5 M-0.25,0.25 L0.25,-0.25 M0.75,1.25 L1.25,0.75', strokeWidth: 1 }, // UPPER RIGHT TO LOWER LEFT FILL
// Smooth mosaic terminal graphic characters (1FB9A-1FB9B)
'\u{1FB9A}': { type: CustomGlyphDefinitionType.VECTOR_SHAPE, data: { d: 'M0,0 L.5,.5 L0,1 L1,1 L.5,.5 L1,0', type: CustomGlyphVectorType.FILL } }, // UPPER AND LOWER TRIANGULAR HALF BLOCK
@@ -820,6 +820,27 @@ export const customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefi
// #endregion
};
export const blockPatternCodepoints = new Set<number>([
// Shade characters (2591-2593)
0x2591,
0x2592,
0x2593,
// Rectangular shade characters (1FB8C-1FB94)
0x1FB8C,
0x1FB8D,
0x1FB8E,
0x1FB8F,
0x1FB90,
0x1FB91,
0x1FB92,
0x1FB94,
// Triangular shade characters (1FB9C-1FB9F)
0x1FB9C,
0x1FB9D,
0x1FB9E,
0x1FB9F
]);
/**
* Generates a drawing function for sextant characters. Sextants are a 2x3 grid where each cell
* can be on or off.
@@ -22,14 +22,15 @@ export function tryDrawCustomGlyph(
deviceCharHeight: number,
fontSize: number,
devicePixelRatio: number,
backgroundColor?: string
backgroundColor?: string,
variantOffset: number = 0
): boolean {
const unifiedCharDefinition = customGlyphDefinitions[c];
if (unifiedCharDefinition) {
// Normalize to array for uniform handling
const parts = Array.isArray(unifiedCharDefinition) ? unifiedCharDefinition : [unifiedCharDefinition];
for (const part of parts) {
drawDefinitionPart(ctx, part, xOffset, yOffset, deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, fontSize, devicePixelRatio, backgroundColor);
drawDefinitionPart(ctx, part, xOffset, yOffset, deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, fontSize, devicePixelRatio, backgroundColor, variantOffset);
}
return true;
}
@@ -48,7 +49,8 @@ function drawDefinitionPart(
deviceCharHeight: number,
fontSize: number,
devicePixelRatio: number,
backgroundColor?: string
backgroundColor?: string,
variantOffset: number = 0
): void {
// Handle scaleType - adjust dimensions and offset when scaling to character area
let drawWidth = deviceCellWidth;
@@ -74,7 +76,7 @@ function drawDefinitionPart(
drawBlockVectorChar(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight);
break;
case CustomGlyphDefinitionType.BLOCK_PATTERN:
drawPatternChar(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight);
drawPatternChar(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, variantOffset);
break;
case CustomGlyphDefinitionType.PATH_FUNCTION:
drawPathFunctionCharacter(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, devicePixelRatio, part.strokeWidth);
@@ -437,7 +439,8 @@ function drawPatternChar(
xOffset: number,
yOffset: number,
deviceCellWidth: number,
deviceCellHeight: number
deviceCellHeight: number,
variantOffset: number = 0
): void {
let patternSet = cachedPatterns.get(charDefinition);
if (!patternSet) {
@@ -486,6 +489,15 @@ function drawPatternChar(
pattern = throwIfFalsy(ctx.createPattern(tmpCanvas, null));
patternSet.set(fillStyle, pattern);
}
// Apply pattern offset to ensure seamless tiling across cells when cell dimensions are odd.
// variantOffset encodes: bit 1 = x pixel shift, bit 0 = y pixel shift.
const dx = (variantOffset >> 1) & 1;
const dy = variantOffset & 1;
if (dx !== 0 || dy !== 0) {
pattern.setTransform(new DOMMatrix().translateSelf(-dx, -dy));
} else {
pattern.setTransform(new DOMMatrix());
}
ctx.fillStyle = pattern;
ctx.fillRect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
}
@@ -500,6 +512,11 @@ function drawPathFunctionCharacter(
devicePixelRatio: number,
strokeWidth?: number
): void {
ctx.save();
ctx.beginPath();
ctx.rect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
ctx.clip();
ctx.beginPath();
let actualInstructions: string;
if (typeof charDefinition === 'function') {
@@ -526,7 +543,7 @@ function drawPathFunctionCharacter(
if (!args[0] || !args[1]) {
continue;
}
f(ctx, translateArgs(args, deviceCellWidth, deviceCellHeight, xOffset, yOffset, true, devicePixelRatio), state);
f(ctx, translateArgs(args, deviceCellWidth, deviceCellHeight, xOffset, yOffset, true, devicePixelRatio, 0, 0, false), state);
state.lastCommand = type;
}
if (strokeWidth !== undefined) {
@@ -537,6 +554,7 @@ function drawPathFunctionCharacter(
ctx.fill();
}
ctx.closePath();
ctx.restore();
}
/**
@@ -685,7 +703,7 @@ const svgToCanvasInstructionMap: { [index: string]: (ctx: CanvasRenderingContext
}
};
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, doClamp: boolean, devicePixelRatio: number, leftPadding: number = 0, rightPadding: number = 0): number[] {
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, doClamp: boolean, devicePixelRatio: number, leftPadding: number = 0, rightPadding: number = 0, clampToCell: boolean = true): number[] {
const result = args.map(e => parseFloat(e) || parseInt(e));
if (result.length < 2) {
@@ -695,10 +713,11 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO
for (let x = 0; x < result.length; x += 2) {
// Translate from 0-1 to 0-cellWidth
result[x] *= cellWidth - (leftPadding * devicePixelRatio) - (rightPadding * devicePixelRatio);
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
// line at 100% devicePixelRatio
// Round to the nearest 0.5 to ensure a crisp line at 100% devicePixelRatio, and optionally
// clamp to the cell bounds.
if (doClamp && result[x] !== 0) {
result[x] = clamp(Math.round(result[x] + 0.5) - 0.5, cellWidth, 0);
const rounded = Math.round(result[x] + 0.5) - 0.5;
result[x] = clampToCell ? clamp(rounded, cellWidth, 0) : rounded;
}
// Apply the cell's offset (ie. x*cellWidth)
result[x] += xOffset + (leftPadding * devicePixelRatio);
@@ -707,10 +726,11 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO
for (let y = 1; y < result.length; y += 2) {
// Translate from 0-1 to 0-cellHeight
result[y] *= cellHeight;
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
// line at 100% devicePixelRatio
// Round to the nearest 0.5 to ensure a crisp line at 100% devicePixelRatio, and optionally
// clamp to the cell bounds.
if (doClamp && result[y] !== 0) {
result[y] = clamp(Math.round(result[y] + 0.5) - 0.5, cellHeight, 0);
const rounded = Math.round(result[y] + 0.5) - 0.5;
result[y] = clampToCell ? clamp(rounded, cellHeight, 0) : rounded;
}
// Apply the cell's offset (ie. x*cellHeight)
result[y] += yOffset;
-6
View File
@@ -18,9 +18,6 @@
"browser/*": [
"../../../src/browser/*"
],
"vs/*": [
"../../../src/vs/*"
],
"@xterm/addon-webgl": [
"../typings/addon-webgl.d.ts"
]
@@ -42,9 +39,6 @@
},
{
"path": "../../../src/browser"
},
{
"path": "../../../src/vs"
}
]
}
+2 -3
View File
@@ -127,6 +127,7 @@ if (config.addon) {
} else if (config.isDemoClient) {
bundleConfig = {
...bundleConfig,
sourcemap: false,
entryPoints: [`demo/client/client.ts`],
outfile: 'demo/dist/client-bundle.js',
external: ['util', 'os', 'fs', 'path', 'stream', 'Terminal'],
@@ -190,9 +191,7 @@ if (config.addon) {
entryPoints: [
'src/browser/**/*.ts',
'src/common/**/*.ts',
'src/headless/**/*.ts',
'src/vs/base/**/*.ts',
'src/vs/patches/**/*.ts'
'src/headless/**/*.ts'
],
outdir: 'out-esbuild/'
};
+24 -11
View File
@@ -218,17 +218,30 @@
/* Derived from vs/base/browser/ui/scrollbar/media/scrollbar.css */
/* xterm.js customization: Override xterm's cursor style */
.xterm .xterm-scrollable-element > .scrollbar {
.xterm .xterm-scrollable-element > .xterm-scrollbar {
cursor: default;
}
/* Arrows */
.xterm .xterm-scrollable-element > .scrollbar > .scra {
.xterm .xterm-scrollable-element > .xterm-scrollbar > .xterm-scra {
cursor: pointer;
font-size: 11px !important;
background-color: var(--vscode-scrollbarSliderBackground, rgba(100, 100, 100, 0.4));
mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 11 11'><path d='M2.5 8.5 L5.5 2.5 L8.5 8.5 Z' fill='black'/></svg>");
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 11 11'><path d='M2.5 8.5 L5.5 2.5 L8.5 8.5 Z' fill='black'/></svg>");
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-position: center center;
-webkit-mask-position: center center;
mask-size: 100% 100%;
-webkit-mask-size: 100% 100%;
}
.xterm .xterm-scrollable-element > .visible {
.xterm .xterm-scrollable-element > .xterm-scrollbar > .xterm-scra.xterm-arrow-down {
transform: rotate(180deg);
}
.xterm .xterm-scrollable-element > .xterm-visible {
opacity: 1;
/* Background rule added for IE9 - to allow clicks on dom node */
@@ -238,20 +251,20 @@
/* In front of peek view */
z-index: 11;
}
.xterm .xterm-scrollable-element > .invisible {
.xterm .xterm-scrollable-element > .xterm-invisible {
opacity: 0;
pointer-events: none;
}
.xterm .xterm-scrollable-element > .invisible.fade {
.xterm .xterm-scrollable-element > .xterm-invisible.xterm-fade {
transition: opacity 800ms linear;
}
/* Scrollable Content Inset Shadow */
.xterm .xterm-scrollable-element > .shadow {
.xterm .xterm-scrollable-element > .xterm-shadow {
position: absolute;
display: none;
}
.xterm .xterm-scrollable-element > .shadow.top {
.xterm .xterm-scrollable-element > .xterm-shadow.xterm-shadow-top {
display: block;
top: 0;
left: 3px;
@@ -259,7 +272,7 @@
width: 100%;
box-shadow: var(--vscode-scrollbar-shadow, #000) 0 6px 6px -6px inset;
}
.xterm .xterm-scrollable-element > .shadow.left {
.xterm .xterm-scrollable-element > .xterm-shadow.xterm-shadow-left {
display: block;
top: 3px;
left: 0;
@@ -267,13 +280,13 @@
width: 3px;
box-shadow: var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset;
}
.xterm .xterm-scrollable-element > .shadow.top-left-corner {
.xterm .xterm-scrollable-element > .xterm-shadow.xterm-shadow-top-left-corner {
display: block;
top: 0;
left: 0;
height: 3px;
width: 3px;
}
.xterm .xterm-scrollable-element > .shadow.top.left {
.xterm .xterm-scrollable-element > .xterm-shadow.xterm-shadow-top.xterm-shadow-left {
box-shadow: var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset;
}
+1
View File
@@ -284,6 +284,7 @@ function createTerminal(): Terminal {
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
term = new Terminal({
scrollbar: { showScrollbar: true },
allowProposedApi: true,
windowsPty: isWindows ? {
// In a real scenario, these values should be verified on the backend
+25 -9
View File
@@ -115,17 +115,20 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
'documentOverride',
'linkHandler',
'logger',
'overviewRuler',
'quirks',
'theme',
'vtExtensions',
'windowOptions',
'windowsPty',
];
const nestedBooleanOptions: { label: string, parent: string, prop: string }[] = [
{ label: 'vtExtensions.kittyKeyboard', parent: 'vtExtensions', prop: 'kittyKeyboard' },
{ label: 'vtExtensions.kittySgrBoldFaintControl', parent: 'vtExtensions', prop: 'kittySgrBoldFaintControl' },
{ label: 'vtExtensions.win32InputMode', parent: 'vtExtensions', prop: 'win32InputMode' }
const nestedBooleanOptions: { label: string, path: string[], prop: string }[] = [
{ label: 'scrollbar.showScrollbar', path: ['scrollbar'], prop: 'showScrollbar' },
{ label: 'scrollbar.showArrows', path: ['scrollbar'], prop: 'showArrows' },
{ label: 'scrollbar.overviewRuler.showTopBorder', path: ['scrollbar', 'overviewRuler'], prop: 'showTopBorder' },
{ label: 'scrollbar.overviewRuler.showBottomBorder', path: ['scrollbar', 'overviewRuler'], prop: 'showBottomBorder' },
{ label: 'vtExtensions.kittyKeyboard', path: ['vtExtensions'], prop: 'kittyKeyboard' },
{ label: 'vtExtensions.kittySgrBoldFaintControl', path: ['vtExtensions'], prop: 'kittySgrBoldFaintControl' },
{ label: 'vtExtensions.win32InputMode', path: ['vtExtensions'], prop: 'win32InputMode' }
];
const stringOptions: { [key: string]: string[] | null } = {
cursorStyle: ['block', 'underline', 'bar'],
@@ -161,8 +164,10 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
booleanOptions.forEach(o => {
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${(this._terminal.options as Record<string, unknown>)[o] ? 'checked' : ''}/> ${o}</label></div>`;
});
nestedBooleanOptions.forEach(({ label, parent, prop }) => {
const checked = (this._terminal.options as Record<string, Record<string, unknown> | undefined>)[parent]?.[prop] ?? false;
nestedBooleanOptions.forEach(({ label, path, prop }) => {
const options = this._terminal.options as Record<string, unknown>;
const parent = path.reduce<Record<string, unknown> | undefined>((acc, key) => (acc as Record<string, unknown> | undefined)?.[key] as Record<string, unknown> | undefined, options);
const checked = (parent as Record<string, unknown> | undefined)?.[prop] ?? false;
html += `<div class="option"><label><input id="opt-${label.replace('.', '-')}" type="checkbox" ${checked ? 'checked' : ''}/> ${label}</label></div>`;
});
html += '</div><div class="option-group">';
@@ -196,11 +201,22 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
}
});
});
nestedBooleanOptions.forEach(({ label, parent, prop }) => {
nestedBooleanOptions.forEach(({ label, path, prop }) => {
const input = document.getElementById(`opt-${label.replace('.', '-')}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', label, input.checked);
(this._terminal.options as Record<string, unknown>)[parent] = { ...(this._terminal.options as Record<string, Record<string, unknown> | undefined>)[parent], [prop]: input.checked };
const options = this._terminal.options as Record<string, unknown>;
if (path.length === 1) {
const parentKey = path[0];
options[parentKey] = { ...(options[parentKey] as Record<string, unknown> | undefined), [prop]: input.checked };
return;
}
if (path.length === 2) {
const [parentKey, childKey] = path;
const parent = (options[parentKey] as Record<string, unknown> | undefined) ?? {};
const child = (parent[childKey] as Record<string, unknown> | undefined) ?? {};
options[parentKey] = { ...parent, [childKey]: { ...child, [prop]: input.checked } };
}
});
});
numberOptions.forEach(o => {
+9 -2
View File
@@ -397,6 +397,13 @@ function customGlyphAlignmentHandler(term: Terminal): void {
}
}
term.write('\x1b[0mTriangular fill tests:\x1b[36m\n\r');
term.write('1FB9C 1FB9D 1FB9E 1FB9F all\n\r');
term.write('\u{02592}\u{02592}\u{02592}\u{1FB9C} \u{1FB9D}\u{02592}\u{02592}\u{02592} \u{00020}\u{00020}\u{00020}\u{1FB9E} \u{1FB9F}\u{00020}\u{00020}\u{00020} \u{00020}\u{1FB9E}\u{1FB9F}\u{00020}\n\r');
term.write('\u{02592}\u{02592}\u{1FB9C}\u{00020} \u{00020}\u{1FB9D}\u{02592}\u{02592} \u{00020}\u{00020}\u{1FB9E}\u{02592} \u{02592}\u{1FB9F}\u{00020}\u{00020} \u{1FB9E}\u{02592}\u{02592}\u{1FB9F}\n\r');
term.write('\u{02592}\u{1FB9C}\u{00020}\u{00020} \u{00020}\u{00020}\u{1FB9D}\u{02592} \u{00020}\u{1FB9E}\u{02592}\u{02592} \u{02592}\u{02592}\u{1FB9F}\u{00020} \u{1FB9D}\u{02592}\u{02592}\u{1FB9C}\n\r');
term.write('\u{1FB9C}\u{00020}\u{00020}\u{00020} \u{00020}\u{00020}\u{00020}\u{1FB9D} \u{1FB9E}\u{02592}\u{02592}\u{02592} \u{02592}\u{02592}\u{02592}\u{1FB9F} \u{00020}\u{1FB9D}\u{1FB9C}\u{00020}\n\r');
term.write('\x1b[0mPowerline alignment tests:\n\r');
const powerlineLeftChars = ['\u{E0B2}', '\u{E0B3}', '\u{E0B6}', '\u{E0B7}', '\u{E0BA}', '\u{E0BB}', '\u{E0BE}', '\u{E0BF}', '\u{E0C2}', '\u{E0C3}', '\u{E0C5}', '\u{E0C7}', '\u{E0CA}', '\u{E0D4}'];
const powerlineRightChars = ['\u{E0B0}', '\u{E0B1}', '\u{E0B4}', '\u{E0B5}', '\u{E0B8}', '\u{E0B9}', '\u{E0BC}', '\u{E0BD}', '\u{E0C0}', '\u{E0C1}', '\u{E0C4}', '\u{E0C6}', '\u{E0C8}', '\u{E0D2}', '\u{E0CC}', '\u{E0CD}', '\u{E0CE}', '\u{E0CF}', '\u{E0D0}', '\u{E0D1}'];
@@ -738,7 +745,7 @@ function loadTestLongLines(term: Terminal, addons: AddonCollection): void {
}
function addDecoration(term: Terminal, dim: number = 1): void {
term.options['overviewRuler'] = { width: 14 };
term.options.scrollbar = { ...(term.options.scrollbar ?? {}), width: 14, overviewRuler: term.options.scrollbar?.overviewRuler ?? {} };
const marker = term.registerMarker(1);
const decoration = term.registerDecoration({
marker,
@@ -755,7 +762,7 @@ function addDecoration(term: Terminal, dim: number = 1): void {
}
function addOverviewRuler(term: Terminal): void {
term.options['overviewRuler'] = { width: 14 };
term.options.scrollbar = { ...(term.options.scrollbar ?? {}), width: 14, overviewRuler: term.options.scrollbar?.overviewRuler ?? {} };
term.registerDecoration({ marker: term.registerMarker(1), overviewRulerOptions: { color: '#ef2929' } });
term.registerDecoration({ marker: term.registerMarker(3), overviewRulerOptions: { color: '#8ae234' } });
term.registerDecoration({ marker: term.registerMarker(5), overviewRulerOptions: { color: '#729fcf' } });
+1 -1
View File
@@ -19,7 +19,7 @@ import type { WebFontsAddon } from '@xterm/addon-web-fonts';
import type { WebLinksAddon } from '@xterm/addon-web-links';
import type { WebglAddon } from '@xterm/addon-webgl';
export type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'progress' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webFonts' | 'webLinks' | 'webgl' | 'ligatures';
export type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'ligatures' | 'progress' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webFonts' | 'webLinks' | 'webgl';
export interface IDemoAddon<T extends AddonType> {
name: T;
-1
View File
@@ -10,7 +10,6 @@ export default tseslint.config(
{
ignores: [
'addons/*/src/third-party/*.ts',
'src/vs/*',
'**/out/*',
'**/out-test/*',
'**/out-esbuild/*',
+11 -279
View File
@@ -79,7 +79,7 @@
"license": "MIT",
"devDependencies": {
"sixel": "^0.16.0",
"xterm-wasm-parts": "^0.1.0"
"xterm-wasm-parts": "^0.3.0"
}
},
"addons/addon-ligatures": {
@@ -1278,102 +1278,6 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -1546,16 +1450,6 @@
"pako": "^2.0.4"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@playwright/test": {
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
@@ -3511,12 +3405,6 @@
"node": ">= 0.4"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -4868,54 +4756,12 @@
"node": ">= 0.10"
}
},
"node_modules/inwasm": {
"version": "0.0.13",
"resolved": "https://registry.npmjs.org/inwasm/-/inwasm-0.0.13.tgz",
"integrity": "sha512-gmULhw1wfF3tQ19y0TvcNH6A5jN7IuTD51kbZuy+ittUU59d+ZTQMb53wbGQuciMrledgagL3/ohnjUj5qJikQ==",
"node_modules/inwasm-runtime": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/inwasm-runtime/-/inwasm-runtime-0.1.2.tgz",
"integrity": "sha512-in+Lk4d7PGwQnG1zxRe6qHB2nvvc5LUOsI9ByoJRuaDONTY8xDlaF+CvapGoPiqreJ0cMHxRzKtS0SJBccpYfA==",
"dev": true,
"dependencies": {
"acorn": "^8.8.2",
"acorn-walk": "^8.2.0",
"chokidar": "^3.5.3",
"colorette": "^2.0.20",
"glob": "^10.0.0",
"wabt": "^1.0.32"
},
"bin": {
"inwasm": "lib/cli.js"
}
},
"node_modules/inwasm/node_modules/acorn-walk": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"dev": true,
"dependencies": {
"acorn": "^8.11.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/inwasm/node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
"license": "MIT"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
@@ -5415,21 +5261,6 @@
"node": ">=8"
}
},
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/javascript-natural-sort": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
@@ -5878,15 +5709,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
@@ -6424,12 +6246,6 @@
"node": ">=8"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"dev": true
},
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
@@ -6517,28 +6333,6 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
@@ -7532,21 +7326,6 @@
"node": ">=8"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -7559,19 +7338,6 @@
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-bom": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
@@ -8090,23 +7856,6 @@
"node": ">=18"
}
},
"node_modules/wabt": {
"version": "1.0.39",
"resolved": "https://registry.npmjs.org/wabt/-/wabt-1.0.39.tgz",
"integrity": "sha512-ba+dRL/75VQQY7RkU/CgriGbkoWAfS8TDyUlJfJhJ8KhtXgMl5dhNvoPNUcQ9IWRhW8u41glMSuZeTvsYq2rRg==",
"dev": true,
"bin": {
"wasm-decompile": "bin/wasm-decompile",
"wasm-interp": "bin/wasm-interp",
"wasm-objdump": "bin/wasm-objdump",
"wasm-stats": "bin/wasm-stats",
"wasm-strip": "bin/wasm-strip",
"wasm-validate": "bin/wasm-validate",
"wasm2c": "bin/wasm2c",
"wasm2wat": "bin/wasm2wat",
"wat2wasm": "bin/wat2wasm"
}
},
"node_modules/watchpack": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.0.tgz",
@@ -8450,24 +8199,6 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -8580,12 +8311,13 @@
}
},
"node_modules/xterm-wasm-parts": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/xterm-wasm-parts/-/xterm-wasm-parts-0.1.0.tgz",
"integrity": "sha512-GFE8yNJfdkytGpcsOZhkL3B8XyUqkR/Du3SdxRyFbJg+BBCKm3raaaflgIM4TwNQ2AOLz01BEEuB5FZXx8aNTQ==",
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/xterm-wasm-parts/-/xterm-wasm-parts-0.3.0.tgz",
"integrity": "sha512-V/lhvDv2Scov2ukhTNmmur6jMq2DSL8QOdQdXWgfqcAYbUf7bgixwl2sB1gYPGE21NTw2OvbAw+vANRncWzR4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"inwasm": "^0.0.13"
"inwasm-runtime": "^0.1.2"
}
},
"node_modules/y18n": {
+1 -1
View File
@@ -10,7 +10,7 @@ import { Disposable, toDisposable } from 'common/Lifecycle';
import { ICoreBrowserService, IRenderService } from 'browser/services/Services';
import { IBuffer } from 'common/buffer/Types';
import { IInstantiationService } from 'common/services/Services';
import { addDisposableListener } from 'vs/base/browser/dom';
import { addDisposableListener } from 'browser/Dom';
const MAX_ROWS_TO_READ = 20;
+7 -4
View File
@@ -56,7 +56,7 @@ import { WindowsOptionsReportType } from '../common/InputHandler';
import { AccessibilityManager } from './AccessibilityManager';
import { Linkifier } from './Linkifier';
import { Emitter, EventUtils, type IEvent } from 'common/Event';
import { addDisposableListener } from 'vs/base/browser/dom';
import { addDisposableListener } from 'browser/Dom';
import { MutableDisposable, toDisposable } from 'common/Lifecycle';
export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
@@ -616,11 +616,14 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
}
this._register(this.optionsService.onSpecificOptionChange('screenReaderMode', e => this._handleScreenReaderModeOptionChange(e)));
if (this.options.overviewRuler.width) {
const showScrollbar = this.options.scrollbar?.showScrollbar ?? true;
const overviewRulerWidth = this.options.scrollbar?.width;
if (showScrollbar && overviewRulerWidth) {
this._overviewRulerRenderer = this._register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement));
}
this.optionsService.onSpecificOptionChange('overviewRuler', value => {
if (!this._overviewRulerRenderer && value && this._viewportElement && this.screenElement) {
this.optionsService.onSpecificOptionChange('scrollbar', value => {
const shouldShow = (value?.showScrollbar ?? true) && !!value?.width;
if (!this._overviewRulerRenderer && shouldShow && this._viewportElement && this.screenElement) {
this._overviewRulerRenderer = this._register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement));
}
});
+178
View File
@@ -0,0 +1,178 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*
* Minimal DOM helpers for xterm.js browser code.
*/
import { IntervalTimer } from 'common/Async';
import { IDisposable } from 'common/Lifecycle';
export function getWindow(e: Node | UIEvent | undefined | null): Window {
const candidateNode = e as Node | undefined | null;
if (candidateNode?.ownerDocument?.defaultView) {
return candidateNode.ownerDocument.defaultView;
}
const candidateEvent = e as UIEvent | undefined | null;
if (candidateEvent?.view) {
return candidateEvent.view;
}
return window;
}
class DomListener implements IDisposable {
private _handler: ((e: any) => void) | null;
private _node: EventTarget | null;
private readonly _type: string;
private readonly _options: boolean | AddEventListenerOptions | undefined;
constructor(node: EventTarget, type: string, handler: (e: any) => void, options?: boolean | AddEventListenerOptions) {
this._node = node;
this._type = type;
this._handler = handler;
this._options = options;
node.addEventListener(type, handler, options);
}
public dispose(): void {
if (!this._node || !this._handler) {
return;
}
this._node.removeEventListener(this._type, this._handler, this._options);
this._node = null;
this._handler = null;
}
}
export function addDisposableListener<K extends keyof GlobalEventHandlersEventMap>(node: EventTarget, type: K, handler: (event: GlobalEventHandlersEventMap[K]) => void, useCapture?: boolean): IDisposable;
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable;
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, options: AddEventListenerOptions): IDisposable;
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCaptureOrOptions?: boolean | AddEventListenerOptions): IDisposable {
return new DomListener(node, type, handler, useCaptureOrOptions);
}
export function addStandardDisposableListener(node: HTMLElement, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable {
return addDisposableListener(node, type, handler, useCapture);
}
export const eventType = {
CLICK: 'click',
MOUSE_DOWN: 'mousedown',
MOUSE_OVER: 'mouseover',
MOUSE_LEAVE: 'mouseleave',
KEY_DOWN: 'keydown',
KEY_UP: 'keyup',
INPUT: 'input',
BLUR: 'blur',
FOCUS: 'focus',
CHANGE: 'change',
POINTER_DOWN: 'pointerdown',
POINTER_MOVE: 'pointermove',
POINTER_UP: 'pointerup',
MOUSE_WHEEL: 'wheel',
WHEEL: 'wheel'
} as const;
export function getDomNodePagePosition(domNode: HTMLElement): { left: number, top: number, width: number, height: number } {
const bb = domNode.getBoundingClientRect();
const win = getWindow(domNode);
return {
left: bb.left + win.scrollX,
top: bb.top + win.scrollY,
width: bb.width,
height: bb.height
};
}
class AnimationFrameQueueItem implements IDisposable {
private _canceled = false;
constructor(private readonly _runner: () => void, public priority: number) {
}
public dispose(): void {
this._canceled = true;
}
public execute(): void {
if (this._canceled) {
return;
}
try {
this._runner();
} catch (e) {
console.error(e);
}
}
public static sort(a: AnimationFrameQueueItem, b: AnimationFrameQueueItem): number {
return b.priority - a.priority;
}
}
interface IWindowAnimationFrameState {
next: AnimationFrameQueueItem[];
current: AnimationFrameQueueItem[];
animFrameRequested: boolean;
inAnimationFrameRunner: boolean;
}
const animationFrameState = new Map<Window, IWindowAnimationFrameState>();
function getAnimationFrameState(targetWindow: Window): IWindowAnimationFrameState {
let state = animationFrameState.get(targetWindow);
if (!state) {
state = {
next: [],
current: [],
animFrameRequested: false,
inAnimationFrameRunner: false
};
animationFrameState.set(targetWindow, state);
}
return state;
}
function animationFrameRunner(targetWindow: Window): void {
const state = getAnimationFrameState(targetWindow);
state.animFrameRequested = false;
state.current = state.next;
state.next = [];
state.inAnimationFrameRunner = true;
while (state.current.length > 0) {
state.current.sort(AnimationFrameQueueItem.sort);
const top = state.current.shift()!;
top.execute();
}
state.inAnimationFrameRunner = false;
}
export function scheduleAtNextAnimationFrame(targetWindow: Window, runner: () => void, priority: number = 0): IDisposable {
const state = getAnimationFrameState(targetWindow);
const item = new AnimationFrameQueueItem(runner, priority);
state.next.push(item);
if (!state.animFrameRequested) {
state.animFrameRequested = true;
targetWindow.requestAnimationFrame(() => animationFrameRunner(targetWindow));
}
return item;
}
export class WindowIntervalTimer extends IntervalTimer {
private readonly _defaultTarget?: Window;
constructor(node?: Node) {
super();
this._defaultTarget = node ? getWindow(node) : undefined;
}
public cancelAndSet(runner: () => void, interval: number, targetWindow?: Window): void {
super.cancelAndSet(runner, interval, targetWindow ?? this._defaultTarget ?? window);
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ import { IDisposable } from 'common/Types';
import { IBufferService } from 'common/services/Services';
import { ILinkProviderService, IMouseService, IRenderService } from './services/Services';
import { Emitter } from 'common/Event';
import { addDisposableListener } from 'vs/base/browser/dom';
import { addDisposableListener } from 'browser/Dom';
export class Linkifier extends Disposable implements ILinkifier2 {
public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
+24 -16
View File
@@ -8,12 +8,12 @@ import { ViewportConstants } from 'browser/shared/Constants';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IBufferService, ICoreMouseService, IOptionsService } from 'common/services/Services';
import { CoreMouseEventType } from 'common/Types';
import { addDisposableListener, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom';
import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import type { ScrollableElementChangeOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
import { addDisposableListener, scheduleAtNextAnimationFrame } from 'browser/Dom';
import { SmoothScrollableElement } from 'browser/scrollable/scrollableElement';
import type { IScrollableElementChangeOptions } from 'browser/scrollable/scrollableElementOptions';
import { Emitter, EventUtils } from 'common/Event';
import { Scrollable, ScrollbarVisibility, type ScrollEvent } from 'vs/base/common/scrollable';
import { Gesture, EventType as GestureEventType, type GestureEvent } from 'vs/base/browser/touch';
import { Scrollable, ScrollbarVisibility, type IScrollEvent } from 'browser/scrollable/scrollable';
import { Gesture, EventType as GestureEventType, type IGestureEvent } from 'browser/scrollable/touch';
export class Viewport extends Disposable {
@@ -52,16 +52,17 @@ export class Viewport extends Disposable {
}));
this._scrollableElement = this._register(new SmoothScrollableElement(screenElement, {
vertical: ScrollbarVisibility.Auto,
horizontal: ScrollbarVisibility.Hidden,
vertical: ScrollbarVisibility.AUTO,
horizontal: ScrollbarVisibility.HIDDEN,
useShadows: false,
mouseWheelSmoothScroll: true,
verticalHasArrows: this._optionsService.rawOptions.scrollbar?.showArrows ?? false,
...this._getChangeOptions()
}, scrollable));
this._register(this._optionsService.onMultipleOptionChange([
'scrollSensitivity',
'fastScrollSensitivity',
'overviewRuler'
'scrollbar'
], () => this._scrollableElement.updateOptions(this._getChangeOptions())));
// Don't handle mouse wheel if wheel events are supported by the current mouse prototcol
this._register(coreMouseService.onProtocolChange(type => {
@@ -83,13 +84,13 @@ export class Viewport extends Disposable {
this._register(toDisposable(() => this._styleElement.remove()));
this._register(EventUtils.runAndSubscribe(themeService.onChangeColors, () => {
this._styleElement.textContent = [
`.xterm .xterm-scrollable-element > .scrollbar > .slider {`,
`.xterm .xterm-scrollable-element > .xterm-scrollbar > .xterm-slider {`,
` background: ${themeService.colors.scrollbarSliderBackground.css};`,
`}`,
`.xterm .xterm-scrollable-element > .scrollbar > .slider:hover {`,
`.xterm .xterm-scrollable-element > .xterm-scrollbar > .xterm-slider:hover {`,
` background: ${themeService.colors.scrollbarSliderHoverBackground.css};`,
`}`,
`.xterm .xterm-scrollable-element > .scrollbar > .slider.active {`,
`.xterm .xterm-scrollable-element > .xterm-scrollbar > .xterm-slider.xterm-active {`,
` background: ${themeService.colors.scrollbarSliderActiveBackground.css};`,
`}`
].join('\n');
@@ -108,7 +109,7 @@ export class Viewport extends Disposable {
// Touch/gesture scrolling support
this._register(Gesture.addTarget(screenElement));
this._register(addDisposableListener(screenElement, GestureEventType.Change, (e: GestureEvent) => this._handleGestureChange(e)));
this._register(addDisposableListener(screenElement, GestureEventType.CHANGE, (e: IGestureEvent) => this._handleGestureChange(e)));
}
public scrollLines(disp: number): void {
@@ -129,11 +130,18 @@ export class Viewport extends Disposable {
});
}
private _getChangeOptions(): ScrollableElementChangeOptions {
private _getChangeOptions(): IScrollableElementChangeOptions {
const showScrollbar = this._optionsService.rawOptions.scrollbar?.showScrollbar ?? true;
const showArrows = this._optionsService.rawOptions.scrollbar?.showArrows ?? false;
const verticalScrollbarSize = showScrollbar
? (this._optionsService.rawOptions.scrollbar?.width ?? ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH)
: 0;
return {
mouseWheelScrollSensitivity: this._optionsService.rawOptions.scrollSensitivity,
fastScrollSensitivity: this._optionsService.rawOptions.fastScrollSensitivity,
verticalScrollbarSize: this._optionsService.rawOptions.overviewRuler?.width || ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH
vertical: showScrollbar ? ScrollbarVisibility.AUTO : ScrollbarVisibility.HIDDEN,
verticalScrollbarSize,
verticalHasArrows: showArrows
};
}
@@ -179,7 +187,7 @@ export class Viewport extends Disposable {
this._isSyncing = false;
}
private _handleScroll(e: ScrollEvent): void {
private _handleScroll(e: IScrollEvent): void {
if (!this._renderService) {
return;
}
@@ -196,7 +204,7 @@ export class Viewport extends Disposable {
this._isHandlingScroll = false;
}
private _handleGestureChange(e: GestureEvent): void {
private _handleGestureChange(e: IGestureEvent): void {
e.preventDefault();
e.stopPropagation();
const pos = this._scrollableElement.getScrollPosition();
@@ -38,7 +38,12 @@ export class OverviewRulerRenderer extends Disposable {
private readonly _ctx: CanvasRenderingContext2D;
private readonly _colorZoneStore: IColorZoneStore = new ColorZoneStore();
private get _width(): number {
return this._optionsService.options.overviewRuler?.width || 0;
const scrollbar = this._optionsService.rawOptions.scrollbar;
const showScrollbar = scrollbar?.showScrollbar ?? true;
if (!showScrollbar) {
return 0;
}
return scrollbar?.width ?? 0;
}
private _animationFrame: number | undefined;
@@ -95,7 +100,7 @@ export class OverviewRulerRenderer extends Disposable {
}));
this._register(this._coreBrowserService.onDprChange(() => this._queueRefresh(true)));
this._register(this._optionsService.onSpecificOptionChange('overviewRuler', () => this._queueRefresh(true)));
this._register(this._optionsService.onSpecificOptionChange('scrollbar', () => this._queueRefresh(true)));
this._register(this._themeService.onChangeColors(() => this._queueRefresh()));
this._queueRefresh(true);
}
@@ -176,10 +181,10 @@ export class OverviewRulerRenderer extends Disposable {
private _renderRulerOutline(): void {
this._ctx.fillStyle = this._themeService.colors.overviewRulerBorder.css;
this._ctx.fillRect(0, 0, Constants.OVERVIEW_RULER_BORDER_WIDTH, this._canvas.height);
if (this._optionsService.rawOptions.overviewRuler.showTopBorder) {
if (this._optionsService.rawOptions.scrollbar?.overviewRuler?.showTopBorder) {
this._ctx.fillRect(Constants.OVERVIEW_RULER_BORDER_WIDTH, 0, this._canvas.width - Constants.OVERVIEW_RULER_BORDER_WIDTH, Constants.OVERVIEW_RULER_BORDER_WIDTH);
}
if (this._optionsService.rawOptions.overviewRuler.showBottomBorder) {
if (this._optionsService.rawOptions.scrollbar?.overviewRuler?.showBottomBorder) {
this._ctx.fillRect(Constants.OVERVIEW_RULER_BORDER_WIDTH, this._canvas.height - Constants.OVERVIEW_RULER_BORDER_WIDTH, this._canvas.width - Constants.OVERVIEW_RULER_BORDER_WIDTH, this._canvas.height);
}
}
+1
View File
@@ -83,6 +83,7 @@ export class Terminal extends Disposable implements ITerminalApi {
public get onDimensionsChange(): IEvent<IRenderDimensions> { return this._core.onDimensionsChange; }
public get element(): HTMLElement | undefined { return this._core.element; }
public get screenElement(): HTMLElement | undefined { return this._core.screenElement; }
public get parser(): IParser {
return this._parser ??= new ParserApi(this._core);
}
+53 -4
View File
@@ -8,6 +8,7 @@ import { WidthCache } from 'browser/renderer/dom/WidthCache';
import { INVERTED_DEFAULT_COLOR, RendererConstants } from 'browser/renderer/shared/Constants';
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
import { TextBlinkStateManager } from 'browser/renderer/shared/TextBlinkStateManager';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ISelectionRenderModel } from 'browser/renderer/shared/Types';
import { ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { ILinkifier2, ILinkifierEvent, ITerminal, ReadonlyColorSet } from 'browser/Types';
@@ -15,7 +16,7 @@ import { color } from 'common/Color';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IBufferService, ICoreService, IInstantiationService, IOptionsService } from 'common/services/Services';
import { Emitter } from 'common/Event';
import { addDisposableListener } from 'vs/base/browser/dom';
import { addDisposableListener } from 'browser/Dom';
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
@@ -45,10 +46,14 @@ export class DomRenderer extends Disposable implements IRenderer {
private _widthCache: WidthCache;
private _selectionRenderModel: ISelectionRenderModel = createSelectionRenderModel();
private _cursorBlinkStateManager: CursorBlinkStateManager;
private _textBlinkStateManager: TextBlinkStateManager;
private _rowHasBlinkingCells: boolean[] = [];
private _rowHasBlinkingCellsCount: number = 0;
public dimensions: IRenderDimensions;
public readonly onRequestRedraw = this._register(new Emitter<IRequestRedrawEvent>()).event;
private readonly _onRequestRedraw = this._register(new Emitter<IRequestRedrawEvent>());
public readonly onRequestRedraw = this._onRequestRedraw.event;
constructor(
private readonly _terminal: ITerminal,
@@ -95,6 +100,11 @@ export class DomRenderer extends Disposable implements IRenderer {
this._cursorBlinkStateManager = new CursorBlinkStateManager(this._rowContainer, this._coreBrowserService);
this._register(addDisposableListener(this._document, 'mousedown', () => this._cursorBlinkStateManager.restartBlinkAnimation()));
this._register(toDisposable(() => this._cursorBlinkStateManager.dispose()));
this._textBlinkStateManager = this._register(new TextBlinkStateManager(
() => this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 }),
this._coreBrowserService,
this._optionsService
));
this._register(toDisposable(() => {
this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass);
@@ -193,6 +203,9 @@ export class DomRenderer extends Disposable implements IRenderer {
`}` +
`${this._terminalSelector} span.${RowCss.ITALIC_CLASS} {` +
` font-style: italic;` +
`}` +
`${this._terminalSelector} span.${RowCss.BLINK_HIDDEN_CLASS} {` +
` visibility: hidden;` +
`}`;
// Blink animation
const blinkAnimationUnderlineId = `blink_underline_${this._terminalClass}`;
@@ -318,10 +331,14 @@ export class DomRenderer extends Disposable implements IRenderer {
const row = this._document.createElement('div');
this._rowContainer.appendChild(row);
this._rowElements.push(row);
this._rowHasBlinkingCells.push(false);
}
// Remove excess elements
while (this._rowElements.length > rows) {
this._rowContainer.removeChild(this._rowElements.pop()!);
if (this._rowHasBlinkingCells.pop()) {
this._rowHasBlinkingCellsCount--;
}
}
}
@@ -349,6 +366,10 @@ export class DomRenderer extends Disposable implements IRenderer {
this.renderRows(this._bufferService.buffer.y, this._bufferService.buffer.y);
}
public handleViewportVisibilityChange(isVisible: boolean): void {
this._textBlinkStateManager.setViewportVisible(isVisible);
}
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
// Remove all selections
this._selectionContainer.replaceChildren();
@@ -450,6 +471,11 @@ export class DomRenderer extends Disposable implements IRenderer {
*/
e.replaceChildren();
}
if (this._rowHasBlinkingCellsCount > 0) {
this._rowHasBlinkingCells.fill(false);
this._rowHasBlinkingCellsCount = 0;
this._textBlinkStateManager.setNeedsBlinkInViewport(false);
}
}
public renderRows(start: number, end: number): void {
@@ -459,6 +485,7 @@ export class DomRenderer extends Disposable implements IRenderer {
const cursorBlink = this._coreService.decPrivateModes.cursorBlink ?? this._optionsService.rawOptions.cursorBlink;
const cursorStyle = this._coreService.decPrivateModes.cursorStyle ?? this._optionsService.rawOptions.cursorStyle;
const cursorInactiveStyle = this._optionsService.rawOptions.cursorInactiveStyle;
const rowInfo = { hasBlinkingCells: false };
for (let y = start; y <= end; y++) {
const row = y + buffer.ydisp;
@@ -476,13 +503,17 @@ export class DomRenderer extends Disposable implements IRenderer {
cursorInactiveStyle,
cursorX,
cursorBlink,
this._textBlinkStateManager.isBlinkOn,
this.dimensions.css.cell.width,
this._widthCache,
-1,
-1
-1,
rowInfo
)
);
this._setRowBlinkState(y, rowInfo.hasBlinkingCells);
}
this._updateTextBlinkState();
}
private get _terminalSelector(): string {
@@ -527,6 +558,7 @@ export class DomRenderer extends Disposable implements IRenderer {
const cursorBlink = this._optionsService.rawOptions.cursorBlink;
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
const cursorInactiveStyle = this._optionsService.rawOptions.cursorInactiveStyle;
const rowInfo = { hasBlinkingCells: false };
// refresh rows within link range
for (let i = y; i <= y2; ++i) {
@@ -545,13 +577,30 @@ export class DomRenderer extends Disposable implements IRenderer {
cursorInactiveStyle,
cursorX,
cursorBlink,
this._textBlinkStateManager.isBlinkOn,
this.dimensions.css.cell.width,
this._widthCache,
enabled ? (i === y ? x : 0) : -1,
enabled ? ((i === y2 ? x2 : cols) - 1) : -1
enabled ? ((i === y2 ? x2 : cols) - 1) : -1,
rowInfo
)
);
this._setRowBlinkState(i, rowInfo.hasBlinkingCells);
}
this._updateTextBlinkState();
}
private _setRowBlinkState(row: number, hasBlinkingCells: boolean): void {
const previous = this._rowHasBlinkingCells[row];
if (previous === hasBlinkingCells) {
return;
}
this._rowHasBlinkingCells[row] = hasBlinkingCells;
this._rowHasBlinkingCellsCount += hasBlinkingCells ? 1 : -1;
}
private _updateTextBlinkState(): void {
this._textBlinkStateManager.setNeedsBlinkInViewport(this._rowHasBlinkingCellsCount > 0);
}
}
@@ -40,7 +40,7 @@ describe('DomRendererRowFactory', () => {
describe('createRow', () => {
it('should not create anything for an empty row', () => {
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
''
);
@@ -51,7 +51,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]));
// There should be no element for the following "empty" cell
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '', 0, 0]));
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span>語</span>'
);
@@ -59,7 +59,7 @@ describe('DomRendererRowFactory', () => {
it('should add class for cursor and cursor style', () => {
for (const style of ['block', 'bar', 'underline']) {
const spans = rowFactory.createRow(lineData, 0, true, style, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, true, style, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
`<span class="xterm-cursor xterm-cursor-${style}"> </span>`
);
@@ -67,7 +67,7 @@ describe('DomRendererRowFactory', () => {
});
it('should add class for cursor blink', () => {
const spans = rowFactory.createRow(lineData, 0, true, 'block', undefined, 0, true, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, true, 'block', undefined, 0, true, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
`<span class="xterm-cursor xterm-cursor-blink xterm-cursor-block"> </span>`
);
@@ -86,7 +86,7 @@ describe('DomRendererRowFactory', () => {
new MockThemeService()
);
for (const inactiveStyle of ['outline', 'block', 'bar', 'underline', 'none']){
const spans = rowFactory.createRow(lineData, 0, true, 'block', inactiveStyle, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, true, 'block', inactiveStyle, 0, false, true, 5, widthCache, -1, -1);
if (inactiveStyle === 'none') {
assert.equal(extractHtml(spans),
`<span class="xterm-cursor"> </span>`);
@@ -109,7 +109,7 @@ describe('DomRendererRowFactory', () => {
new MockDecorationService(),
new MockThemeService()
);
const spans = rowFactory.createRow(lineData, 0, true, 'block', undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, true, 'block', undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
`<span> </span>`
);
@@ -120,7 +120,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.BOLD;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-bold">a</span>'
);
@@ -130,7 +130,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.ITALIC;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-italic">a</span>'
);
@@ -140,7 +140,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.DIM;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-dim">a</span>'
);
@@ -153,7 +153,7 @@ describe('DomRendererRowFactory', () => {
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.SINGLE;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-underline-1">a</span>'
);
@@ -164,7 +164,7 @@ describe('DomRendererRowFactory', () => {
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.DOUBLE;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-underline-2">a</span>'
);
@@ -175,7 +175,7 @@ describe('DomRendererRowFactory', () => {
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.CURLY;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-underline-3">a</span>'
);
@@ -186,7 +186,7 @@ describe('DomRendererRowFactory', () => {
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.DOTTED;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-underline-4">a</span>'
);
@@ -197,7 +197,7 @@ describe('DomRendererRowFactory', () => {
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.DASHED;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-underline-5">a</span>'
);
@@ -208,7 +208,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.OVERLINE;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-overline">a</span>'
);
@@ -218,12 +218,28 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.STRIKETHROUGH;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-strikethrough">a</span>'
);
});
it('should hide blinking text when blink is off', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.BLINK | FgFlags.UNDERLINE;
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.SINGLE;
lineData.setCell(0, cell);
const onSpans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(onSpans),
'<span class="xterm-underline-1">a</span>'
);
const offSpans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, false, 5, widthCache, -1, -1);
assert.equal(extractHtml(offSpans),
'<span class="xterm-blink-hidden xterm-underline-1">a</span>'
);
});
it('should add classes for 256 foreground colors', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.fg |= Attributes.CM_P256;
@@ -231,7 +247,7 @@ describe('DomRendererRowFactory', () => {
cell.fg &= ~Attributes.PCOLOR_MASK;
cell.fg |= i;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
`<span class="xterm-fg-${i}">a</span>`
);
@@ -245,7 +261,7 @@ describe('DomRendererRowFactory', () => {
cell.bg &= ~Attributes.PCOLOR_MASK;
cell.bg |= i;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
`<span class="xterm-bg-${i}">a</span>`
);
@@ -257,7 +273,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= Attributes.CM_P16 | 2 | FgFlags.INVERSE;
cell.bg |= Attributes.CM_P16 | 1;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-bg-2 xterm-fg-1">a</span>'
);
@@ -268,7 +284,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= FgFlags.INVERSE;
cell.bg |= Attributes.CM_P16 | 1;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-bg-257 xterm-fg-1">a</span>'
);
@@ -278,7 +294,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.fg |= Attributes.CM_P16 | 1 | FgFlags.INVERSE;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-bg-1 xterm-fg-257">a</span>'
);
@@ -291,7 +307,7 @@ describe('DomRendererRowFactory', () => {
cell.fg &= ~Attributes.PCOLOR_MASK;
cell.fg |= i;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
`<span class="xterm-bold xterm-fg-${i + 8}">a</span>`
);
@@ -303,7 +319,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3;
cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span style="background-color:#040506;color:#010203;">a</span>'
);
@@ -314,7 +330,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3 | FgFlags.INVERSE;
cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span style="background-color:#010203;color:#040506;">a</span>'
);
@@ -326,7 +342,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
rowFactory.handleSelectionChanged([1, 0], [2, 0], false);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span>a</span><span style="background-color:#ff0000;" class="xterm-decoration-top">b</span>'
);
@@ -334,7 +350,7 @@ describe('DomRendererRowFactory', () => {
it('should force whitespace cells to be rendered above the background', () => {
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
rowFactory.handleSelectionChanged([0, 0], [2, 0], false);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span style="background-color:#ff0000;" class="xterm-decoration-top"> a</span>'
);
@@ -351,7 +367,7 @@ describe('DomRendererRowFactory', () => {
});
it('should not create anything for an empty row', () => {
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
''
);
@@ -361,7 +377,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)]));
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span>abc</span>'
);
@@ -372,7 +388,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '€', 1, '€'.charCodeAt(0)]));
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)]));
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span>a</span><span style="letter-spacing: 3px;">€</span><span>c</span>'
);
@@ -387,7 +403,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(1, aColor1);
lineData.setCell(2, bColor2);
lineData.setCell(3, bColor2);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-fg-1">aa</span><span class="xterm-fg-2">bb</span>'
);
@@ -399,7 +415,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'X', 1, 'X'.charCodeAt(0)]));
lineData.setCell(3, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
const spans = rowFactory.createRow(lineData, 0, true, undefined, undefined, 2, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, true, undefined, undefined, 2, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span>aa</span><span class="xterm-cursor xterm-cursor-block">X</span><span>bb</span>'
);
@@ -412,7 +428,7 @@ describe('DomRendererRowFactory', () => {
nullCell.bg = Attributes.CM_P16 | 2;
lineData.setCell(3, nullCell);
lineData.setCell(4, nullCell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span> </span><span class="xterm-bg-1"> </span><span class="xterm-bg-2"> </span>'
);
@@ -422,23 +438,23 @@ describe('DomRendererRowFactory', () => {
const nullCell = lineData.loadCell(0, new CellData());
nullCell.bg = Attributes.CM_P16 | 1;
lineData.setCell(0, nullCell);
let spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
let spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-bg-1"> </span>'
);
lineData.setCell(1, nullCell);
spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-bg-1"> </span>'
);
lineData.setCell(2, nullCell);
lineData.setCell(3, nullCell);
spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-bg-1"> </span>'
);
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-bg-1"> </span><span>a</span>'
);
@@ -451,7 +467,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)]));
lineData.setCell(3, CellData.fromCharData([DEFAULT_ATTR, '語', 2, 'c'.charCodeAt(0)]));
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, '𝄞', 1, 'c'.charCodeAt(0)]));
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span>a</span><span style="letter-spacing: 3px;">€</span><span>c語</span><span style="letter-spacing: -2px;">𝄞</span>'
);
@@ -465,7 +481,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)]));
lineData.setCell(5, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
lineData.setCell(6, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, 2, 4);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, 2, 4);
assert.equal(extractHtml(spans),
'<span>aa</span><span style="text-decoration: underline;">xxx</span><span>bb</span>'
);
@@ -476,7 +492,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)]));
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)]));
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, 2, 4);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, 2, 4);
assert.equal(extractHtml(spans),
'<span>aa</span><span style="text-decoration: underline;">x x</span>'
);
@@ -486,7 +502,7 @@ describe('DomRendererRowFactory', () => {
for (let i = 0; i < 10; ++i) {
lineData.setCell(i, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
}
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -100, 100);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -100, 100);
assert.equal(extractHtml(spans),
'<span style="text-decoration: underline;">aaaaaaaaaa</span>'
);
@@ -24,6 +24,7 @@ export const enum RowCss {
UNDERLINE_CLASS = 'xterm-underline',
OVERLINE_CLASS = 'xterm-overline',
STRIKETHROUGH_CLASS = 'xterm-strikethrough',
BLINK_HIDDEN_CLASS = 'xterm-blink-hidden',
CURSOR_CLASS = 'xterm-cursor',
CURSOR_BLINK_CLASS = 'xterm-cursor-blink',
CURSOR_STYLE_BLOCK_CLASS = 'xterm-cursor-block',
@@ -66,13 +67,18 @@ export class DomRendererRowFactory {
cursorInactiveStyle: string | undefined,
cursorX: number,
cursorBlink: boolean,
blinkOn: boolean,
cellWidth: number,
widthCache: WidthCache,
linkStart: number,
linkEnd: number
linkEnd: number,
rowInfo?: { hasBlinkingCells: boolean }
): HTMLSpanElement[] {
const elements: HTMLSpanElement[] = [];
if (rowInfo) {
rowInfo.hasBlinkingCells = false;
}
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
const colors = this._themeService.colors;
@@ -118,7 +124,7 @@ export class DomRendererRowFactory {
// Process any joined character ranges as needed. Because of how the
// ranges are produced, we know that they are valid for the characters
// and attributes of our input.
let cell = this._workCell;
let cell: ICellData = this._workCell;
if (joinedRanges.length > 0 && x === joinedRanges[0][0] && isValidJoinRange) {
const range = joinedRanges.shift()!;
// If the ligature's selection state is not consistent, don't join it. This helps the
@@ -153,6 +159,13 @@ export class DomRendererRowFactory {
const isInSelection = this._isCellInSelection(x, row);
const isCursorCell = isCursorRow && x === cursorX;
const isLinkHover = hasHover && x >= linkStart && x <= linkEnd;
if (rowInfo && cell.isBlink()) {
rowInfo.hasBlinkingCells = true;
}
const isBlinkHidden = !blinkOn && cell.isBlink();
if (isBlinkHidden) {
classes.push(RowCss.BLINK_HIDDEN_CLASS);
}
let isDecorated = false;
this._decorationService.forEachDecorationAtCell(x, row, undefined, d => {
@@ -0,0 +1,98 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { TextBlinkStateManager } from 'browser/renderer/shared/TextBlinkStateManager';
import { MockOptionsService } from 'common/TestUtils.test';
import type { ICoreBrowserService } from 'browser/services/Services';
import { Emitter } from 'common/Event';
class FakeWindow {
public nextId = 1;
public intervals = new Map<number, () => void>();
public setInterval(callback: () => void, _duration: number): number {
const id = this.nextId++;
this.intervals.set(id, callback);
return id;
}
public clearInterval(id: number): void {
this.intervals.delete(id);
}
}
function createManager(duration: number): {
manager: TextBlinkStateManager;
window: FakeWindow;
getRenderCount: () => number;
} {
const fakeWindow = new FakeWindow();
let renderCount = 0;
const coreBrowserService: ICoreBrowserService = {
serviceBrand: undefined,
isFocused: true,
dpr: 1,
onDprChange: new Emitter<number>().event,
onWindowChange: new Emitter<Window & typeof globalThis>().event,
window: fakeWindow as any,
mainDocument: {} as any
};
const optionsService = new MockOptionsService({ blinkIntervalDuration: duration });
const manager = new TextBlinkStateManager(() => {
renderCount++;
}, coreBrowserService, optionsService);
return {
manager,
window: fakeWindow,
getRenderCount: () => renderCount
};
}
function getOnlyIntervalCallback(window: FakeWindow): () => void {
const iterator = window.intervals.values();
const first = iterator.next();
assert.ok(!first.done);
assert.ok(iterator.next().done);
return first.value;
}
describe('TextBlinkStateManager', () => {
it('starts interval only when needed', () => {
const { manager, window } = createManager(100);
assert.equal(window.intervals.size, 0);
manager.setNeedsBlinkInViewport(true);
assert.equal(window.intervals.size, 1);
});
it('stops interval and restores blink visibility when no longer needed', () => {
const { manager, window, getRenderCount } = createManager(100);
manager.setNeedsBlinkInViewport(true);
const tick = getOnlyIntervalCallback(window);
tick();
const rendersAfterTick = getRenderCount();
assert.equal(manager.isBlinkOn, false);
manager.setNeedsBlinkInViewport(false);
assert.equal(window.intervals.size, 0);
assert.equal(manager.isBlinkOn, true);
assert.equal(getRenderCount(), rendersAfterTick + 1);
});
it('pauses while viewport is hidden and resumes when visible', () => {
const { manager, window } = createManager(100);
manager.setNeedsBlinkInViewport(true);
assert.equal(window.intervals.size, 1);
manager.setViewportVisible(false);
assert.equal(window.intervals.size, 0);
manager.setViewportVisible(true);
assert.equal(window.intervals.size, 1);
});
it('does not start interval when duration is zero', () => {
const { manager, window } = createManager(0);
manager.setNeedsBlinkInViewport(true);
assert.equal(window.intervals.size, 0);
});
});
@@ -0,0 +1,97 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ICoreBrowserService } from 'browser/services/Services';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IOptionsService } from 'common/services/Services';
export class TextBlinkStateManager extends Disposable {
private _intervalDuration: number = 0;
private _interval: number | undefined;
private _blinkOn: boolean = true;
private _needsBlinkInViewport: boolean = false;
private _isViewportVisible: boolean = true;
constructor(
private readonly _renderCallback: () => void,
private readonly _coreBrowserService: ICoreBrowserService,
private readonly _optionsService: IOptionsService
) {
super();
this._register(this._optionsService.onSpecificOptionChange('blinkIntervalDuration', duration => {
this.setIntervalDuration(duration);
}));
this.setIntervalDuration(this._optionsService.rawOptions.blinkIntervalDuration);
this._register(toDisposable(() => this._clearInterval()));
}
public get isBlinkOn(): boolean {
return this._blinkOn;
}
public get isEnabled(): boolean {
return this._intervalDuration > 0;
}
public setNeedsBlinkInViewport(needsBlinkInViewport: boolean): void {
if (this._needsBlinkInViewport === needsBlinkInViewport) {
return;
}
this._needsBlinkInViewport = needsBlinkInViewport;
this._updateIntervalState();
}
public setViewportVisible(isVisible: boolean): void {
if (this._isViewportVisible === isVisible) {
return;
}
this._isViewportVisible = isVisible;
this._updateIntervalState();
}
public setIntervalDuration(duration: number): void {
if (duration === this._intervalDuration) {
return;
}
this._intervalDuration = duration;
this._clearInterval();
this._updateIntervalState();
}
private _updateIntervalState(): void {
const shouldBlink = this._intervalDuration > 0 && this._needsBlinkInViewport && this._isViewportVisible;
if (shouldBlink) {
if (this._interval !== undefined) {
return;
}
const wasBlinkOn = this._blinkOn;
this._blinkOn = true;
this._interval = this._coreBrowserService.window.setInterval(() => {
this._blinkOn = !this._blinkOn;
this._renderCallback();
}, this._intervalDuration);
if (!wasBlinkOn) {
this._renderCallback();
}
return;
}
this._clearInterval();
if (!this._blinkOn) {
this._blinkOn = true;
this._renderCallback();
}
}
private _clearInterval(): void {
if (this._interval !== undefined) {
this._coreBrowserService.window.clearInterval(this._interval);
this._interval = undefined;
}
}
}
+1
View File
@@ -65,6 +65,7 @@ export interface IRenderer extends IDisposable {
handleCharSizeChanged(): void;
handleBlur(): void;
handleFocus(): void;
handleViewportVisibilityChange?(isVisible: boolean): void;
handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
handleCursorMove(): void;
clear(): void;
+300
View File
@@ -0,0 +1,300 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from '../Dom';
import { FastDomNode } from './fastDomNode';
import { GlobalPointerMoveMonitor } from './globalPointerMoveMonitor';
import { StandardWheelEvent } from './mouseEvent';
import { ScrollbarArrow, IScrollbarArrowOptions } from './scrollbarArrow';
import { ScrollbarState } from './scrollbarState';
import { ScrollbarVisibilityController } from './scrollbarVisibilityController';
import { Widget } from './widget';
import * as platform from 'common/Platform';
import { INewScrollPosition, Scrollable, ScrollbarVisibility } from './scrollable';
/**
* The orthogonal distance to the slider at which dragging "resets". This implements "snapping"
*/
const POINTER_DRAG_RESET_DISTANCE = 140;
export interface ISimplifiedPointerEvent {
buttons: number;
pageX: number;
pageY: number;
}
export interface IScrollbarHost {
handleMouseWheel(mouseWheelEvent: StandardWheelEvent): void;
handleDragStart(): void;
handleDragEnd(): void;
}
interface IAbstractScrollbarOptions {
lazyRender: boolean;
host: IScrollbarHost;
scrollbarState: ScrollbarState;
visibility: ScrollbarVisibility;
extraScrollbarClassName: string;
scrollable: Scrollable;
scrollByPage: boolean;
}
export abstract class AbstractScrollbar extends Widget {
protected _host: IScrollbarHost;
protected _scrollable: Scrollable;
protected _scrollByPage: boolean;
private _lazyRender: boolean;
protected _scrollbarState: ScrollbarState;
protected _visibilityController: ScrollbarVisibilityController;
private _pointerMoveMonitor: GlobalPointerMoveMonitor;
public domNode: FastDomNode<HTMLElement>;
public slider!: FastDomNode<HTMLElement>;
protected _shouldRender: boolean;
constructor(opts: IAbstractScrollbarOptions) {
super();
this._lazyRender = opts.lazyRender;
this._host = opts.host;
this._scrollable = opts.scrollable;
this._scrollByPage = opts.scrollByPage;
this._scrollbarState = opts.scrollbarState;
this._visibilityController = this._register(new ScrollbarVisibilityController(opts.visibility, 'xterm-visible xterm-scrollbar ' + opts.extraScrollbarClassName, 'xterm-invisible xterm-scrollbar ' + opts.extraScrollbarClassName));
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
this._pointerMoveMonitor = this._register(new GlobalPointerMoveMonitor());
this._shouldRender = true;
this.domNode = new FastDomNode(document.createElement('div'));
this.domNode.setAttribute('role', 'presentation');
this.domNode.setAttribute('aria-hidden', 'true');
this._visibilityController.setDomNode(this.domNode);
this.domNode.setPosition('absolute');
this._register(dom.addDisposableListener(this.domNode.domNode, dom.eventType.POINTER_DOWN, (e: PointerEvent) => this._domNodePointerDown(e)));
}
// ----------------- creation
/**
* Creates the dom node for an arrow & adds it to the container
*/
protected _createArrow(opts: IScrollbarArrowOptions): ScrollbarArrow {
const arrow = this._register(new ScrollbarArrow(opts));
this.domNode.domNode.appendChild(arrow.bgDomNode);
this.domNode.domNode.appendChild(arrow.domNode);
return arrow;
}
/**
* Creates the slider dom node, adds it to the container & hooks up the events
*/
protected _createSlider(top: number, left: number, width: number | undefined, height: number | undefined): void {
this.slider = new FastDomNode(document.createElement('div'));
this.slider.setClassName('xterm-slider');
this.slider.setPosition('absolute');
this.slider.setTop(top);
this.slider.setLeft(left);
if (typeof width === 'number') {
this.slider.setWidth(width);
}
if (typeof height === 'number') {
this.slider.setHeight(height);
}
this.slider.setLayerHinting(true);
this.slider.setContain('strict');
this.domNode.domNode.appendChild(this.slider.domNode);
this._register(dom.addDisposableListener(
this.slider.domNode,
dom.eventType.POINTER_DOWN,
(e: PointerEvent) => {
if (e.button === 0) {
e.preventDefault();
this._sliderPointerDown(e);
}
}
));
this._onclick(this.slider.domNode, e => {
if (e.leftButton) {
e.stopPropagation();
}
});
}
// ----------------- Update state
protected _handleElementSize(visibleSize: number): boolean {
if (this._scrollbarState.setVisibleSize(visibleSize)) {
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
this._shouldRender = true;
if (!this._lazyRender) {
this.render();
}
}
return this._shouldRender;
}
protected _handleElementScrollSize(elementScrollSize: number): boolean {
if (this._scrollbarState.setScrollSize(elementScrollSize)) {
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
this._shouldRender = true;
if (!this._lazyRender) {
this.render();
}
}
return this._shouldRender;
}
protected _handleElementScrollPosition(elementScrollPosition: number): boolean {
if (this._scrollbarState.setScrollPosition(elementScrollPosition)) {
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
this._shouldRender = true;
if (!this._lazyRender) {
this.render();
}
}
return this._shouldRender;
}
// ----------------- rendering
public beginReveal(): void {
this._visibilityController.setShouldBeVisible(true);
}
public beginHide(): void {
this._visibilityController.setShouldBeVisible(false);
}
public render(): void {
if (!this._shouldRender) {
return;
}
this._shouldRender = false;
this._renderDomNode(this._scrollbarState.getRectangleLargeSize(), this._scrollbarState.getRectangleSmallSize());
this._updateSlider(this._scrollbarState.getSliderSize(), this._scrollbarState.getArrowSize() + this._scrollbarState.getSliderPosition());
}
// ----------------- DOM events
private _domNodePointerDown(e: PointerEvent): void {
if (e.target !== this.domNode.domNode) {
return;
}
this._handlePointerDown(e);
}
public delegatePointerDown(e: PointerEvent): void {
const domTop = this.domNode.domNode.getClientRects()[0].top;
const sliderStart = domTop + this._scrollbarState.getSliderPosition();
const sliderStop = domTop + this._scrollbarState.getSliderPosition() + this._scrollbarState.getSliderSize();
const pointerPos = this._sliderPointerPosition(e);
if (sliderStart <= pointerPos && pointerPos <= sliderStop) {
if (e.button === 0) {
e.preventDefault();
this._sliderPointerDown(e);
}
} else {
this._handlePointerDown(e);
}
}
private _handlePointerDown(e: PointerEvent): void {
let offsetX: number;
let offsetY: number;
if (e.target === this.domNode.domNode && typeof e.offsetX === 'number' && typeof e.offsetY === 'number') {
offsetX = e.offsetX;
offsetY = e.offsetY;
} else {
const domNodePosition = dom.getDomNodePagePosition(this.domNode.domNode);
offsetX = e.pageX - domNodePosition.left;
offsetY = e.pageY - domNodePosition.top;
}
const offset = this._pointerDownRelativePosition(offsetX, offsetY);
this._setDesiredScrollPositionNow(
this._scrollByPage
? this._scrollbarState.getDesiredScrollPositionFromOffsetPaged(offset)
: this._scrollbarState.getDesiredScrollPositionFromOffset(offset)
);
if (e.button === 0) {
e.preventDefault();
this._sliderPointerDown(e);
}
}
private _sliderPointerDown(e: PointerEvent): void {
if (!e.target || !(e.target instanceof Element)) {
return;
}
const initialPointerPosition = this._sliderPointerPosition(e);
const initialPointerOrthogonalPosition = this._sliderOrthogonalPointerPosition(e);
const initialScrollbarState = this._scrollbarState.clone();
this.slider.toggleClassName('xterm-active', true);
this._pointerMoveMonitor.startMonitoring(
e.target,
e.pointerId,
e.buttons,
(pointerMoveData: PointerEvent) => {
const pointerOrthogonalPosition = this._sliderOrthogonalPointerPosition(pointerMoveData);
const pointerOrthogonalDelta = Math.abs(pointerOrthogonalPosition - initialPointerOrthogonalPosition);
if (platform.isWindows && pointerOrthogonalDelta > POINTER_DRAG_RESET_DISTANCE) {
this._setDesiredScrollPositionNow(initialScrollbarState.getScrollPosition());
return;
}
const pointerPosition = this._sliderPointerPosition(pointerMoveData);
const pointerDelta = pointerPosition - initialPointerPosition;
this._setDesiredScrollPositionNow(initialScrollbarState.getDesiredScrollPositionFromDelta(pointerDelta));
},
() => {
this.slider.toggleClassName('xterm-active', false);
this._host.handleDragEnd();
}
);
this._host.handleDragStart();
}
private _setDesiredScrollPositionNow(_desiredScrollPosition: number): void {
const desiredScrollPosition: INewScrollPosition = {};
this.writeScrollPosition(desiredScrollPosition, _desiredScrollPosition);
this._scrollable.setScrollPositionNow(desiredScrollPosition);
}
public updateScrollbarSize(scrollbarSize: number): void {
this._updateScrollbarSize(scrollbarSize);
this._scrollbarState.setScrollbarSize(scrollbarSize);
this._shouldRender = true;
if (!this._lazyRender) {
this.render();
}
}
public isNeeded(): boolean {
return this._scrollbarState.isNeeded();
}
// ----------------- Overwrite these
protected abstract _renderDomNode(largeSize: number, smallSize: number): void;
protected abstract _updateSlider(sliderSize: number, sliderPosition: number): void;
protected abstract _pointerDownRelativePosition(offsetX: number, offsetY: number): number;
protected abstract _sliderPointerPosition(e: ISimplifiedPointerEvent): number;
protected abstract _sliderOrthogonalPointerPosition(e: ISimplifiedPointerEvent): number;
protected abstract _updateScrollbarSize(size: number): void;
public abstract writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void;
}
+126
View File
@@ -0,0 +1,126 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export class FastDomNode<T extends HTMLElement> {
private _width: string = '';
private _height: string = '';
private _top: string = '';
private _left: string = '';
private _bottom: string = '';
private _right: string = '';
private _className: string = '';
private _position: string = '';
private _layerHint: boolean = false;
private _contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint' = 'none';
constructor(
public readonly domNode: T
) { }
public setWidth(_width: number | string): void {
const width = numberAsPixels(_width);
if (this._width === width) {
return;
}
this._width = width;
this.domNode.style.width = this._width;
}
public setHeight(_height: number | string): void {
const height = numberAsPixels(_height);
if (this._height === height) {
return;
}
this._height = height;
this.domNode.style.height = this._height;
}
public setTop(_top: number | string): void {
const top = numberAsPixels(_top);
if (this._top === top) {
return;
}
this._top = top;
this.domNode.style.top = this._top;
}
public setLeft(_left: number | string): void {
const left = numberAsPixels(_left);
if (this._left === left) {
return;
}
this._left = left;
this.domNode.style.left = this._left;
}
public setBottom(_bottom: number | string): void {
const bottom = numberAsPixels(_bottom);
if (this._bottom === bottom) {
return;
}
this._bottom = bottom;
this.domNode.style.bottom = this._bottom;
}
public setRight(_right: number | string): void {
const right = numberAsPixels(_right);
if (this._right === right) {
return;
}
this._right = right;
this.domNode.style.right = this._right;
}
public setClassName(className: string): void {
if (this._className === className) {
return;
}
this._className = className;
this.domNode.className = this._className;
}
public toggleClassName(className: string, shouldHaveIt?: boolean): void {
this.domNode.classList.toggle(className, shouldHaveIt);
this._className = this.domNode.className;
}
public setPosition(position: string): void {
if (this._position === position) {
return;
}
this._position = position;
this.domNode.style.position = this._position;
}
public setLayerHinting(layerHint: boolean): void {
if (this._layerHint === layerHint) {
return;
}
this._layerHint = layerHint;
if (layerHint) {
this.domNode.style.transform = 'translate3d(0px, 0px, 0px)';
} else {
this.domNode.style.transform = '';
}
}
public setContain(contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint'): void {
if (this._contain === contain) {
return;
}
this._contain = contain;
this.domNode.style.contain = this._contain;
}
public setAttribute(name: string, value: string): void {
this.domNode.setAttribute(name, value);
}
}
function numberAsPixels(value: number | string): string {
return (typeof value === 'number' ? `${value}px` : value);
}
@@ -0,0 +1,90 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from '../Dom';
import { DisposableStore, IDisposable, toDisposable } from 'common/Lifecycle';
type PointerMoveCallback = (event: PointerEvent) => void;
type OnStopCallback = () => void;
export class GlobalPointerMoveMonitor implements IDisposable {
private readonly _hooks = new DisposableStore();
private _pointerMoveCallback: PointerMoveCallback | null = null;
private _onStopCallback: OnStopCallback | null = null;
public dispose(): void {
this.stopMonitoring(false);
this._hooks.dispose();
}
public stopMonitoring(invokeStopCallback: boolean): void {
if (!this.isMonitoring()) {
return;
}
this._hooks.clear();
this._pointerMoveCallback = null;
const onStopCallback = this._onStopCallback;
this._onStopCallback = null;
if (invokeStopCallback && onStopCallback) {
onStopCallback();
}
}
public isMonitoring(): boolean {
return !!this._pointerMoveCallback;
}
public startMonitoring(
initialElement: Element,
pointerId: number,
initialButtons: number,
pointerMoveCallback: PointerMoveCallback,
onStopCallback: OnStopCallback
): void {
if (this.isMonitoring()) {
this.stopMonitoring(false);
}
this._pointerMoveCallback = pointerMoveCallback;
this._onStopCallback = onStopCallback;
let eventSource: Element | Window = initialElement;
try {
initialElement.setPointerCapture(pointerId);
this._hooks.add(toDisposable(() => {
try {
initialElement.releasePointerCapture(pointerId);
} catch {
// ignore
}
}));
} catch {
eventSource = dom.getWindow(initialElement);
}
this._hooks.add(dom.addDisposableListener(
eventSource,
dom.eventType.POINTER_MOVE,
(e) => {
if (e.buttons !== initialButtons) {
this.stopMonitoring(true);
return;
}
e.preventDefault();
this._pointerMoveCallback!(e);
}
));
this._hooks.add(dom.addDisposableListener(
eventSource,
dom.eventType.POINTER_UP,
(e: PointerEvent) => this.stopMonitoring(true)
));
}
}
@@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AbstractScrollbar, ISimplifiedPointerEvent, IScrollbarHost } from './abstractScrollbar';
import { IScrollableElementResolvedOptions } from './scrollableElementOptions';
import { ScrollbarState } from './scrollbarState';
import { INewScrollPosition, Scrollable, ScrollbarVisibility, IScrollEvent } from './scrollable';
export class HorizontalScrollbar extends AbstractScrollbar {
constructor(scrollable: Scrollable, options: IScrollableElementResolvedOptions, host: IScrollbarHost) {
const scrollDimensions = scrollable.getScrollDimensions();
const scrollPosition = scrollable.getCurrentScrollPosition();
super({
lazyRender: options.lazyRender,
host: host,
scrollbarState: new ScrollbarState(
(options.horizontalHasArrows ? options.horizontalScrollbarSize : 0),
(options.horizontal === ScrollbarVisibility.HIDDEN ? 0 : options.horizontalScrollbarSize),
(options.vertical === ScrollbarVisibility.HIDDEN ? 0 : options.verticalScrollbarSize),
scrollDimensions.width,
scrollDimensions.scrollWidth,
scrollPosition.scrollLeft
),
visibility: options.horizontal,
extraScrollbarClassName: 'xterm-horizontal',
scrollable: scrollable,
scrollByPage: options.scrollByPage
});
if (options.horizontalHasArrows) {
throw new Error('horizontalHasArrows is not supported in xterm.js');
}
this._createSlider(Math.floor((options.horizontalScrollbarSize - options.horizontalSliderSize) / 2), 0, undefined, options.horizontalSliderSize);
}
protected _updateSlider(sliderSize: number, sliderPosition: number): void {
this.slider.setWidth(sliderSize);
this.slider.setLeft(sliderPosition);
}
protected _renderDomNode(largeSize: number, smallSize: number): void {
this.domNode.setWidth(largeSize);
this.domNode.setHeight(smallSize);
this.domNode.setLeft(0);
this.domNode.setBottom(0);
}
public handleScroll(e: IScrollEvent): boolean {
this._shouldRender = this._handleElementScrollSize(e.scrollWidth) || this._shouldRender;
this._shouldRender = this._handleElementScrollPosition(e.scrollLeft) || this._shouldRender;
this._shouldRender = this._handleElementSize(e.width) || this._shouldRender;
return this._shouldRender;
}
protected _pointerDownRelativePosition(offsetX: number, offsetY: number): number {
return offsetX;
}
protected _sliderPointerPosition(e: ISimplifiedPointerEvent): number {
return e.pageX;
}
protected _sliderOrthogonalPointerPosition(e: ISimplifiedPointerEvent): number {
return e.pageY;
}
protected _updateScrollbarSize(size: number): void {
this.slider.setHeight(size);
}
public writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void {
target.scrollLeft = scrollPosition;
}
public updateOptions(options: IScrollableElementResolvedOptions): void {
this.updateScrollbarSize(options.horizontal === ScrollbarVisibility.HIDDEN ? 0 : options.horizontalScrollbarSize);
this._scrollbarState.setOppositeScrollbarSize(options.vertical === ScrollbarVisibility.HIDDEN ? 0 : options.verticalScrollbarSize);
this._visibilityController.setVisibility(options.horizontal);
this._scrollByPage = options.scrollByPage;
}
}
+292
View File
@@ -0,0 +1,292 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as platform from 'common/Platform';
interface IWindowChainElement {
readonly window: WeakRef<Window>;
readonly iframeElement: Element | null;
}
const sameOriginWindowChainCache = new WeakMap<Window, IWindowChainElement[] | null>();
function getParentWindowIfSameOrigin(w: Window): Window | null {
if (!w.parent || w.parent === w) {
return null;
}
try {
const location = w.location;
const parentLocation = w.parent.location;
if (location.origin !== 'null' && parentLocation.origin !== 'null' && location.origin !== parentLocation.origin) {
return null;
}
} catch {
return null;
}
return w.parent;
}
class IframeUtils {
private static _getSameOriginWindowChain(targetWindow: Window): IWindowChainElement[] {
let windowChainCache = sameOriginWindowChainCache.get(targetWindow);
if (!windowChainCache) {
windowChainCache = [];
sameOriginWindowChainCache.set(targetWindow, windowChainCache);
let w: Window | null = targetWindow;
let parent: Window | null;
do {
parent = getParentWindowIfSameOrigin(w);
if (parent) {
windowChainCache.push({
window: new WeakRef(w),
iframeElement: w.frameElement ?? null
});
} else {
windowChainCache.push({
window: new WeakRef(w),
iframeElement: null
});
}
w = parent;
} while (w);
}
return windowChainCache.slice(0);
}
public static getPositionOfChildWindowRelativeToAncestorWindow(childWindow: Window, ancestorWindow: Window | null): { top: number, left: number } {
if (!ancestorWindow || childWindow === ancestorWindow) {
return {
top: 0,
left: 0
};
}
let top = 0;
let left = 0;
const windowChain = this._getSameOriginWindowChain(childWindow);
for (const windowChainEl of windowChain) {
const windowInChain = windowChainEl.window.deref();
top += windowInChain?.scrollY ?? 0;
left += windowInChain?.scrollX ?? 0;
if (windowInChain === ancestorWindow) {
break;
}
if (!windowChainEl.iframeElement) {
break;
}
const boundingRect = windowChainEl.iframeElement.getBoundingClientRect();
top += boundingRect.top;
left += boundingRect.left;
}
return {
top: top,
left: left
};
}
}
export interface IMouseEvent {
readonly browserEvent: MouseEvent;
readonly leftButton: boolean;
readonly middleButton: boolean;
readonly rightButton: boolean;
readonly buttons: number;
readonly target: HTMLElement;
readonly detail: number;
readonly posx: number;
readonly posy: number;
readonly ctrlKey: boolean;
readonly shiftKey: boolean;
readonly altKey: boolean;
readonly metaKey: boolean;
readonly timestamp: number;
preventDefault(): void;
stopPropagation(): void;
}
export class StandardMouseEvent implements IMouseEvent {
public readonly browserEvent: MouseEvent;
public readonly leftButton: boolean;
public readonly middleButton: boolean;
public readonly rightButton: boolean;
public readonly buttons: number;
public readonly target: HTMLElement;
public detail: number;
public readonly posx: number;
public readonly posy: number;
public readonly ctrlKey: boolean;
public readonly shiftKey: boolean;
public readonly altKey: boolean;
public readonly metaKey: boolean;
public readonly timestamp: number;
constructor(targetWindow: Window, e: MouseEvent) {
this.timestamp = Date.now();
this.browserEvent = e;
this.leftButton = e.button === 0;
this.middleButton = e.button === 1;
this.rightButton = e.button === 2;
this.buttons = e.buttons;
this.target = e.target as HTMLElement;
this.detail = e.detail ?? 1;
if (e.type === 'dblclick') {
this.detail = 2;
}
this.ctrlKey = e.ctrlKey;
this.shiftKey = e.shiftKey;
this.altKey = e.altKey;
this.metaKey = e.metaKey;
if (typeof e.pageX === 'number') {
this.posx = e.pageX;
this.posy = e.pageY;
} else {
this.posx = e.clientX + this.target.ownerDocument.body.scrollLeft + this.target.ownerDocument.documentElement.scrollLeft;
this.posy = e.clientY + this.target.ownerDocument.body.scrollTop + this.target.ownerDocument.documentElement.scrollTop;
}
const iframeOffsets = IframeUtils.getPositionOfChildWindowRelativeToAncestorWindow(targetWindow, e.view);
this.posx -= iframeOffsets.left;
this.posy -= iframeOffsets.top;
}
public preventDefault(): void {
this.browserEvent.preventDefault();
}
public stopPropagation(): void {
this.browserEvent.stopPropagation();
}
}
export interface IMouseWheelEvent extends MouseEvent {
readonly wheelDelta: number;
readonly wheelDeltaX: number;
readonly wheelDeltaY: number;
readonly deltaX: number;
readonly deltaY: number;
readonly deltaZ: number;
readonly deltaMode: number;
}
interface IWebKitMouseWheelEvent {
wheelDeltaY: number;
wheelDeltaX: number;
}
interface IGeckoMouseWheelEvent {
HORIZONTAL_AXIS: number;
VERTICAL_AXIS: number;
axis: number;
detail: number;
}
export class StandardWheelEvent {
public readonly browserEvent: IMouseWheelEvent | null;
public readonly deltaY: number;
public readonly deltaX: number;
public readonly target: Node | null;
constructor(e: IMouseWheelEvent | null, deltaX: number = 0, deltaY: number = 0) {
this.browserEvent = e ?? null;
this.target = e ? (e.target ?? (e as any).targetNode ?? e.srcElement ?? null) : null;
this.deltaY = deltaY;
this.deltaX = deltaX;
let shouldFactorDPR: boolean = false;
if (platform.isChrome) {
const chromeVersionMatch = navigator.userAgent.match(/Chrome\/(\d+)/);
const chromeMajorVersion = chromeVersionMatch ? parseInt(chromeVersionMatch[1]) : 123;
shouldFactorDPR = chromeMajorVersion <= 122;
}
if (e) {
const e1 = e as IWebKitMouseWheelEvent as any;
const e2 = e as unknown as IGeckoMouseWheelEvent;
const devicePixelRatio = e.view?.devicePixelRatio ?? 1;
if (typeof e1.wheelDeltaY !== 'undefined') {
if (shouldFactorDPR) {
this.deltaY = e1.wheelDeltaY / (120 * devicePixelRatio);
} else {
this.deltaY = e1.wheelDeltaY / 120;
}
} else if (typeof e2.VERTICAL_AXIS !== 'undefined' && e2.axis === e2.VERTICAL_AXIS) {
this.deltaY = -e2.detail / 3;
} else if (e.type === 'wheel') {
const ev = e as unknown as WheelEvent;
if (ev.deltaMode === ev.DOM_DELTA_LINE) {
if (platform.isFirefox && !platform.isMac) {
this.deltaY = -e.deltaY / 3;
} else {
this.deltaY = -e.deltaY;
}
} else {
this.deltaY = -e.deltaY / 40;
}
}
if (typeof e1.wheelDeltaX !== 'undefined') {
if (platform.isSafari && platform.isWindows) {
this.deltaX = -(e1.wheelDeltaX / 120);
} else if (shouldFactorDPR) {
this.deltaX = e1.wheelDeltaX / (120 * devicePixelRatio);
} else {
this.deltaX = e1.wheelDeltaX / 120;
}
} else if (typeof e2.HORIZONTAL_AXIS !== 'undefined' && e2.axis === e2.HORIZONTAL_AXIS) {
this.deltaX = -e.detail / 3;
} else if (e.type === 'wheel') {
const ev = e as unknown as WheelEvent;
if (ev.deltaMode === ev.DOM_DELTA_LINE) {
if (platform.isFirefox && !platform.isMac) {
this.deltaX = -e.deltaX / 3;
} else {
this.deltaX = -e.deltaX;
}
} else {
this.deltaX = -e.deltaX / 40;
}
}
if (this.deltaY === 0 && this.deltaX === 0 && e.wheelDelta) {
if (shouldFactorDPR) {
this.deltaY = e.wheelDelta / (120 * devicePixelRatio);
} else {
this.deltaY = e.wheelDelta / 120;
}
}
}
}
public preventDefault(): void {
this.browserEvent?.preventDefault();
}
public stopPropagation(): void {
this.browserEvent?.stopPropagation();
}
}
+486
View File
@@ -0,0 +1,486 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, IEvent } from 'common/Event';
import { Disposable, IDisposable } from 'common/Lifecycle';
export const enum ScrollbarVisibility {
AUTO = 1,
HIDDEN = 2,
VISIBLE = 3
}
export interface IScrollEvent {
inSmoothScrolling: boolean;
oldWidth: number;
oldScrollWidth: number;
oldScrollLeft: number;
width: number;
scrollWidth: number;
scrollLeft: number;
oldHeight: number;
oldScrollHeight: number;
oldScrollTop: number;
height: number;
scrollHeight: number;
scrollTop: number;
widthChanged: boolean;
scrollWidthChanged: boolean;
scrollLeftChanged: boolean;
heightChanged: boolean;
scrollHeightChanged: boolean;
scrollTopChanged: boolean;
}
export class ScrollState implements IScrollDimensions, IScrollPosition {
private _scrollStateBrand: void = undefined;
public readonly rawScrollLeft: number;
public readonly rawScrollTop: number;
public readonly width: number;
public readonly scrollWidth: number;
public readonly scrollLeft: number;
public readonly height: number;
public readonly scrollHeight: number;
public readonly scrollTop: number;
constructor(
private readonly _forceIntegerValues: boolean,
width: number,
scrollWidth: number,
scrollLeft: number,
height: number,
scrollHeight: number,
scrollTop: number
) {
if (this._forceIntegerValues) {
width = width | 0;
scrollWidth = scrollWidth | 0;
scrollLeft = scrollLeft | 0;
height = height | 0;
scrollHeight = scrollHeight | 0;
scrollTop = scrollTop | 0;
}
this.rawScrollLeft = scrollLeft;
this.rawScrollTop = scrollTop;
if (width < 0) {
width = 0;
}
if (scrollLeft + width > scrollWidth) {
scrollLeft = scrollWidth - width;
}
if (scrollLeft < 0) {
scrollLeft = 0;
}
if (height < 0) {
height = 0;
}
if (scrollTop + height > scrollHeight) {
scrollTop = scrollHeight - height;
}
if (scrollTop < 0) {
scrollTop = 0;
}
this.width = width;
this.scrollWidth = scrollWidth;
this.scrollLeft = scrollLeft;
this.height = height;
this.scrollHeight = scrollHeight;
this.scrollTop = scrollTop;
}
public equals(other: ScrollState): boolean {
return (
this.rawScrollLeft === other.rawScrollLeft
&& this.rawScrollTop === other.rawScrollTop
&& this.width === other.width
&& this.scrollWidth === other.scrollWidth
&& this.scrollLeft === other.scrollLeft
&& this.height === other.height
&& this.scrollHeight === other.scrollHeight
&& this.scrollTop === other.scrollTop
);
}
public withScrollDimensions(update: INewScrollDimensions, useRawScrollPositions: boolean): ScrollState {
return new ScrollState(
this._forceIntegerValues,
(typeof update.width !== 'undefined' ? update.width : this.width),
(typeof update.scrollWidth !== 'undefined' ? update.scrollWidth : this.scrollWidth),
useRawScrollPositions ? this.rawScrollLeft : this.scrollLeft,
(typeof update.height !== 'undefined' ? update.height : this.height),
(typeof update.scrollHeight !== 'undefined' ? update.scrollHeight : this.scrollHeight),
useRawScrollPositions ? this.rawScrollTop : this.scrollTop
);
}
public withScrollPosition(update: INewScrollPosition): ScrollState {
return new ScrollState(
this._forceIntegerValues,
this.width,
this.scrollWidth,
(typeof update.scrollLeft !== 'undefined' ? update.scrollLeft : this.rawScrollLeft),
this.height,
this.scrollHeight,
(typeof update.scrollTop !== 'undefined' ? update.scrollTop : this.rawScrollTop)
);
}
public createScrollEvent(previous: ScrollState, inSmoothScrolling: boolean): IScrollEvent {
const widthChanged = (this.width !== previous.width);
const scrollWidthChanged = (this.scrollWidth !== previous.scrollWidth);
const scrollLeftChanged = (this.scrollLeft !== previous.scrollLeft);
const heightChanged = (this.height !== previous.height);
const scrollHeightChanged = (this.scrollHeight !== previous.scrollHeight);
const scrollTopChanged = (this.scrollTop !== previous.scrollTop);
return {
inSmoothScrolling: inSmoothScrolling,
oldWidth: previous.width,
oldScrollWidth: previous.scrollWidth,
oldScrollLeft: previous.scrollLeft,
width: this.width,
scrollWidth: this.scrollWidth,
scrollLeft: this.scrollLeft,
oldHeight: previous.height,
oldScrollHeight: previous.scrollHeight,
oldScrollTop: previous.scrollTop,
height: this.height,
scrollHeight: this.scrollHeight,
scrollTop: this.scrollTop,
widthChanged: widthChanged,
scrollWidthChanged: scrollWidthChanged,
scrollLeftChanged: scrollLeftChanged,
heightChanged: heightChanged,
scrollHeightChanged: scrollHeightChanged,
scrollTopChanged: scrollTopChanged,
};
}
}
export interface IScrollDimensions {
readonly width: number;
readonly scrollWidth: number;
readonly height: number;
readonly scrollHeight: number;
}
export interface INewScrollDimensions {
width?: number;
scrollWidth?: number;
height?: number;
scrollHeight?: number;
}
export interface IScrollPosition {
readonly scrollLeft: number;
readonly scrollTop: number;
}
export interface ISmoothScrollPosition {
readonly scrollLeft: number;
readonly scrollTop: number;
readonly width: number;
readonly height: number;
}
export interface INewScrollPosition {
scrollLeft?: number;
scrollTop?: number;
}
export interface IScrollableOptions {
forceIntegerValues: boolean;
smoothScrollDuration: number;
scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable;
}
export class Scrollable extends Disposable {
private _scrollableBrand: void = undefined;
private _smoothScrollDuration: number;
private readonly _scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable;
private _state: ScrollState;
private _smoothScrolling: SmoothScrollingOperation | null;
private _onScroll = this._register(new Emitter<IScrollEvent>());
public readonly onScroll: IEvent<IScrollEvent> = this._onScroll.event;
constructor(options: IScrollableOptions) {
super();
this._smoothScrollDuration = options.smoothScrollDuration;
this._scheduleAtNextAnimationFrame = options.scheduleAtNextAnimationFrame;
this._state = new ScrollState(options.forceIntegerValues, 0, 0, 0, 0, 0, 0);
this._smoothScrolling = null;
}
public override dispose(): void {
if (this._smoothScrolling) {
this._smoothScrolling.dispose();
this._smoothScrolling = null;
}
super.dispose();
}
public setSmoothScrollDuration(smoothScrollDuration: number): void {
this._smoothScrollDuration = smoothScrollDuration;
}
public validateScrollPosition(scrollPosition: INewScrollPosition): IScrollPosition {
return this._state.withScrollPosition(scrollPosition);
}
public getScrollDimensions(): IScrollDimensions {
return this._state;
}
public setScrollDimensions(dimensions: INewScrollDimensions, useRawScrollPositions: boolean): void {
const newState = this._state.withScrollDimensions(dimensions, useRawScrollPositions);
this._setState(newState, Boolean(this._smoothScrolling));
this._smoothScrolling?.acceptScrollDimensions(this._state);
}
public getFutureScrollPosition(): IScrollPosition {
if (this._smoothScrolling) {
return this._smoothScrolling.to;
}
return this._state;
}
public getCurrentScrollPosition(): IScrollPosition {
return this._state;
}
public setScrollPositionNow(update: INewScrollPosition): void {
const newState = this._state.withScrollPosition(update);
if (this._smoothScrolling) {
this._smoothScrolling.dispose();
this._smoothScrolling = null;
}
this._setState(newState, false);
}
public setScrollPositionSmooth(update: INewScrollPosition, reuseAnimation?: boolean): void {
if (this._smoothScrollDuration === 0) {
this.setScrollPositionNow(update); return;
}
if (this._smoothScrolling) {
update = {
scrollLeft: (typeof update.scrollLeft === 'undefined' ? this._smoothScrolling.to.scrollLeft : update.scrollLeft),
scrollTop: (typeof update.scrollTop === 'undefined' ? this._smoothScrolling.to.scrollTop : update.scrollTop)
};
const validTarget = this._state.withScrollPosition(update);
if (this._smoothScrolling.to.scrollLeft === validTarget.scrollLeft && this._smoothScrolling.to.scrollTop === validTarget.scrollTop) {
return;
}
let newSmoothScrolling: SmoothScrollingOperation;
if (reuseAnimation) {
newSmoothScrolling = new SmoothScrollingOperation(this._smoothScrolling.from, validTarget, this._smoothScrolling.startTime, this._smoothScrolling.duration);
} else {
newSmoothScrolling = SmoothScrollingOperation.start(this._state, validTarget, this._smoothScrollDuration);
}
this._smoothScrolling.dispose();
this._smoothScrolling = newSmoothScrolling;
} else {
const validTarget = this._state.withScrollPosition(update);
this._smoothScrolling = SmoothScrollingOperation.start(this._state, validTarget, this._smoothScrollDuration);
}
this._smoothScrolling.animationFrameDisposable = this._scheduleAtNextAnimationFrame(() => {
if (!this._smoothScrolling) {
return;
}
this._smoothScrolling.animationFrameDisposable = null;
this._performSmoothScrolling();
});
}
public hasPendingScrollAnimation(): boolean {
return Boolean(this._smoothScrolling);
}
private _performSmoothScrolling(): void {
if (!this._smoothScrolling) {
return;
}
const update = this._smoothScrolling.tick();
const newState = this._state.withScrollPosition(update);
this._setState(newState, true);
if (!this._smoothScrolling) {
return;
}
if (update.isDone) {
this._smoothScrolling.dispose();
this._smoothScrolling = null;
return;
}
this._smoothScrolling.animationFrameDisposable = this._scheduleAtNextAnimationFrame(() => {
if (!this._smoothScrolling) {
return;
}
this._smoothScrolling.animationFrameDisposable = null;
this._performSmoothScrolling();
});
}
private _setState(newState: ScrollState, inSmoothScrolling: boolean): void {
const oldState = this._state;
if (oldState.equals(newState)) {
return;
}
this._state = newState;
this._onScroll.fire(this._state.createScrollEvent(oldState, inSmoothScrolling));
}
}
class SmoothScrollingUpdate {
public readonly scrollLeft: number;
public readonly scrollTop: number;
public readonly isDone: boolean;
constructor(scrollLeft: number, scrollTop: number, isDone: boolean) {
this.scrollLeft = scrollLeft;
this.scrollTop = scrollTop;
this.isDone = isDone;
}
}
interface IAnimation {
(completion: number): number;
}
function createEaseOutCubic(from: number, to: number): IAnimation {
const delta = to - from;
return function (completion: number): number {
return from + delta * easeOutCubic(completion);
};
}
function createComposed(a: IAnimation, b: IAnimation, cut: number): IAnimation {
return function (completion: number): number {
if (completion < cut) {
return a(completion / cut);
}
return b((completion - cut) / (1 - cut));
};
}
class SmoothScrollingOperation {
public readonly from: ISmoothScrollPosition;
public to: ISmoothScrollPosition;
public readonly duration: number;
public readonly startTime: number;
public animationFrameDisposable: IDisposable | null;
private _scrollLeft!: IAnimation;
private _scrollTop!: IAnimation;
constructor(from: ISmoothScrollPosition, to: ISmoothScrollPosition, startTime: number, duration: number) {
this.from = from;
this.to = to;
this.duration = duration;
this.startTime = startTime;
this.animationFrameDisposable = null;
this._initAnimations();
}
private _initAnimations(): void {
this._scrollLeft = this._initAnimation(this.from.scrollLeft, this.to.scrollLeft, this.to.width);
this._scrollTop = this._initAnimation(this.from.scrollTop, this.to.scrollTop, this.to.height);
}
private _initAnimation(from: number, to: number, viewportSize: number): IAnimation {
const delta = Math.abs(from - to);
if (delta > 2.5 * viewportSize) {
let stop1: number; let stop2: number;
if (from < to) {
stop1 = from + 0.75 * viewportSize;
stop2 = to - 0.75 * viewportSize;
} else {
stop1 = from - 0.75 * viewportSize;
stop2 = to + 0.75 * viewportSize;
}
return createComposed(createEaseOutCubic(from, stop1), createEaseOutCubic(stop2, to), 0.33);
}
return createEaseOutCubic(from, to);
}
public dispose(): void {
if (this.animationFrameDisposable !== null) {
this.animationFrameDisposable.dispose();
this.animationFrameDisposable = null;
}
}
public acceptScrollDimensions(state: ScrollState): void {
this.to = state.withScrollPosition(this.to);
this._initAnimations();
}
public tick(): SmoothScrollingUpdate {
return this._tick(Date.now());
}
protected _tick(now: number): SmoothScrollingUpdate {
const completion = (now - this.startTime) / this.duration;
if (completion < 1) {
const newScrollLeft = this._scrollLeft(completion);
const newScrollTop = this._scrollTop(completion);
return new SmoothScrollingUpdate(newScrollLeft, newScrollTop, false);
}
return new SmoothScrollingUpdate(this.to.scrollLeft, this.to.scrollTop, true);
}
public static start(from: ISmoothScrollPosition, to: ISmoothScrollPosition, duration: number): SmoothScrollingOperation {
duration = duration + 10;
const startTime = Date.now() - 10;
return new SmoothScrollingOperation(from, to, startTime, duration);
}
}
function easeInCubic(t: number): number {
return Math.pow(t, 3);
}
function easeOutCubic(t: number): number {
return 1 - easeInCubic(1 - t);
}
+579
View File
@@ -0,0 +1,579 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from '../Dom';
import { FastDomNode } from './fastDomNode';
import { IMouseEvent, IMouseWheelEvent, StandardWheelEvent } from './mouseEvent';
import { IScrollbarHost } from './abstractScrollbar';
import { HorizontalScrollbar } from './horizontalScrollbar';
import { IScrollableElementChangeOptions, IScrollableElementCreationOptions, IScrollableElementResolvedOptions } from './scrollableElementOptions';
import { VerticalScrollbar } from './verticalScrollbar';
import { Widget } from './widget';
import { TimeoutTimer } from 'common/Async';
import { Emitter, IEvent } from 'common/Event';
import { IDisposable, dispose } from 'common/Lifecycle';
import * as platform from 'common/Platform';
import { INewScrollDimensions, INewScrollPosition, IScrollDimensions, IScrollPosition, IScrollEvent, Scrollable, ScrollbarVisibility } from './scrollable';
// import 'vs/css!./media/scrollbars';
const HIDE_TIMEOUT = 500;
const SCROLL_WHEEL_SENSITIVITY = 50;
class MouseWheelClassifierItem {
public timestamp: number;
public deltaX: number;
public deltaY: number;
public score: number;
constructor(timestamp: number, deltaX: number, deltaY: number) {
this.timestamp = timestamp;
this.deltaX = deltaX;
this.deltaY = deltaY;
this.score = 0;
}
}
class MouseWheelClassifier {
public static readonly INSTANCE = new MouseWheelClassifier();
private readonly _capacity: number;
private _memory: MouseWheelClassifierItem[];
private _front: number;
private _rear: number;
constructor() {
this._capacity = 5;
this._memory = [];
this._front = -1;
this._rear = -1;
}
public isPhysicalMouseWheel(): boolean {
if (this._front === -1 && this._rear === -1) {
return false;
}
let remainingInfluence = 1;
let score = 0;
let iteration = 1;
let index = this._rear;
while (index !== -1) {
const influence = (index === this._front ? remainingInfluence : Math.pow(2, -iteration));
remainingInfluence -= influence;
score += this._memory[index].score * influence;
if (index === this._front) {
break;
}
index = (this._capacity + index - 1) % this._capacity;
iteration++;
}
return (score <= 0.5);
}
public acceptStandardWheelEvent(e: StandardWheelEvent): void {
if (platform.isChrome) {
const targetWindow = dom.getWindow(e.browserEvent);
const pageZoomFactor = platform.getZoomFactor(targetWindow);
this.accept(Date.now(), e.deltaX * pageZoomFactor, e.deltaY * pageZoomFactor);
} else {
this.accept(Date.now(), e.deltaX, e.deltaY);
}
}
public accept(timestamp: number, deltaX: number, deltaY: number): void {
let previousItem = null;
const item = new MouseWheelClassifierItem(timestamp, deltaX, deltaY);
if (this._front === -1 && this._rear === -1) {
this._memory[0] = item;
this._front = 0;
this._rear = 0;
} else {
previousItem = this._memory[this._rear];
this._rear = (this._rear + 1) % this._capacity;
if (this._rear === this._front) {
this._front = (this._front + 1) % this._capacity;
}
this._memory[this._rear] = item;
}
item.score = this._computeScore(item, previousItem);
}
private _computeScore(item: MouseWheelClassifierItem, previousItem: MouseWheelClassifierItem | null): number {
if (Math.abs(item.deltaX) > 0 && Math.abs(item.deltaY) > 0) {
return 1;
}
let score: number = 0.5;
if (!this._isAlmostInt(item.deltaX) || !this._isAlmostInt(item.deltaY)) {
score += 0.25;
}
if (previousItem) {
const absDeltaX = Math.abs(item.deltaX);
const absDeltaY = Math.abs(item.deltaY);
const absPreviousDeltaX = Math.abs(previousItem.deltaX);
const absPreviousDeltaY = Math.abs(previousItem.deltaY);
const minDeltaX = Math.max(Math.min(absDeltaX, absPreviousDeltaX), 1);
const minDeltaY = Math.max(Math.min(absDeltaY, absPreviousDeltaY), 1);
const maxDeltaX = Math.max(absDeltaX, absPreviousDeltaX);
const maxDeltaY = Math.max(absDeltaY, absPreviousDeltaY);
const isSameModulo = (maxDeltaX % minDeltaX === 0 && maxDeltaY % minDeltaY === 0);
if (isSameModulo) {
score -= 0.5;
}
}
return Math.min(Math.max(score, 0), 1);
}
private _isAlmostInt(value: number): boolean {
const delta = Math.abs(Math.round(value) - value);
return (delta < 0.01);
}
}
export class SmoothScrollableElement extends Widget {
private readonly _options: IScrollableElementResolvedOptions;
protected readonly _scrollable: Scrollable;
private readonly _verticalScrollbar: VerticalScrollbar;
private readonly _horizontalScrollbar: HorizontalScrollbar;
private readonly _domNode: HTMLElement;
private readonly _leftShadowDomNode: FastDomNode<HTMLElement> | null;
private readonly _topShadowDomNode: FastDomNode<HTMLElement> | null;
private readonly _topLeftShadowDomNode: FastDomNode<HTMLElement> | null;
private readonly _listenOnDomNode: HTMLElement;
private _mouseWheelToDispose: IDisposable[];
private _isDragging: boolean;
private _mouseIsOver: boolean;
private readonly _hideTimeout: TimeoutTimer;
private _shouldRender: boolean;
private _revealOnScroll: boolean;
private readonly _onScroll = this._register(new Emitter<IScrollEvent>());
public readonly onScroll: IEvent<IScrollEvent> = this._onScroll.event;
public get options(): Readonly<IScrollableElementResolvedOptions> {
return this._options;
}
public constructor(element: HTMLElement, options: IScrollableElementCreationOptions, scrollable?: Scrollable) {
super();
options = options ?? {};
let resolvedScrollable: Scrollable;
const ownsScrollable = !scrollable;
if (scrollable) {
resolvedScrollable = scrollable;
} else {
options.mouseWheelSmoothScroll = false;
resolvedScrollable = new Scrollable({
forceIntegerValues: true,
smoothScrollDuration: 0,
scheduleAtNextAnimationFrame: (callback) => dom.scheduleAtNextAnimationFrame(dom.getWindow(element), callback)
});
}
this._options = resolveOptions(options);
this._scrollable = resolvedScrollable;
this._register(this._scrollable.onScroll((e) => {
this._handleScroll(e);
this._onScroll.fire(e);
}));
if (ownsScrollable) {
this._register(this._scrollable);
}
const scrollbarHost: IScrollbarHost = {
handleMouseWheel: (mouseWheelEvent: StandardWheelEvent) => this._handleMouseWheel(mouseWheelEvent),
handleDragStart: () => this._handleDragStart(),
handleDragEnd: () => this._handleDragEnd(),
};
this._verticalScrollbar = this._register(new VerticalScrollbar(this._scrollable, this._options, scrollbarHost));
this._horizontalScrollbar = this._register(new HorizontalScrollbar(this._scrollable, this._options, scrollbarHost));
this._domNode = document.createElement('div');
this._domNode.className = 'xterm-scrollable-element ' + this._options.className;
this._domNode.setAttribute('role', 'presentation');
this._domNode.style.position = 'relative';
this._domNode.appendChild(element);
this._domNode.appendChild(this._horizontalScrollbar.domNode.domNode);
this._domNode.appendChild(this._verticalScrollbar.domNode.domNode);
if (this._options.useShadows) {
this._leftShadowDomNode = new FastDomNode(document.createElement('div'));
this._leftShadowDomNode.setClassName('xterm-shadow');
this._domNode.appendChild(this._leftShadowDomNode.domNode);
this._topShadowDomNode = new FastDomNode(document.createElement('div'));
this._topShadowDomNode.setClassName('xterm-shadow');
this._domNode.appendChild(this._topShadowDomNode.domNode);
this._topLeftShadowDomNode = new FastDomNode(document.createElement('div'));
this._topLeftShadowDomNode.setClassName('xterm-shadow');
this._domNode.appendChild(this._topLeftShadowDomNode.domNode);
} else {
this._leftShadowDomNode = null;
this._topShadowDomNode = null;
this._topLeftShadowDomNode = null;
}
this._listenOnDomNode = this._options.listenOnDomNode ?? this._domNode;
this._mouseWheelToDispose = [];
this._setListeningToMouseWheel(this._options.handleMouseWheel);
this._onmouseover(this._listenOnDomNode, (e) => this._handleMouseOver(e));
this._onmouseleave(this._listenOnDomNode, (e) => this._handleMouseLeave(e));
this._hideTimeout = this._register(new TimeoutTimer());
this._isDragging = false;
this._mouseIsOver = false;
this._shouldRender = true;
this._revealOnScroll = true;
}
public override dispose(): void {
this._mouseWheelToDispose = dispose(this._mouseWheelToDispose);
super.dispose();
}
public getDomNode(): HTMLElement {
return this._domNode;
}
public getScrollDimensions(): IScrollDimensions {
return this._scrollable.getScrollDimensions();
}
public setScrollDimensions(dimensions: INewScrollDimensions): void {
this._scrollable.setScrollDimensions(dimensions, false);
}
public setScrollPosition(update: INewScrollPosition & { reuseAnimation?: boolean }): void {
if (update.reuseAnimation) {
this._scrollable.setScrollPositionSmooth(update, update.reuseAnimation);
} else {
this._scrollable.setScrollPositionNow(update);
}
}
public getScrollPosition(): IScrollPosition {
return this._scrollable.getCurrentScrollPosition();
}
public updateClassName(newClassName: string): void {
this._options.className = newClassName;
if (platform.isMac) {
this._options.className += ' xterm-mac';
}
this._domNode.className = 'xterm-scrollable-element ' + this._options.className;
}
public updateOptions(newOptions: IScrollableElementChangeOptions): void {
if (typeof newOptions.handleMouseWheel !== 'undefined') {
this._options.handleMouseWheel = newOptions.handleMouseWheel;
this._setListeningToMouseWheel(this._options.handleMouseWheel);
}
if (typeof newOptions.mouseWheelScrollSensitivity !== 'undefined') {
this._options.mouseWheelScrollSensitivity = newOptions.mouseWheelScrollSensitivity;
}
if (typeof newOptions.fastScrollSensitivity !== 'undefined') {
this._options.fastScrollSensitivity = newOptions.fastScrollSensitivity;
}
if (typeof newOptions.scrollPredominantAxis !== 'undefined') {
this._options.scrollPredominantAxis = newOptions.scrollPredominantAxis;
}
if (typeof newOptions.horizontal !== 'undefined') {
this._options.horizontal = newOptions.horizontal;
}
if (typeof newOptions.vertical !== 'undefined') {
this._options.vertical = newOptions.vertical;
}
if (typeof newOptions.horizontalHasArrows !== 'undefined') {
this._options.horizontalHasArrows = newOptions.horizontalHasArrows;
}
if (typeof newOptions.verticalHasArrows !== 'undefined') {
this._options.verticalHasArrows = newOptions.verticalHasArrows;
}
if (typeof newOptions.horizontalScrollbarSize !== 'undefined') {
this._options.horizontalScrollbarSize = newOptions.horizontalScrollbarSize;
}
if (typeof newOptions.verticalScrollbarSize !== 'undefined') {
this._options.verticalScrollbarSize = newOptions.verticalScrollbarSize;
}
if (typeof newOptions.scrollByPage !== 'undefined') {
this._options.scrollByPage = newOptions.scrollByPage;
}
this._horizontalScrollbar.updateOptions(this._options);
this._verticalScrollbar.updateOptions(this._options);
if (!this._options.lazyRender) {
this._render();
}
}
public delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent): void {
this._handleMouseWheel(new StandardWheelEvent(browserEvent));
}
// -------------------- mouse wheel scrolling --------------------
private _setListeningToMouseWheel(shouldListen: boolean): void {
const isListening = (this._mouseWheelToDispose.length > 0);
if (isListening === shouldListen) {
return;
}
this._mouseWheelToDispose = dispose(this._mouseWheelToDispose);
if (shouldListen) {
const onMouseWheel = (browserEvent: IMouseWheelEvent): void => {
this._handleMouseWheel(new StandardWheelEvent(browserEvent));
};
this._mouseWheelToDispose.push(dom.addDisposableListener(this._listenOnDomNode, dom.eventType.MOUSE_WHEEL, onMouseWheel, { passive: false }));
}
}
private _handleMouseWheel(e: StandardWheelEvent): void {
if (e.browserEvent?.defaultPrevented) {
return;
}
const classifier = MouseWheelClassifier.INSTANCE;
classifier.acceptStandardWheelEvent(e);
let didScroll = false;
if (e.deltaY || e.deltaX) {
let deltaY = e.deltaY * this._options.mouseWheelScrollSensitivity;
let deltaX = e.deltaX * this._options.mouseWheelScrollSensitivity;
if (this._options.scrollPredominantAxis) {
if (this._options.scrollYToX && deltaX + deltaY === 0) {
deltaX = deltaY = 0;
} else if (Math.abs(deltaY) >= Math.abs(deltaX)) {
deltaX = 0;
} else {
deltaY = 0;
}
}
if (this._options.flipAxes) {
[deltaY, deltaX] = [deltaX, deltaY];
}
const shiftConvert = !platform.isMac && e.browserEvent && e.browserEvent.shiftKey;
if ((this._options.scrollYToX || shiftConvert) && !deltaX) {
deltaX = deltaY;
deltaY = 0;
}
if (e.browserEvent && e.browserEvent.altKey) {
deltaX = deltaX * this._options.fastScrollSensitivity;
deltaY = deltaY * this._options.fastScrollSensitivity;
}
const futureScrollPosition = this._scrollable.getFutureScrollPosition();
let desiredScrollPosition: INewScrollPosition = {};
if (deltaY) {
const deltaScrollTop = SCROLL_WHEEL_SENSITIVITY * deltaY;
const desiredScrollTop = futureScrollPosition.scrollTop - (deltaScrollTop < 0 ? Math.floor(deltaScrollTop) : Math.ceil(deltaScrollTop));
this._verticalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollTop);
}
if (deltaX) {
const deltaScrollLeft = SCROLL_WHEEL_SENSITIVITY * deltaX;
const desiredScrollLeft = futureScrollPosition.scrollLeft - (deltaScrollLeft < 0 ? Math.floor(deltaScrollLeft) : Math.ceil(deltaScrollLeft));
this._horizontalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollLeft);
}
desiredScrollPosition = this._scrollable.validateScrollPosition(desiredScrollPosition);
if (futureScrollPosition.scrollLeft !== desiredScrollPosition.scrollLeft || futureScrollPosition.scrollTop !== desiredScrollPosition.scrollTop) {
const canPerformSmoothScroll = (
this._options.mouseWheelSmoothScroll
&& classifier.isPhysicalMouseWheel()
);
if (canPerformSmoothScroll) {
this._scrollable.setScrollPositionSmooth(desiredScrollPosition);
} else {
this._scrollable.setScrollPositionNow(desiredScrollPosition);
}
didScroll = true;
}
}
let consumeMouseWheel = didScroll;
if (!consumeMouseWheel && this._options.alwaysConsumeMouseWheel) {
consumeMouseWheel = true;
}
if (!consumeMouseWheel && this._options.consumeMouseWheelIfScrollbarIsNeeded && (this._verticalScrollbar.isNeeded() || this._horizontalScrollbar.isNeeded())) {
consumeMouseWheel = true;
}
if (consumeMouseWheel) {
e.preventDefault();
e.stopPropagation();
}
}
private _handleScroll(e: IScrollEvent): void {
this._shouldRender = this._horizontalScrollbar.handleScroll(e) || this._shouldRender;
this._shouldRender = this._verticalScrollbar.handleScroll(e) || this._shouldRender;
if (this._options.useShadows) {
this._shouldRender = true;
}
if (this._revealOnScroll) {
this._reveal();
}
if (!this._options.lazyRender) {
this._render();
}
}
public renderNow(): void {
if (!this._options.lazyRender) {
throw new Error('Please use `lazyRender` together with `renderNow`!');
}
this._render();
}
private _render(): void {
if (!this._shouldRender) {
return;
}
this._shouldRender = false;
this._horizontalScrollbar.render();
this._verticalScrollbar.render();
if (this._options.useShadows) {
const scrollState = this._scrollable.getCurrentScrollPosition();
const enableTop = scrollState.scrollTop > 0;
const enableLeft = scrollState.scrollLeft > 0;
const leftClassName = (enableLeft ? ' xterm-shadow-left' : '');
const topClassName = (enableTop ? ' xterm-shadow-top' : '');
const topLeftClassName = (enableLeft || enableTop ? ' xterm-shadow-top-left-corner' : '');
this._leftShadowDomNode!.setClassName(`xterm-shadow${leftClassName}`);
this._topShadowDomNode!.setClassName(`xterm-shadow${topClassName}`);
this._topLeftShadowDomNode!.setClassName(`xterm-shadow${topLeftClassName}${topClassName}${leftClassName}`);
}
}
// -------------------- fade in / fade out --------------------
private _handleDragStart(): void {
this._isDragging = true;
this._reveal();
}
private _handleDragEnd(): void {
this._isDragging = false;
this._hide();
}
private _handleMouseLeave(e: IMouseEvent): void {
this._mouseIsOver = false;
this._hide();
}
private _handleMouseOver(e: IMouseEvent): void {
this._mouseIsOver = true;
this._reveal();
}
private _reveal(): void {
this._verticalScrollbar.beginReveal();
this._horizontalScrollbar.beginReveal();
this._scheduleHide();
}
private _hide(): void {
if (!this._mouseIsOver && !this._isDragging) {
this._verticalScrollbar.beginHide();
this._horizontalScrollbar.beginHide();
}
}
private _scheduleHide(): void {
if (!this._mouseIsOver && !this._isDragging) {
this._hideTimeout.cancelAndSet(() => this._hide(), HIDE_TIMEOUT);
}
}
}
function resolveOptions(opts: IScrollableElementCreationOptions): IScrollableElementResolvedOptions {
const result: IScrollableElementResolvedOptions = {
lazyRender: (typeof opts.lazyRender !== 'undefined' ? opts.lazyRender : false),
className: (typeof opts.className !== 'undefined' ? opts.className : ''),
useShadows: (typeof opts.useShadows !== 'undefined' ? opts.useShadows : true),
handleMouseWheel: (typeof opts.handleMouseWheel !== 'undefined' ? opts.handleMouseWheel : true),
flipAxes: (typeof opts.flipAxes !== 'undefined' ? opts.flipAxes : false),
consumeMouseWheelIfScrollbarIsNeeded: (typeof opts.consumeMouseWheelIfScrollbarIsNeeded !== 'undefined' ? opts.consumeMouseWheelIfScrollbarIsNeeded : false),
alwaysConsumeMouseWheel: (typeof opts.alwaysConsumeMouseWheel !== 'undefined' ? opts.alwaysConsumeMouseWheel : false),
scrollYToX: (typeof opts.scrollYToX !== 'undefined' ? opts.scrollYToX : false),
mouseWheelScrollSensitivity: (typeof opts.mouseWheelScrollSensitivity !== 'undefined' ? opts.mouseWheelScrollSensitivity : 1),
fastScrollSensitivity: (typeof opts.fastScrollSensitivity !== 'undefined' ? opts.fastScrollSensitivity : 5),
scrollPredominantAxis: (typeof opts.scrollPredominantAxis !== 'undefined' ? opts.scrollPredominantAxis : true),
mouseWheelSmoothScroll: (typeof opts.mouseWheelSmoothScroll !== 'undefined' ? opts.mouseWheelSmoothScroll : true),
listenOnDomNode: (typeof opts.listenOnDomNode !== 'undefined' ? opts.listenOnDomNode : null),
horizontal: (typeof opts.horizontal !== 'undefined' ? opts.horizontal : ScrollbarVisibility.AUTO),
horizontalScrollbarSize: (typeof opts.horizontalScrollbarSize !== 'undefined' ? opts.horizontalScrollbarSize : 10),
horizontalSliderSize: (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : 0),
horizontalHasArrows: (typeof opts.horizontalHasArrows !== 'undefined' ? opts.horizontalHasArrows : false),
vertical: (typeof opts.vertical !== 'undefined' ? opts.vertical : ScrollbarVisibility.AUTO),
verticalScrollbarSize: (typeof opts.verticalScrollbarSize !== 'undefined' ? opts.verticalScrollbarSize : 10),
verticalHasArrows: (typeof opts.verticalHasArrows !== 'undefined' ? opts.verticalHasArrows : false),
verticalSliderSize: (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : 0),
scrollByPage: (typeof opts.scrollByPage !== 'undefined' ? opts.scrollByPage : false)
};
result.horizontalSliderSize = (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : result.horizontalScrollbarSize);
result.verticalSliderSize = (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : result.verticalScrollbarSize);
if (platform.isMac) {
result.className += ' xterm-mac';
}
return result;
}
@@ -0,0 +1,161 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ScrollbarVisibility } from './scrollable';
export interface IScrollableElementCreationOptions {
/**
* The scrollable element should not do any DOM mutations until renderNow() is called.
* Defaults to false.
*/
lazyRender?: boolean;
/**
* CSS Class name for the scrollable element.
*/
className?: string;
/**
* Drop subtle horizontal and vertical shadows.
* Defaults to false.
*/
useShadows?: boolean;
/**
* Handle mouse wheel (listen to mouse wheel scrolling).
* Defaults to true
*/
handleMouseWheel?: boolean;
/**
* If mouse wheel is handled, make mouse wheel scrolling smooth.
* Defaults to true.
*/
mouseWheelSmoothScroll?: boolean;
/**
* Flip axes. Treat vertical scrolling like horizontal and vice-versa.
* Defaults to false.
*/
flipAxes?: boolean;
/**
* If enabled, will scroll horizontally when scrolling vertical.
* Defaults to false.
*/
scrollYToX?: boolean;
/**
* Consume all mouse wheel events if a scrollbar is needed (i.e. scrollSize > size).
* Defaults to false.
*/
consumeMouseWheelIfScrollbarIsNeeded?: boolean;
/**
* Always consume mouse wheel events, even when scrolling is no longer possible.
* Defaults to false.
*/
alwaysConsumeMouseWheel?: boolean;
/**
* A multiplier to be used on the `deltaX` and `deltaY` of mouse wheel scroll events.
* Defaults to 1.
*/
mouseWheelScrollSensitivity?: number;
/**
* FastScrolling mulitplier speed when pressing `Alt`
* Defaults to 5.
*/
fastScrollSensitivity?: number;
/**
* Whether the scrollable will only scroll along the predominant axis when scrolling both
* vertically and horizontally at the same time.
* Prevents horizontal drift when scrolling vertically on a trackpad.
* Defaults to true.
*/
scrollPredominantAxis?: boolean;
/**
* The dom node events should be bound to.
* If no listenOnDomNode is provided, the constructor dom node is used.
*/
listenOnDomNode?: HTMLElement;
/**
* Control the visibility of the horizontal scrollbar.
* Accepted values: 'auto' (on mouse over), 'visible' (always visible), 'hidden' (never visible)
* Defaults to 'auto'.
*/
horizontal?: ScrollbarVisibility;
/**
* Height (in px) of the horizontal scrollbar.
* Defaults to 10.
*/
horizontalScrollbarSize?: number;
/**
* Height (in px) of the horizontal scrollbar slider.
* Defaults to `horizontalScrollbarSize`
*/
horizontalSliderSize?: number;
/**
* Render arrows (left/right) for the horizontal scrollbar.
* Defaults to false.
*/
horizontalHasArrows?: boolean;
/**
* Control the visibility of the vertical scrollbar.
* Accepted values: 'auto' (on mouse over), 'visible' (always visible), 'hidden' (never visible)
* Defaults to 'auto'.
*/
vertical?: ScrollbarVisibility;
/**
* Width (in px) of the vertical scrollbar.
* Defaults to 10.
*/
verticalScrollbarSize?: number;
/**
* Width (in px) of the vertical scrollbar slider.
* Defaults to `verticalScrollbarSize`
*/
verticalSliderSize?: number;
/**
* Render arrows (top/bottom) for the vertical scrollbar.
* Defaults to false.
*/
verticalHasArrows?: boolean;
/**
* Scroll gutter clicks move by page vs. jump to position.
* Defaults to false.
*/
scrollByPage?: boolean;
}
export interface IScrollableElementChangeOptions {
handleMouseWheel?: boolean;
mouseWheelScrollSensitivity?: number;
fastScrollSensitivity?: number;
scrollPredominantAxis?: boolean;
horizontal?: ScrollbarVisibility;
horizontalScrollbarSize?: number;
horizontalHasArrows?: boolean;
vertical?: ScrollbarVisibility;
verticalScrollbarSize?: number;
verticalHasArrows?: boolean;
scrollByPage?: boolean;
}
export interface IScrollableElementResolvedOptions {
lazyRender: boolean;
className: string;
useShadows: boolean;
handleMouseWheel: boolean;
flipAxes: boolean;
scrollYToX: boolean;
consumeMouseWheelIfScrollbarIsNeeded: boolean;
alwaysConsumeMouseWheel: boolean;
mouseWheelScrollSensitivity: number;
fastScrollSensitivity: number;
scrollPredominantAxis: boolean;
mouseWheelSmoothScroll: boolean;
listenOnDomNode: HTMLElement | null;
horizontal: ScrollbarVisibility;
horizontalScrollbarSize: number;
horizontalSliderSize: number;
horizontalHasArrows: boolean;
vertical: ScrollbarVisibility;
verticalScrollbarSize: number;
verticalSliderSize: number;
verticalHasArrows: boolean;
scrollByPage: boolean;
}
+110
View File
@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { GlobalPointerMoveMonitor } from './globalPointerMoveMonitor';
import { Widget } from './widget';
import { TimeoutTimer } from 'common/Async';
import * as dom from '../Dom';
export interface IScrollbarArrowOptions {
handleActivate: () => void;
className: string;
// icon: ThemeIcon;
bgWidth: number;
bgHeight: number;
top?: number;
left?: number;
bottom?: number;
right?: number;
}
export class ScrollbarArrow extends Widget {
private _handleActivate: () => void;
public bgDomNode: HTMLElement;
public domNode: HTMLElement;
private _pointerdownRepeatTimer: dom.WindowIntervalTimer;
private _pointerdownScheduleRepeatTimer: TimeoutTimer;
private _pointerMoveMonitor: GlobalPointerMoveMonitor;
constructor(opts: IScrollbarArrowOptions) {
super();
this._handleActivate = opts.handleActivate;
this.bgDomNode = document.createElement('div');
this.bgDomNode.className = 'xterm-arrow-background';
this.bgDomNode.style.position = 'absolute';
this.bgDomNode.style.width = opts.bgWidth + 'px';
this.bgDomNode.style.height = opts.bgHeight + 'px';
if (typeof opts.top !== 'undefined') {
this.bgDomNode.style.top = '0px';
}
if (typeof opts.left !== 'undefined') {
this.bgDomNode.style.left = '0px';
}
if (typeof opts.bottom !== 'undefined') {
this.bgDomNode.style.bottom = '0px';
}
if (typeof opts.right !== 'undefined') {
this.bgDomNode.style.right = '0px';
}
this.domNode = document.createElement('div');
this.domNode.className = opts.className;
// this.domNode.classList.add(...ThemeIcon.asClassNameArray(opts.icon));
this.domNode.style.position = 'absolute';
const arrowSize = Math.min(opts.bgWidth, opts.bgHeight);
this.domNode.style.width = arrowSize + 'px';
this.domNode.style.height = arrowSize + 'px';
if (typeof opts.top !== 'undefined') {
this.domNode.style.top = opts.top + 'px';
}
if (typeof opts.left !== 'undefined') {
this.domNode.style.left = opts.left + 'px';
}
if (typeof opts.bottom !== 'undefined') {
this.domNode.style.bottom = opts.bottom + 'px';
}
if (typeof opts.right !== 'undefined') {
this.domNode.style.right = opts.right + 'px';
}
this._pointerMoveMonitor = this._register(new GlobalPointerMoveMonitor());
this._register(dom.addStandardDisposableListener(this.bgDomNode, dom.eventType.POINTER_DOWN, (e) => this._arrowPointerDown(e)));
this._register(dom.addStandardDisposableListener(this.domNode, dom.eventType.POINTER_DOWN, (e) => this._arrowPointerDown(e)));
this._pointerdownRepeatTimer = this._register(new dom.WindowIntervalTimer());
this._pointerdownScheduleRepeatTimer = this._register(new TimeoutTimer());
}
private _arrowPointerDown(e: PointerEvent): void {
if (!e.target || !(e.target instanceof Element)) {
return;
}
const scheduleRepeater = (): void => {
this._pointerdownRepeatTimer.cancelAndSet(() => this._handleActivate(), 1000 / 24, dom.getWindow(e));
};
this._handleActivate();
this._pointerdownRepeatTimer.cancel();
this._pointerdownScheduleRepeatTimer.cancelAndSet(scheduleRepeater, 200);
this._pointerMoveMonitor.startMonitoring(
e.target,
e.pointerId,
e.buttons,
(pointerMoveData) => { /* Intentional empty */ },
() => {
this._pointerdownRepeatTimer.cancel();
this._pointerdownScheduleRepeatTimer.cancel();
}
);
e.preventDefault();
}
}
+246
View File
@@ -0,0 +1,246 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* The minimal size of the slider (such that it can still be clickable).
* The slider is artificially enlarged to keep it usable.
*/
const MINIMUM_SLIDER_SIZE = 20;
interface IScrollbarStateComputedValues {
computedAvailableSize: number;
computedIsNeeded: boolean;
computedSliderSize: number;
computedSliderRatio: number;
computedSliderPosition: number;
}
export class ScrollbarState {
/**
* For the vertical scrollbar: the width.
* For the horizontal scrollbar: the height.
*/
private _scrollbarSize: number;
/**
* For the vertical scrollbar: the height of the pair horizontal scrollbar.
* For the horizontal scrollbar: the width of the pair vertical scrollbar.
*/
private _oppositeScrollbarSize: number;
/**
* For the vertical scrollbar: the height of the scrollbar's arrows.
* For the horizontal scrollbar: the width of the scrollbar's arrows.
*/
private _arrowSize: number;
// --- variables
/**
* For the vertical scrollbar: the viewport height.
* For the horizontal scrollbar: the viewport width.
*/
private _visibleSize: number;
/**
* For the vertical scrollbar: the scroll height.
* For the horizontal scrollbar: the scroll width.
*/
private _scrollSize: number;
/**
* For the vertical scrollbar: the scroll top.
* For the horizontal scrollbar: the scroll left.
*/
private _scrollPosition: number;
// --- computed variables
/**
* `visibleSize` - `oppositeScrollbarSize`
*/
private _computedAvailableSize: number;
/**
* (`scrollSize` > 0 && `scrollSize` > `visibleSize`)
*/
private _computedIsNeeded: boolean;
private _computedSliderSize: number;
private _computedSliderRatio: number;
private _computedSliderPosition: number;
constructor(arrowSize: number, scrollbarSize: number, oppositeScrollbarSize: number, visibleSize: number, scrollSize: number, scrollPosition: number) {
this._scrollbarSize = Math.round(scrollbarSize);
this._oppositeScrollbarSize = Math.round(oppositeScrollbarSize);
this._arrowSize = Math.round(arrowSize);
this._visibleSize = visibleSize;
this._scrollSize = scrollSize;
this._scrollPosition = scrollPosition;
this._computedAvailableSize = 0;
this._computedIsNeeded = false;
this._computedSliderSize = 0;
this._computedSliderRatio = 0;
this._computedSliderPosition = 0;
this._refreshComputedValues();
}
public clone(): ScrollbarState {
return new ScrollbarState(this._arrowSize, this._scrollbarSize, this._oppositeScrollbarSize, this._visibleSize, this._scrollSize, this._scrollPosition);
}
public setVisibleSize(visibleSize: number): boolean {
const iVisibleSize = Math.round(visibleSize);
if (this._visibleSize !== iVisibleSize) {
this._visibleSize = iVisibleSize;
this._refreshComputedValues();
return true;
}
return false;
}
public setScrollSize(scrollSize: number): boolean {
const iScrollSize = Math.round(scrollSize);
if (this._scrollSize !== iScrollSize) {
this._scrollSize = iScrollSize;
this._refreshComputedValues();
return true;
}
return false;
}
public setScrollPosition(scrollPosition: number): boolean {
const iScrollPosition = Math.round(scrollPosition);
if (this._scrollPosition !== iScrollPosition) {
this._scrollPosition = iScrollPosition;
this._refreshComputedValues();
return true;
}
return false;
}
public setScrollbarSize(scrollbarSize: number): void {
this._scrollbarSize = Math.round(scrollbarSize);
}
public setArrowSize(arrowSize: number): void {
const iArrowSize = Math.round(arrowSize);
if (this._arrowSize !== iArrowSize) {
this._arrowSize = iArrowSize;
this._refreshComputedValues();
}
}
public setOppositeScrollbarSize(oppositeScrollbarSize: number): void {
this._oppositeScrollbarSize = Math.round(oppositeScrollbarSize);
}
private static _computeValues(
oppositeScrollbarSize: number,
arrowSize: number,
visibleSize: number,
scrollSize: number,
scrollPosition: number
): IScrollbarStateComputedValues {
const computedAvailableSize = Math.max(0, visibleSize - oppositeScrollbarSize);
const computedRepresentableSize = Math.max(0, computedAvailableSize - 2 * arrowSize);
const computedIsNeeded = (scrollSize > 0 && scrollSize > visibleSize);
if (!computedIsNeeded) {
return {
computedAvailableSize: Math.round(computedAvailableSize),
computedIsNeeded: computedIsNeeded,
computedSliderSize: Math.round(computedRepresentableSize),
computedSliderRatio: 0,
computedSliderPosition: 0,
};
}
const computedSliderSize = Math.round(Math.max(MINIMUM_SLIDER_SIZE, Math.floor(visibleSize * computedRepresentableSize / scrollSize)));
const computedSliderRatio = (computedRepresentableSize - computedSliderSize) / (scrollSize - visibleSize);
const computedSliderPosition = (scrollPosition * computedSliderRatio);
return {
computedAvailableSize: Math.round(computedAvailableSize),
computedIsNeeded: computedIsNeeded,
computedSliderSize: Math.round(computedSliderSize),
computedSliderRatio: computedSliderRatio,
computedSliderPosition: Math.round(computedSliderPosition),
};
}
private _refreshComputedValues(): void {
const r = ScrollbarState._computeValues(this._oppositeScrollbarSize, this._arrowSize, this._visibleSize, this._scrollSize, this._scrollPosition);
this._computedAvailableSize = r.computedAvailableSize;
this._computedIsNeeded = r.computedIsNeeded;
this._computedSliderSize = r.computedSliderSize;
this._computedSliderRatio = r.computedSliderRatio;
this._computedSliderPosition = r.computedSliderPosition;
}
public getArrowSize(): number {
return this._arrowSize;
}
public getScrollPosition(): number {
return this._scrollPosition;
}
public getRectangleLargeSize(): number {
return this._computedAvailableSize;
}
public getRectangleSmallSize(): number {
return this._scrollbarSize;
}
public isNeeded(): boolean {
return this._computedIsNeeded;
}
public getSliderSize(): number {
return this._computedSliderSize;
}
public getSliderPosition(): number {
return this._computedSliderPosition;
}
public getDesiredScrollPositionFromOffset(offset: number): number {
if (!this._computedIsNeeded) {
return 0;
}
const desiredSliderPosition = offset - this._arrowSize - this._computedSliderSize / 2;
return Math.round(desiredSliderPosition / this._computedSliderRatio);
}
public getDesiredScrollPositionFromOffsetPaged(offset: number): number {
if (!this._computedIsNeeded) {
return 0;
}
const correctedOffset = offset - this._arrowSize;
let desiredScrollPosition = this._scrollPosition;
if (correctedOffset < this._computedSliderPosition) {
desiredScrollPosition -= this._visibleSize;
} else {
desiredScrollPosition += this._visibleSize;
}
return desiredScrollPosition;
}
public getDesiredScrollPositionFromDelta(delta: number): number {
if (!this._computedIsNeeded) {
return 0;
}
const desiredSliderPosition = this._computedSliderPosition + delta;
return Math.round(desiredSliderPosition / this._computedSliderRatio);
}
}
@@ -0,0 +1,113 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { FastDomNode } from './fastDomNode';
import { TimeoutTimer } from 'common/Async';
import { Disposable } from 'common/Lifecycle';
import { ScrollbarVisibility } from './scrollable';
export class ScrollbarVisibilityController extends Disposable {
private _visibility: ScrollbarVisibility;
private _visibleClassName: string;
private _invisibleClassName: string;
private _domNode: FastDomNode<HTMLElement> | null;
private _rawShouldBeVisible: boolean;
private _shouldBeVisible: boolean;
private _isNeeded: boolean;
private _isVisible: boolean;
private _revealTimer: TimeoutTimer;
constructor(visibility: ScrollbarVisibility, visibleClassName: string, invisibleClassName: string) {
super();
this._visibility = visibility;
this._visibleClassName = visibleClassName;
this._invisibleClassName = invisibleClassName;
this._domNode = null;
this._isVisible = false;
this._isNeeded = false;
this._rawShouldBeVisible = false;
this._shouldBeVisible = false;
this._revealTimer = this._register(new TimeoutTimer());
}
public setVisibility(visibility: ScrollbarVisibility): void {
if (this._visibility !== visibility) {
this._visibility = visibility;
this._updateShouldBeVisible();
}
}
public setShouldBeVisible(rawShouldBeVisible: boolean): void {
this._rawShouldBeVisible = rawShouldBeVisible;
this._updateShouldBeVisible();
}
private _applyVisibilitySetting(): boolean {
if (this._visibility === ScrollbarVisibility.HIDDEN) {
return false;
}
if (this._visibility === ScrollbarVisibility.VISIBLE) {
return true;
}
return this._rawShouldBeVisible;
}
private _updateShouldBeVisible(): void {
const shouldBeVisible = this._applyVisibilitySetting();
if (this._shouldBeVisible !== shouldBeVisible) {
this._shouldBeVisible = shouldBeVisible;
this.ensureVisibility();
}
}
public setIsNeeded(isNeeded: boolean): void {
if (this._isNeeded !== isNeeded) {
this._isNeeded = isNeeded;
this.ensureVisibility();
}
}
public setDomNode(domNode: FastDomNode<HTMLElement>): void {
this._domNode = domNode;
this._domNode.setClassName(this._invisibleClassName);
this.setShouldBeVisible(false);
}
public ensureVisibility(): void {
if (!this._isNeeded) {
this._hide(false);
return;
}
if (this._shouldBeVisible) {
this._reveal();
} else {
this._hide(true);
}
}
private _reveal(): void {
if (this._isVisible) {
return;
}
this._isVisible = true;
this._revealTimer.setIfNotSet(() => {
this._domNode?.setClassName(this._visibleClassName);
}, 0);
}
private _hide(withFadeAway: boolean): void {
this._revealTimer.cancel();
if (!this._isVisible) {
return;
}
this._isVisible = false;
this._domNode?.setClassName(this._invisibleClassName + (withFadeAway ? ' xterm-fade' : ''));
}
}
+481
View File
@@ -0,0 +1,481 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DomUtils from '../Dom';
import { Disposable, IDisposable, toDisposable } from 'common/Lifecycle';
const mainWindow = (typeof window === 'object' ? window : globalThis) as Window & typeof globalThis;
function tail<T>(array: ArrayLike<T>, n: number = 0): T | undefined {
return array[array.length - (1 + n)];
}
function memoize(_target: any, key: string, descriptor: PropertyDescriptor): void {
let fnKey: string | null = null;
let fn: Function | null = null;
if (typeof descriptor.value === 'function') {
fnKey = 'value';
fn = descriptor.value;
if (fn!.length !== 0) {
console.warn('Memoize should only be used in functions with zero parameters');
}
} else if (typeof descriptor.get === 'function') {
fnKey = 'get';
fn = descriptor.get;
}
if (!fn || !fnKey) {
throw new Error('not supported');
}
const memoizeKey = `$memoize$${key}`;
const descriptorAny = descriptor as { [key: string]: any };
descriptorAny[fnKey] = function (...args: any[]) {
if (!this.hasOwnProperty(memoizeKey)) {
Object.defineProperty(this, memoizeKey, {
configurable: false,
enumerable: false,
writable: false,
value: fn.apply(this, args)
});
}
return (this as { [key: string]: any })[memoizeKey];
};
}
class LinkedListNode<E> {
public static readonly Undefined = new LinkedListNode<any>(undefined);
public element: E;
public next: LinkedListNode<E>;
public prev: LinkedListNode<E>;
public constructor(element: E) {
this.element = element;
this.next = LinkedListNode.Undefined;
this.prev = LinkedListNode.Undefined;
}
}
class LinkedList<E> {
private _first: LinkedListNode<E> = LinkedListNode.Undefined;
private _last: LinkedListNode<E> = LinkedListNode.Undefined;
public push(element: E): () => void {
return this._insert(element, true);
}
private _insert(element: E, atTheEnd: boolean): () => void {
const newNode = new LinkedListNode(element);
if (this._first === LinkedListNode.Undefined) {
this._first = newNode;
this._last = newNode;
} else if (atTheEnd) {
const oldLast = this._last;
this._last = newNode;
newNode.prev = oldLast;
oldLast.next = newNode;
} else {
const oldFirst = this._first;
this._first = newNode;
newNode.next = oldFirst;
oldFirst.prev = newNode;
}
let didRemove = false;
return () => {
if (!didRemove) {
didRemove = true;
this._remove(newNode);
}
};
}
private _remove(node: LinkedListNode<E>): void {
if (node.prev !== LinkedListNode.Undefined && node.next !== LinkedListNode.Undefined) {
const anchor = node.prev;
anchor.next = node.next;
node.next.prev = anchor;
} else if (node.prev === LinkedListNode.Undefined && node.next === LinkedListNode.Undefined) {
this._first = LinkedListNode.Undefined;
this._last = LinkedListNode.Undefined;
} else if (node.next === LinkedListNode.Undefined) {
this._last = this._last.prev!;
this._last.next = LinkedListNode.Undefined;
} else if (node.prev === LinkedListNode.Undefined) {
this._first = this._first.next!;
this._first.prev = LinkedListNode.Undefined;
}
}
public *[Symbol.iterator](): Iterator<E> {
let node = this._first;
while (node !== LinkedListNode.Undefined) {
yield node.element;
node = node.next;
}
}
}
export namespace EventType {
export const TAP = '-xterm-gesturetap';
export const CHANGE = '-xterm-gesturechange';
export const START = '-xterm-gesturestart';
export const END = '-xterm-gesturesend';
export const CONTEXT_MENU = '-xterm-gesturecontextmenu';
}
interface ITouchData {
id: number;
initialTarget: EventTarget;
initialTimeStamp: number;
initialPageX: number;
initialPageY: number;
rollingTimestamps: number[];
rollingPageX: number[];
rollingPageY: number[];
}
export interface IGestureEvent extends MouseEvent {
initialTarget: EventTarget | undefined;
translationX: number;
translationY: number;
pageX: number;
pageY: number;
tapCount: number;
}
interface ITouch {
identifier: number;
screenX: number;
screenY: number;
clientX: number;
clientY: number;
pageX: number;
pageY: number;
radiusX: number;
radiusY: number;
rotationAngle: number;
force: number;
target: Element;
}
interface ITouchList {
[i: number]: ITouch;
length: number;
item(index: number): ITouch;
identifiedTouch(id: number): ITouch;
}
interface ITouchEvent extends Event {
touches: ITouchList;
targetTouches: ITouchList;
changedTouches: ITouchList;
}
export class Gesture extends Disposable {
private static readonly _scrollFriction = -0.005;
private static _instance: Gesture;
private static readonly _holdDelay = 700;
private _dispatched = false;
private readonly _targets = new LinkedList<HTMLElement>();
private readonly _ignoreTargets = new LinkedList<HTMLElement>();
private _handle: IDisposable | null;
private readonly _activeTouches: { [id: number]: ITouchData };
private _lastSetTapCountTime: number;
private static readonly _clearTapCountTime = 400; // ms
private constructor() {
super();
this._activeTouches = {};
this._handle = null;
this._lastSetTapCountTime = 0;
const targetWindow = mainWindow;
this._register(DomUtils.addDisposableListener(targetWindow.document, 'touchstart', (e: ITouchEvent) => this._handleTouchStart(e), { passive: false }));
this._register(DomUtils.addDisposableListener(targetWindow.document, 'touchend', (e: ITouchEvent) => this._handleTouchEnd(targetWindow, e)));
this._register(DomUtils.addDisposableListener(targetWindow.document, 'touchmove', (e: ITouchEvent) => this._handleTouchMove(e), { passive: false }));
}
public static addTarget(element: HTMLElement): IDisposable {
if (!Gesture.isTouchDevice()) {
return Disposable.None;
}
if (!Gesture._instance) {
Gesture._instance = new Gesture();
}
const remove = Gesture._instance._targets.push(element);
return toDisposable(remove);
}
public static ignoreTarget(element: HTMLElement): IDisposable {
if (!Gesture.isTouchDevice()) {
return Disposable.None;
}
if (!Gesture._instance) {
Gesture._instance = new Gesture();
}
const remove = Gesture._instance._ignoreTargets.push(element);
return toDisposable(remove);
}
@memoize
public static isTouchDevice(): boolean {
return 'ontouchstart' in mainWindow || navigator.maxTouchPoints > 0;
}
public override dispose(): void {
if (this._handle) {
this._handle.dispose();
this._handle = null;
}
super.dispose();
}
private _handleTouchStart(e: ITouchEvent): void {
const timestamp = Date.now();
if (this._handle) {
this._handle.dispose();
this._handle = null;
}
for (let i = 0, len = e.targetTouches.length; i < len; i++) {
const touch = e.targetTouches.item(i);
this._activeTouches[touch.identifier] = {
id: touch.identifier,
initialTarget: touch.target,
initialTimeStamp: timestamp,
initialPageX: touch.pageX,
initialPageY: touch.pageY,
rollingTimestamps: [timestamp],
rollingPageX: [touch.pageX],
rollingPageY: [touch.pageY]
};
const evt = this._newGestureEvent(EventType.START, touch.target);
evt.pageX = touch.pageX;
evt.pageY = touch.pageY;
this._dispatchEvent(evt);
}
if (this._dispatched) {
e.preventDefault();
e.stopPropagation();
this._dispatched = false;
}
}
private _handleTouchEnd(targetWindow: Window, e: ITouchEvent): void {
const timestamp = Date.now();
const activeTouchCount = Object.keys(this._activeTouches).length;
for (let i = 0, len = e.changedTouches.length; i < len; i++) {
const touch = e.changedTouches.item(i);
if (!this._activeTouches.hasOwnProperty(String(touch.identifier))) {
console.warn('move of an UNKNOWN touch', touch);
continue;
}
const data = this._activeTouches[touch.identifier];
const holdTime = Date.now() - data.initialTimeStamp;
if (holdTime < Gesture._holdDelay
&& Math.abs(data.initialPageX - tail(data.rollingPageX)!) < 30
&& Math.abs(data.initialPageY - tail(data.rollingPageY)!) < 30) {
const evt = this._newGestureEvent(EventType.TAP, data.initialTarget);
evt.pageX = tail(data.rollingPageX)!;
evt.pageY = tail(data.rollingPageY)!;
this._dispatchEvent(evt);
} else if (holdTime >= Gesture._holdDelay
&& Math.abs(data.initialPageX - tail(data.rollingPageX)!) < 30
&& Math.abs(data.initialPageY - tail(data.rollingPageY)!) < 30) {
const evt = this._newGestureEvent(EventType.CONTEXT_MENU, data.initialTarget);
evt.pageX = tail(data.rollingPageX)!;
evt.pageY = tail(data.rollingPageY)!;
this._dispatchEvent(evt);
} else if (activeTouchCount === 1) {
const finalX = tail(data.rollingPageX)!;
const finalY = tail(data.rollingPageY)!;
const deltaT = tail(data.rollingTimestamps)! - data.rollingTimestamps[0];
const deltaX = finalX - data.rollingPageX[0];
const deltaY = finalY - data.rollingPageY[0];
const dispatchTo = [...this._targets].filter(t => data.initialTarget instanceof Node && t.contains(data.initialTarget));
this._inertia(targetWindow, dispatchTo, timestamp,
Math.abs(deltaX) / deltaT,
deltaX > 0 ? 1 : -1,
finalX,
Math.abs(deltaY) / deltaT,
deltaY > 0 ? 1 : -1,
finalY
);
}
this._dispatchEvent(this._newGestureEvent(EventType.END, data.initialTarget));
delete this._activeTouches[touch.identifier];
}
if (this._dispatched) {
e.preventDefault();
e.stopPropagation();
this._dispatched = false;
}
}
private _newGestureEvent(type: string, initialTarget?: EventTarget): IGestureEvent {
const event = document.createEvent('CustomEvent') as unknown as IGestureEvent;
event.initEvent(type, false, true);
event.initialTarget = initialTarget;
event.tapCount = 0;
return event;
}
private _dispatchEvent(event: IGestureEvent): void {
if (event.type === EventType.TAP) {
const currentTime = (new Date()).getTime();
let setTapCount = 0;
if (currentTime - this._lastSetTapCountTime > Gesture._clearTapCountTime) {
setTapCount = 1;
} else {
setTapCount = 2;
}
this._lastSetTapCountTime = currentTime;
event.tapCount = setTapCount;
} else if (event.type === EventType.CHANGE || event.type === EventType.CONTEXT_MENU) {
this._lastSetTapCountTime = 0;
}
if (event.initialTarget instanceof Node) {
for (const ignoreTarget of this._ignoreTargets) {
if (ignoreTarget.contains(event.initialTarget)) {
return;
}
}
const targets: [number, HTMLElement][] = [];
for (const target of this._targets) {
if (target.contains(event.initialTarget)) {
let depth = 0;
let now: Node | null = event.initialTarget;
while (now && now !== target) {
depth++;
now = now.parentElement;
}
targets.push([depth, target]);
}
}
targets.sort((a, b) => a[0] - b[0]);
for (const [, target] of targets) {
target.dispatchEvent(event);
this._dispatched = true;
}
}
}
private _inertia(targetWindow: Window, dispatchTo: ReadonlyArray<EventTarget>, t1: number, vX: number, dirX: number, x: number, vY: number, dirY: number, y: number): void {
this._handle = DomUtils.scheduleAtNextAnimationFrame(targetWindow, () => {
const now = Date.now();
const deltaT = now - t1;
let deltaPosX = 0;
let deltaPosY = 0;
let stopped = true;
vX += Gesture._scrollFriction * deltaT;
vY += Gesture._scrollFriction * deltaT;
if (vX > 0) {
stopped = false;
deltaPosX = dirX * vX * deltaT;
}
if (vY > 0) {
stopped = false;
deltaPosY = dirY * vY * deltaT;
}
const evt = this._newGestureEvent(EventType.CHANGE);
evt.translationX = deltaPosX;
evt.translationY = deltaPosY;
dispatchTo.forEach(d => d.dispatchEvent(evt));
if (!stopped) {
this._inertia(targetWindow, dispatchTo, now, vX, dirX, x + deltaPosX, vY, dirY, y + deltaPosY);
}
});
}
private _handleTouchMove(e: ITouchEvent): void {
const timestamp = Date.now();
for (let i = 0, len = e.changedTouches.length; i < len; i++) {
const touch = e.changedTouches.item(i);
if (!this._activeTouches.hasOwnProperty(String(touch.identifier))) {
console.warn('end of an UNKNOWN touch', touch);
continue;
}
const data = this._activeTouches[touch.identifier];
const evt = this._newGestureEvent(EventType.CHANGE, data.initialTarget);
evt.translationX = touch.pageX - tail(data.rollingPageX)!;
evt.translationY = touch.pageY - tail(data.rollingPageY)!;
evt.pageX = touch.pageX;
evt.pageY = touch.pageY;
this._dispatchEvent(evt);
if (data.rollingPageX.length > 3) {
data.rollingPageX.shift();
data.rollingPageY.shift();
data.rollingTimestamps.shift();
}
data.rollingPageX.push(touch.pageX);
data.rollingPageY.push(touch.pageY);
data.rollingTimestamps.push(timestamp);
}
if (this._dispatched) {
e.preventDefault();
e.stopPropagation();
this._dispatched = false;
}
}
}
+143
View File
@@ -0,0 +1,143 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AbstractScrollbar, ISimplifiedPointerEvent, IScrollbarHost } from './abstractScrollbar';
import { IScrollableElementResolvedOptions } from './scrollableElementOptions';
import { ScrollbarState } from './scrollbarState';
import { INewScrollPosition, Scrollable, ScrollbarVisibility, IScrollEvent } from './scrollable';
import type { ScrollbarArrow } from './scrollbarArrow';
export class VerticalScrollbar extends AbstractScrollbar {
private _arrowUp: ScrollbarArrow | undefined;
private _arrowDown: ScrollbarArrow | undefined;
private _arrowScrollDelta: number = 0;
constructor(scrollable: Scrollable, options: IScrollableElementResolvedOptions, host: IScrollbarHost) {
const scrollDimensions = scrollable.getScrollDimensions();
const scrollPosition = scrollable.getCurrentScrollPosition();
const hasArrows = options.verticalHasArrows;
super({
lazyRender: options.lazyRender,
host: host,
scrollbarState: new ScrollbarState(
(hasArrows ? options.verticalScrollbarSize : 0),
(options.vertical === ScrollbarVisibility.HIDDEN ? 0 : options.verticalScrollbarSize),
0,
scrollDimensions.height,
scrollDimensions.scrollHeight,
scrollPosition.scrollTop
),
visibility: options.vertical,
extraScrollbarClassName: 'xterm-vertical',
scrollable: scrollable,
scrollByPage: options.scrollByPage
});
this._setArrows(hasArrows, options.verticalScrollbarSize);
this._createSlider(0, Math.floor((options.verticalScrollbarSize - options.verticalSliderSize) / 2), options.verticalSliderSize, undefined);
}
protected _updateSlider(sliderSize: number, sliderPosition: number): void {
this.slider.setHeight(sliderSize);
this.slider.setTop(sliderPosition);
}
protected _renderDomNode(largeSize: number, smallSize: number): void {
this.domNode.setWidth(smallSize);
this.domNode.setHeight(largeSize);
this.domNode.setRight(0);
this.domNode.setTop(0);
}
public handleScroll(e: IScrollEvent): boolean {
this._shouldRender = this._handleElementScrollSize(e.scrollHeight) || this._shouldRender;
this._shouldRender = this._handleElementScrollPosition(e.scrollTop) || this._shouldRender;
this._shouldRender = this._handleElementSize(e.height) || this._shouldRender;
return this._shouldRender;
}
protected _pointerDownRelativePosition(offsetX: number, offsetY: number): number {
return offsetY;
}
protected _sliderPointerPosition(e: ISimplifiedPointerEvent): number {
return e.pageY;
}
protected _sliderOrthogonalPointerPosition(e: ISimplifiedPointerEvent): number {
return e.pageX;
}
protected _updateScrollbarSize(size: number): void {
this.slider.setWidth(size);
}
public writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void {
target.scrollTop = scrollPosition;
}
private _arrowScroll(delta: number): void {
const currentPosition = this._scrollable.getCurrentScrollPosition();
this._scrollable.setScrollPositionNow({ scrollTop: currentPosition.scrollTop + delta });
}
private _setArrows(showArrows: boolean, size: number): void {
this._arrowScrollDelta = size;
if (!this._arrowUp || !this._arrowDown) {
const arrowDelta = 0;
this._arrowUp = this._createArrow({
className: 'xterm-scra xterm-arrow-up',
top: arrowDelta,
left: arrowDelta,
bgWidth: size,
bgHeight: size,
handleActivate: () => this._arrowScroll(-this._arrowScrollDelta)
});
this._arrowDown = this._createArrow({
className: 'xterm-scra xterm-arrow-down',
bottom: arrowDelta,
left: arrowDelta,
bgWidth: size,
bgHeight: size,
handleActivate: () => this._arrowScroll(this._arrowScrollDelta)
});
}
this._updateArrowSize(this._arrowUp, size);
this._updateArrowSize(this._arrowDown, size);
if (!this._arrowUp || !this._arrowDown) {
return;
}
const display = showArrows ? '' : 'none';
this._arrowUp.bgDomNode.style.display = display;
this._arrowUp.domNode.style.display = display;
this._arrowDown.bgDomNode.style.display = display;
this._arrowDown.domNode.style.display = display;
}
private _updateArrowSize(arrow: ScrollbarArrow | undefined, size: number): void {
if (!arrow) {
return;
}
arrow.bgDomNode.style.width = `${size}px`;
arrow.bgDomNode.style.height = `${size}px`;
arrow.domNode.style.width = `${size}px`;
arrow.domNode.style.height = `${size}px`;
}
public updateOptions(options: IScrollableElementResolvedOptions): void {
const arrowSize = options.verticalHasArrows ? options.verticalScrollbarSize : 0;
this._scrollbarState.setArrowSize(arrowSize);
this._setArrows(options.verticalHasArrows, options.verticalScrollbarSize);
this.updateScrollbarSize(options.vertical === ScrollbarVisibility.HIDDEN ? 0 : options.verticalScrollbarSize);
this._scrollbarState.setOppositeScrollbarSize(0);
this._visibilityController.setVisibility(options.vertical);
this._scrollByPage = options.scrollByPage;
}
}
+23
View File
@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from '../Dom';
import { IMouseEvent, StandardMouseEvent } from './mouseEvent';
import { Disposable } from 'common/Lifecycle';
export abstract class Widget extends Disposable {
protected _onclick(domNode: HTMLElement, listener: (e: IMouseEvent) => void): void {
this._register(dom.addDisposableListener(domNode, dom.eventType.CLICK, (e: MouseEvent) => listener(new StandardMouseEvent(dom.getWindow(domNode), e))));
}
protected _onmouseover(domNode: HTMLElement, listener: (e: IMouseEvent) => void): void {
this._register(dom.addDisposableListener(domNode, dom.eventType.MOUSE_OVER, (e: MouseEvent) => listener(new StandardMouseEvent(dom.getWindow(domNode), e))));
}
protected _onmouseleave(domNode: HTMLElement, listener: (e: IMouseEvent) => void): void {
this._register(dom.addDisposableListener(domNode, dom.eventType.MOUSE_LEAVE, (e: MouseEvent) => listener(new StandardMouseEvent(dom.getWindow(domNode), e))));
}
}
+1 -1
View File
@@ -5,7 +5,7 @@
import { ICoreBrowserService } from './Services';
import { Emitter, EventUtils } from 'common/Event';
import { addDisposableListener } from 'vs/base/browser/dom';
import { addDisposableListener } from 'browser/Dom';
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
export class CoreBrowserService extends Disposable implements ICoreBrowserService {
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { getWindow } from 'vs/base/browser/dom';
import { getWindow } from 'browser/Dom';
import { ICharSizeService, IRenderService, IMouseService } from './Services';
import { getCoords, getCoordsRelativeToElement } from 'browser/input/Mouse';
+6 -2
View File
@@ -9,7 +9,7 @@ import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
import { DebouncedIdleTask } from 'common/TaskQueue';
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { IBufferService, ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services';
import { Emitter } from 'common/Event';
interface ISelectionState {
@@ -27,7 +27,7 @@ export class RenderService extends Disposable implements IRenderService {
private _renderer: MutableDisposable<IRenderer> = this._register(new MutableDisposable());
private _renderDebouncer: IRenderDebouncerWithCallback;
private _pausedResizeTask = new DebouncedIdleTask();
private _pausedResizeTask: DebouncedIdleTask;
private _observerDisposable = this._register(new MutableDisposable());
private _isPaused: boolean = false;
@@ -58,6 +58,7 @@ export class RenderService extends Disposable implements IRenderService {
private _rowCount: number,
screenElement: HTMLElement,
@IOptionsService private readonly _optionsService: IOptionsService,
@ILogService private readonly _logService: ILogService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@ICoreService private readonly _coreService: ICoreService,
@IDecorationService decorationService: IDecorationService,
@@ -67,6 +68,8 @@ export class RenderService extends Disposable implements IRenderService {
) {
super();
this._pausedResizeTask = new DebouncedIdleTask(this._logService);
this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end), this._coreBrowserService);
this._register(this._renderDebouncer);
@@ -131,6 +134,7 @@ export class RenderService extends Disposable implements IRenderService {
private _handleIntersectionChange(entry: IntersectionObserverEntry): void {
this._isPaused = entry.isIntersecting === undefined ? (entry.intersectionRatio === 0) : !entry.isIntersecting;
this._renderer.value?.handleViewportVisibilityChange?.(!this._isPaused);
// Terminal was hidden on open
if (!this._isPaused && !this._charSizeService.hasValidSize) {
+2 -2
View File
@@ -11,7 +11,7 @@ import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from
import { ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
import { Disposable, toDisposable } from 'common/Lifecycle';
import * as Browser from 'common/Platform';
import { IBufferLine, IDisposable } from 'common/Types';
import { IBufferLine, ICellData, IDisposable } from 'common/Types';
import { getRangeLength } from 'common/buffer/BufferRange';
import { CellData } from 'common/buffer/CellData';
import { IBuffer } from 'common/buffer/Types';
@@ -1021,7 +1021,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* word logic.
* @param cell The cell to check.
*/
private _isCharWordSeparator(cell: CellData): boolean {
private _isCharWordSeparator(cell: ICellData): boolean {
// Zero width characters are never separators as they are always to the
// right of wide characters
if (cell.getWidth() === 0) {
+2 -4
View File
@@ -11,8 +11,7 @@
],
"baseUrl": "..",
"paths": {
"common/*": [ "./common/*" ],
"vs/*": [ "./vs/*" ]
"common/*": [ "./common/*" ]
}
},
"include": [
@@ -20,7 +19,6 @@
"../../typings/xterm.d.ts"
],
"references": [
{ "path": "../common" },
{ "path": "../vs" }
{ "path": "../common" }
]
}
+105
View File
@@ -0,0 +1,105 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*
* Minimal async helpers for xterm.js core.
*/
import { DisposableStore, IDisposable, toDisposable } from 'common/Lifecycle';
export function timeout(millis: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, millis));
}
/**
* Creates a timeout that can be disposed using its returned value.
* @param handler The timeout handler.
* @param timeout An optional timeout in milliseconds.
* @param store An optional {@link DisposableStore} that will have the timeout disposable managed
* automatically.
*/
export function disposableTimeout(handler: () => void, timeout = 0, store?: DisposableStore): IDisposable {
const timer = setTimeout(() => {
handler();
if (store) {
disposable.dispose();
}
}, timeout);
const disposable = toDisposable(() => {
clearTimeout(timer);
});
store?.add(disposable);
return disposable;
}
export class TimeoutTimer implements IDisposable {
private _token: any = -1;
private _isDisposed = false;
public dispose(): void {
this.cancel();
this._isDisposed = true;
}
public cancel(): void {
if (this._token !== -1) {
clearTimeout(this._token);
this._token = -1;
}
}
public cancelAndSet(runner: () => void, timeout: number): void {
if (this._isDisposed) {
throw new Error('Calling cancelAndSet on a disposed TimeoutTimer');
}
this.cancel();
this._token = setTimeout(() => {
this._token = -1;
runner();
}, timeout);
}
public setIfNotSet(runner: () => void, timeout: number): void {
if (this._isDisposed) {
throw new Error('Calling setIfNotSet on a disposed TimeoutTimer');
}
if (this._token !== -1) {
return;
}
this._token = setTimeout(() => {
this._token = -1;
runner();
}, timeout);
}
}
export class IntervalTimer implements IDisposable {
private _disposable: IDisposable | undefined;
private _isDisposed = false;
public cancel(): void {
this._disposable?.dispose();
this._disposable = undefined;
}
public cancelAndSet(runner: () => void, interval: number, context: Window | typeof globalThis = globalThis): void {
if (this._isDisposed) {
throw new Error('Calling cancelAndSet on a disposed IntervalTimer');
}
this.cancel();
const handle = context.setInterval(() => {
runner();
}, interval);
this._disposable = {
dispose: () => {
context.clearInterval(handle as any);
this._disposable = undefined;
}
};
}
public dispose(): void {
this.cancel();
this._isDisposed = true;
}
}
+2 -2
View File
@@ -107,10 +107,10 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this._instantiationService = new InstantiationService();
this.optionsService = this._register(new OptionsService(options));
this._instantiationService.setService(IOptionsService, this.optionsService);
this._bufferService = this._register(this._instantiationService.createInstance(BufferService));
this._instantiationService.setService(IBufferService, this._bufferService);
this._logService = this._register(this._instantiationService.createInstance(LogService));
this._instantiationService.setService(ILogService, this._logService);
this._bufferService = this._register(this._instantiationService.createInstance(BufferService));
this._instantiationService.setService(IBufferService, this._bufferService);
this.coreService = this._register(this._instantiationService.createInstance(CoreService));
this._instantiationService.setService(ICoreService, this.coreService);
this.coreMouseService = this._register(this._instantiationService.createInstance(CoreMouseService));
+4 -2
View File
@@ -109,8 +109,10 @@ export namespace EventUtils {
};
}
export function runAndSubscribe<T>(event: IEvent<T>, handler: (e: T | undefined) => void): IDisposable {
handler(undefined);
export function runAndSubscribe<T>(event: IEvent<T>, handler: (e: T) => void, initial: T): IDisposable;
export function runAndSubscribe<T>(event: IEvent<T>, handler: (e: T | undefined) => void): IDisposable;
export function runAndSubscribe<T>(event: IEvent<T>, handler: (e: T | undefined) => void, initial?: T): IDisposable {
handler(initial);
return event(e => handler(e));
}
}
+3 -3
View File
@@ -65,7 +65,7 @@ describe('InputHandler', () => {
beforeEach(() => {
optionsService = new MockOptionsService();
bufferService = new BufferService(optionsService);
bufferService = new BufferService(optionsService, new MockLogService());
bufferService.resize(80, 30);
coreService = new CoreService(bufferService, new MockLogService(), optionsService);
oscLinkService = new OscLinkService(bufferService);
@@ -2458,7 +2458,7 @@ describe('InputHandler', () => {
beforeEach(() => {
optionsService = new MockOptionsService({ vtExtensions: { kittyKeyboard: true } });
bufferService = new BufferService(optionsService);
bufferService = new BufferService(optionsService, new MockLogService());
bufferService.resize(80, 30);
coreService = new CoreService(bufferService, new MockLogService(), optionsService);
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
@@ -2508,7 +2508,7 @@ describe('InputHandler', () => {
beforeEach(() => {
optionsService = new MockOptionsService();
bufferService = new BufferService(optionsService);
bufferService = new BufferService(optionsService, new MockLogService());
bufferService.resize(80, 30);
coreService = new CoreService(bufferService, new MockLogService(), optionsService);
coreService.onData(data => { console.log(data); });
+20 -7
View File
@@ -14,17 +14,21 @@ export function toDisposable(fn: () => void): IDisposable {
return { dispose: fn };
}
export function dispose<T extends IDisposable>(disposables: T | T[] | undefined): void {
if (!disposables) {
return;
export function dispose<T extends IDisposable>(disposable: T): T;
export function dispose<T extends IDisposable>(disposable: T | undefined): T | undefined;
export function dispose<T extends IDisposable>(disposables: T[]): T[];
export function dispose<T extends IDisposable>(arg: T | T[] | undefined): T | T[] | undefined {
if (!arg) {
return arg;
}
if (Array.isArray(disposables)) {
for (const d of disposables) {
if (Array.isArray(arg)) {
for (const d of arg) {
d.dispose();
}
} else {
disposables.dispose();
return [];
}
arg.dispose();
return arg;
}
export function combinedDisposable(...disposables: IDisposable[]): IDisposable {
@@ -58,9 +62,18 @@ export class DisposableStore implements IDisposable {
}
this._disposables.clear();
}
public clear(): void {
for (const d of this._disposables) {
d.dispose();
}
this._disposables.clear();
}
}
export abstract class Disposable implements IDisposable {
public static readonly None: IDisposable = Object.freeze({ dispose() { } });
protected readonly _store = new DisposableStore();
public dispose(): void {
+9
View File
@@ -22,8 +22,17 @@ const userAgent = (isNode) ? 'node' : navigator.userAgent;
const platform = (isNode) ? 'node' : navigator.platform;
export const isFirefox = userAgent.includes('Firefox');
export const isChrome = userAgent.includes('Chrome');
export const isLegacyEdge = userAgent.includes('Edge');
export const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);
interface IZoomWindow {
devicePixelRatio?: number;
}
export function getZoomFactor(_targetWindow: IZoomWindow): number {
return 1;
}
export function getSafariVersion(): number {
if (!isSafari) {
return 0;
+3 -2
View File
@@ -5,6 +5,7 @@
import { assert } from 'chai';
import { SortedList } from 'common/SortedList';
import { MockLogService } from 'common/TestUtils.test';
const deepStrictEqual = assert.deepStrictEqual;
@@ -15,7 +16,7 @@ describe('SortedList', () => {
}
beforeEach(() => {
list = new SortedList<number>(e => e);
list = new SortedList<number>(e => e, new MockLogService());
});
describe('insert', () => {
@@ -90,7 +91,7 @@ describe('SortedList', () => {
assertList([]);
});
it('custom key', () => {
const customList = new SortedList<{ key: number }>(e => e.key);
const customList = new SortedList<{ key: number }>(e => e.key, new MockLogService());
customList.insert({ key: 5 });
customList.insert({ key: 2 });
customList.insert({ key: 10 });
+7 -3
View File
@@ -4,6 +4,7 @@
*/
import { IdleTaskQueue } from 'common/TaskQueue';
import type { ILogService } from 'common/services/Services';
// Work variables to avoid garbage collection.
let i = 0;
@@ -18,16 +19,19 @@ export class SortedList<T> {
private _array: T[] = [];
private readonly _insertedValues: T[] = [];
private readonly _flushInsertedTask = new IdleTaskQueue();
private readonly _flushInsertedTask: InstanceType<typeof IdleTaskQueue>;
private _isFlushingInserted = false;
private readonly _deletedIndices: number[] = [];
private readonly _flushDeletedTask = new IdleTaskQueue();
private readonly _flushDeletedTask: InstanceType<typeof IdleTaskQueue>;
private _isFlushingDeleted = false;
constructor(
private readonly _getKey: (value: T) => number
private readonly _getKey: (value: T) => number,
logService: ILogService
) {
this._flushInsertedTask = new IdleTaskQueue(logService);
this._flushDeletedTask = new IdleTaskQueue(logService);
}
public clear(): void {
+9 -3
View File
@@ -4,6 +4,7 @@
*/
import { isNode } from 'common/Platform';
import type { ILogService } from 'common/services/Services';
interface ITaskQueue {
/**
@@ -34,6 +35,11 @@ abstract class TaskQueue implements ITaskQueue {
private _tasks: (() => boolean | void)[] = [];
private _idleCallback?: number;
private _i = 0;
protected readonly _logService: ILogService;
constructor(logService: ILogService) {
this._logService = logService;
}
protected abstract _requestCallback(callback: CallbackWithDeadline): number;
protected abstract _cancelCallback(identifier: number): void;
@@ -90,7 +96,7 @@ abstract class TaskQueue implements ITaskQueue {
// Warn when the time exceeding the deadline is over 20ms, if this happens in practice the
// task should be split into sub-tasks to ensure the UI remains responsive.
if (lastDeadlineRemaining - taskDuration < -20) {
console.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(lastDeadlineRemaining - taskDuration))}ms`);
this._logService.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(lastDeadlineRemaining - taskDuration))}ms`);
}
this._start();
return;
@@ -151,8 +157,8 @@ export const IdleTaskQueue = (!isNode && 'requestIdleCallback' in window) ? Idle
export class DebouncedIdleTask {
private _queue: ITaskQueue;
constructor() {
this._queue = new IdleTaskQueue();
constructor(logService: ILogService) {
this._queue = new IdleTaskQueue(logService);
}
public set(task: () => boolean | void): void {
+1 -1
View File
@@ -27,7 +27,7 @@ export class MockBufferService implements IBufferService {
public rows: number,
optionsService: IOptionsService = new MockOptionsService()
) {
this.buffers = new BufferSet(optionsService, this);
this.buffers = new BufferSet(optionsService, this, new MockLogService());
// Listen to buffer activation events and automatically fire scroll events
this.buffers.onBufferActivate(e => {
this._onScroll.fire(e.activeBuffer.ydisp);
+7 -7
View File
@@ -6,7 +6,7 @@
import { assert } from 'chai';
import { Buffer } from 'common/buffer/Buffer';
import { CircularList } from 'common/CircularList';
import { MockOptionsService, MockBufferService } from 'common/TestUtils.test';
import { MockOptionsService, MockBufferService, MockLogService } from 'common/TestUtils.test';
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { CellData } from 'common/buffer/CellData';
import { ExtendedAttrs } from 'common/buffer/AttributeData';
@@ -23,7 +23,7 @@ describe('Buffer', () => {
beforeEach(() => {
optionsService = new MockOptionsService({ scrollback: INIT_SCROLLBACK });
bufferService = new MockBufferService(INIT_COLS, INIT_ROWS);
buffer = new Buffer(true, optionsService, bufferService);
buffer = new Buffer(true, optionsService, bufferService, new MockLogService());
});
describe('constructor', () => {
@@ -151,7 +151,7 @@ describe('Buffer', () => {
describe('no scrollback', () => {
it('should trim from the top of the buffer when the cursor reaches the bottom', () => {
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService);
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService, new MockLogService());
assert.equal(buffer.lines.maxLength, INIT_ROWS);
buffer.y = INIT_ROWS - 1;
buffer.fillViewportRows();
@@ -1054,7 +1054,7 @@ describe('Buffer', () => {
describe('buffer marked to have no scrollback', () => {
it('should always have a scrollback of 0', () => {
// Test size on initialization
buffer = new Buffer(false, new MockOptionsService({ scrollback: 1000 }), bufferService);
buffer = new Buffer(false, new MockOptionsService({ scrollback: 1000 }), bufferService, new MockLogService());
buffer.fillViewportRows();
assert.equal(buffer.lines.maxLength, INIT_ROWS);
// Test size on buffer increase
@@ -1068,7 +1068,7 @@ describe('Buffer', () => {
describe('addMarker', () => {
it('should adjust a marker line when the buffer is trimmed', () => {
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService);
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService, new MockLogService());
buffer.fillViewportRows();
const marker = buffer.addMarker(buffer.lines.length - 1);
assert.equal(marker.line, buffer.lines.length - 1);
@@ -1076,7 +1076,7 @@ describe('Buffer', () => {
assert.equal(marker.line, buffer.lines.length - 2);
});
it('should dispose of a marker if it is trimmed off the buffer', () => {
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService);
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService, new MockLogService());
buffer.fillViewportRows();
assert.equal(buffer.markers.length, 0);
const marker = buffer.addMarker(0);
@@ -1088,7 +1088,7 @@ describe('Buffer', () => {
});
it('should call onDispose', () => {
const eventStack: string[] = [];
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService);
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService, new MockLogService());
buffer.fillViewportRows();
assert.equal(buffer.markers.length, 0);
const marker = buffer.addMarker(0);
+6 -5
View File
@@ -14,7 +14,7 @@ import { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR,
import { Marker } from 'common/buffer/Marker';
import { IBuffer } from 'common/buffer/Types';
import { DEFAULT_CHARSET } from 'common/data/Charsets';
import { IBufferService, IOptionsService } from 'common/services/Services';
import { IBufferService, ILogService, IOptionsService } from 'common/services/Services';
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
@@ -48,11 +48,14 @@ export class Buffer implements IBuffer {
private _cols: number;
private _rows: number;
private _isClearing: boolean = false;
private _memoryCleanupQueue: InstanceType<typeof IdleTaskQueue>;
private _memoryCleanupPosition = 0;
constructor(
private _hasScrollback: boolean,
private _optionsService: IOptionsService,
private _bufferService: IBufferService
private _bufferService: IBufferService,
private readonly _logService: ILogService
) {
this._cols = this._bufferService.cols;
this._rows = this._bufferService.rows;
@@ -60,6 +63,7 @@ export class Buffer implements IBuffer {
this.scrollTop = 0;
this.scrollBottom = this._rows - 1;
this.setupTabStops();
this._memoryCleanupQueue = new IdleTaskQueue(this._logService);
}
public getNullCell(attr?: IAttributeData): ICellData {
@@ -277,9 +281,6 @@ export class Buffer implements IBuffer {
}
}
private _memoryCleanupQueue = new IdleTaskQueue();
private _memoryCleanupPosition = 0;
private _batchedMemoryCleanup(): boolean {
let normalRun = true;
if (this._memoryCleanupPosition >= this.lines.length) {
+3 -2
View File
@@ -6,7 +6,7 @@
import { assert } from 'chai';
import { BufferSet } from 'common/buffer/BufferSet';
import { Buffer } from 'common/buffer/Buffer';
import { MockOptionsService, MockBufferService } from 'common/TestUtils.test';
import { MockOptionsService, MockBufferService, MockLogService } from 'common/TestUtils.test';
describe('BufferSet', () => {
let bufferSet: BufferSet;
@@ -14,7 +14,8 @@ describe('BufferSet', () => {
beforeEach(() => {
bufferSet = new BufferSet(
new MockOptionsService({ scrollback: 1000 }),
new MockBufferService(80, 24)
new MockBufferService(80, 24),
new MockLogService()
);
});
+5 -4
View File
@@ -7,7 +7,7 @@ import { Disposable } from 'common/Lifecycle';
import { IAttributeData } from 'common/Types';
import { Buffer } from 'common/buffer/Buffer';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { IBufferService, IOptionsService } from 'common/services/Services';
import { IBufferService, ILogService, IOptionsService } from 'common/services/Services';
import { Emitter } from 'common/Event';
/**
@@ -27,7 +27,8 @@ export class BufferSet extends Disposable implements IBufferSet {
*/
constructor(
private readonly _optionsService: IOptionsService,
private readonly _bufferService: IBufferService
private readonly _bufferService: IBufferService,
private readonly _logService: ILogService
) {
super();
this.reset();
@@ -36,12 +37,12 @@ export class BufferSet extends Disposable implements IBufferSet {
}
public reset(): void {
this._normal = new Buffer(true, this._optionsService, this._bufferService);
this._normal = new Buffer(true, this._optionsService, this._bufferService, this._logService);
this._normal.fillViewportRows();
// The alt buffer should never have scrollback.
// See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
this._alt = new Buffer(false, this._optionsService, this._bufferService);
this._alt = new Buffer(false, this._optionsService, this._bufferService, this._logService);
this._activeBuffer = this._normal;
this._onBufferActivate.fire({
activeBuffer: this._normal,
+65
View File
@@ -0,0 +1,65 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Attributes, BgFlags, FgFlags, UnderlineStyle } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { assert } from 'chai';
function createStyledCell(char: string, underlineStyle: UnderlineStyle, underlineColor: number): CellData {
const cell = new CellData();
const fg = Attributes.CM_P256 | 12 | FgFlags.BOLD | FgFlags.UNDERLINE;
cell.setFromCharData([fg, char, 1, char.charCodeAt(0)]);
cell.bg = Attributes.CM_P16 | 2 | BgFlags.ITALIC;
cell.extended.underlineStyle = underlineStyle;
cell.extended.underlineColor = Attributes.CM_P256 | underlineColor;
cell.updateExtended();
return cell;
}
describe('CellData', () => {
describe('attributesEquals', () => {
it('returns true for same attributes with different chars', () => {
const cellA = createStyledCell('A', UnderlineStyle.DOUBLE, 45);
const cellB = createStyledCell('B', UnderlineStyle.DOUBLE, 45);
assert.equal(cellA.attributesEquals(cellB), true);
});
it('detects underline style changes', () => {
const cellA = createStyledCell('A', UnderlineStyle.DOUBLE, 45);
const cellB = createStyledCell('B', UnderlineStyle.SINGLE, 45);
assert.equal(cellA.attributesEquals(cellB), false);
});
it('detects underline color changes', () => {
const cellA = createStyledCell('A', UnderlineStyle.SINGLE, 45);
const cellB = createStyledCell('B', UnderlineStyle.SINGLE, 46);
assert.equal(cellA.attributesEquals(cellB), false);
});
it('ignores underline variant offsets', () => {
const cellA = createStyledCell('A', UnderlineStyle.SINGLE, 45);
const cellB = createStyledCell('B', UnderlineStyle.SINGLE, 45);
cellA.extended.underlineVariantOffset = 1;
cellB.extended.underlineVariantOffset = 3;
cellA.updateExtended();
cellB.updateExtended();
assert.equal(cellA.attributesEquals(cellB), true);
});
it('ignores url ids', () => {
const cellA = createStyledCell('A', UnderlineStyle.SINGLE, 45);
const cellB = createStyledCell('B', UnderlineStyle.SINGLE, 45);
cellA.extended.urlId = 1;
cellB.extended.urlId = 2;
cellA.updateExtended();
cellB.updateExtended();
assert.equal(cellA.attributesEquals(cellB), true);
});
});
});
+57
View File
@@ -7,6 +7,7 @@ import { CharData, ICellData, IExtendedAttrs } from 'common/Types';
import { stringFromCodePoint } from 'common/input/TextDecoder';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, Content } from 'common/buffer/Constants';
import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData';
import type { IBufferCell as IBufferCellApi } from '@xterm/xterm';
/**
* CellData - represents a single Cell in the terminal buffer.
@@ -91,4 +92,60 @@ export class CellData extends AttributeData implements ICellData {
public getAsCharData(): CharData {
return [this.fg, this.getChars(), this.getWidth(), this.getCode()];
}
public attributesEquals(other: IBufferCellApi): boolean {
if (this.getFgColorMode() !== other.getFgColorMode() || this.getFgColor() !== other.getFgColor()) {
return false;
}
if (this.getBgColorMode() !== other.getBgColorMode() || this.getBgColor() !== other.getBgColor()) {
return false;
}
if (this.isInverse() !== other.isInverse()) {
return false;
}
if (this.isBold() !== other.isBold()) {
return false;
}
if (this.isUnderline() !== other.isUnderline()) {
return false;
}
if (this.isUnderline()) {
if (this.getUnderlineStyle() !== other.getUnderlineStyle()) {
return false;
}
const thisDefault = this.isUnderlineColorDefault();
const otherDefault = other.isUnderlineColorDefault();
if (!(thisDefault && otherDefault)) {
if (thisDefault !== otherDefault) {
return false;
}
if (this.getUnderlineColor() !== other.getUnderlineColor()) {
return false;
}
if (this.getUnderlineColorMode() !== other.getUnderlineColorMode()) {
return false;
}
}
}
if (this.isOverline() !== other.isOverline()) {
return false;
}
if (this.isBlink() !== other.isBlink()) {
return false;
}
if (this.isInvisible() !== other.isInvisible()) {
return false;
}
if (this.isItalic() !== other.isItalic()) {
return false;
}
if (this.isDim() !== other.isDim()) {
return false;
}
if (this.isStrikethrough() !== other.isStrikethrough()) {
return false;
}
return true;
}
}
+15 -1
View File
@@ -474,11 +474,25 @@ describe('KittyKeyboard', () => {
});
it('modifier key release includes its own bit cleared', () => {
const result = kitty.evaluate(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: false }), flags, KittyKeyboardEventType.RELEASE);
const result = kitty.evaluate(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: false }), flags | KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES, KittyKeyboardEventType.RELEASE);
assert.strictEqual(result.key, '\x1b[57441;1:3u');
});
});
describe('modifier-only reporting', () => {
const flags = KittyKeyboardFlags.REPORT_EVENT_TYPES;
it('does not report modifier press without REPORT_ALL_KEYS_AS_ESCAPE_CODES', () => {
const result = kitty.evaluate(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: true }), flags);
assert.strictEqual(result.key, undefined);
});
it('does not report modifier release without REPORT_ALL_KEYS_AS_ESCAPE_CODES', () => {
const result = kitty.evaluate(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: false }), flags, KittyKeyboardEventType.RELEASE);
assert.strictEqual(result.key, undefined);
});
});
describe('REPORT_ALL_KEYS_AS_ESCAPE_CODES flag', () => {
const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES;
+1 -1
View File
@@ -418,7 +418,7 @@ export class KittyKeyboard {
return result;
}
if (isMod && !(flags & KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES) && !reportEventTypes) {
if (isMod && !(flags & KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES)) {
return result;
}
+2 -2
View File
@@ -18,10 +18,10 @@ export class BufferLineApiView implements IBufferLineApi {
}
if (cell) {
this._line.loadCell(x, cell as ICellData);
this._line.loadCell(x, cell as unknown as ICellData);
return cell;
}
return this._line.loadCell(x, new CellData());
return this._line.loadCell(x, new CellData()) as unknown as IBufferCellApi;
}
public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string {
return this._line.translateToString(trimRight, startColumn, endColumn);
+6 -3
View File
@@ -7,7 +7,7 @@ import { Disposable } from 'common/Lifecycle';
import { IAttributeData, IBufferLine } from 'common/Types';
import { BufferSet } from 'common/buffer/BufferSet';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { IBufferService, IOptionsService, type IBufferResizeEvent } from 'common/services/Services';
import { IBufferService, ILogService, IOptionsService, type IBufferResizeEvent } from 'common/services/Services';
import { Emitter } from 'common/Event';
export const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars
@@ -32,11 +32,14 @@ export class BufferService extends Disposable implements IBufferService {
/** An IBufferline to clone/copy from for new blank lines */
private _cachedBlankLine: IBufferLine | undefined;
constructor(@IOptionsService optionsService: IOptionsService) {
constructor(
@IOptionsService optionsService: IOptionsService,
@ILogService logService: ILogService
) {
super();
this.cols = Math.max(optionsService.rawOptions.cols || 0, MINIMUM_COLS);
this.rows = Math.max(optionsService.rawOptions.rows || 0, MINIMUM_ROWS);
this.buffers = this._register(new BufferSet(optionsService, this));
this.buffers = this._register(new BufferSet(optionsService, this, logService));
this._register(this.buffers.onBufferActivate(e => {
this._onScroll.fire(e.activeBuffer.ydisp);
}));
@@ -8,6 +8,7 @@ import { DecorationService } from './DecorationService';
import { IMarker } from 'common/Types';
import { Disposable } from 'common/Lifecycle';
import { Emitter } from 'common/Event';
import { MockLogService } from 'common/TestUtils.test';
function createFakeMarker(line: number): IMarker {
return Object.freeze(new class extends Disposable {
@@ -22,7 +23,7 @@ const fakeMarker: IMarker = createFakeMarker(1);
describe('DecorationService', () => {
it('should set isDisposed to true after dispose', () => {
const service = new DecorationService();
const service = new DecorationService(new MockLogService());
const decoration = service.registerDecoration({
marker: fakeMarker
});
@@ -34,7 +35,7 @@ describe('DecorationService', () => {
describe('forEachDecorationAtCell', () => {
it('should find decoration at its marker line', () => {
const service = new DecorationService();
const service = new DecorationService(new MockLogService());
const decoration = service.registerDecoration({
marker: createFakeMarker(5),
width: 10
@@ -47,7 +48,7 @@ describe('DecorationService', () => {
});
it('should find decoration with height > 1 on subsequent lines', () => {
const service = new DecorationService();
const service = new DecorationService(new MockLogService());
const decoration = service.registerDecoration({
marker: createFakeMarker(5),
width: 10,
@@ -73,7 +74,7 @@ describe('DecorationService', () => {
});
it('should not find decoration outside its x range', () => {
const service = new DecorationService();
const service = new DecorationService(new MockLogService());
const decoration = service.registerDecoration({
marker: createFakeMarker(5),
x: 5,
@@ -102,7 +103,7 @@ describe('DecorationService', () => {
describe('getDecorationsAtCell', () => {
it('should find decoration with height > 1 on subsequent lines', () => {
const service = new DecorationService();
const service = new DecorationService(new MockLogService());
const decoration = service.registerDecoration({
marker: createFakeMarker(5),
width: 10,
+5 -3
View File
@@ -5,7 +5,7 @@
import { css } from 'common/Color';
import { Disposable, DisposableStore, toDisposable } from 'common/Lifecycle';
import { IDecorationService, IInternalDecoration } from 'common/services/Services';
import { IDecorationService, IInternalDecoration, ILogService } from 'common/services/Services';
import { SortedList } from 'common/SortedList';
import { IColor } from 'common/Types';
import { IDecoration, IDecorationOptions, IMarker } from '@xterm/xterm';
@@ -25,7 +25,7 @@ export class DecorationService extends Disposable implements IDecorationService
* while marker line values do change, they should all change by the same amount so this should
* never become out of order.
*/
private readonly _decorations: SortedList<IInternalDecoration> = new SortedList(e => e?.marker.line);
private readonly _decorations: SortedList<IInternalDecoration>;
private readonly _onDecorationRegistered = this._register(new Emitter<IInternalDecoration>());
public readonly onDecorationRegistered = this._onDecorationRegistered.event;
@@ -34,9 +34,11 @@ export class DecorationService extends Disposable implements IDecorationService
public get decorations(): IterableIterator<IInternalDecoration> { return this._decorations.values(); }
constructor() {
constructor(@ILogService private readonly _logService: ILogService) {
super();
this._decorations = new SortedList(e => e?.marker.line, this._logService);
this._register(toDisposable(() => this.reset()));
}
+8 -1
View File
@@ -14,6 +14,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
rows: 24,
showCursorImmediately: false,
cursorBlink: false,
blinkIntervalDuration: 0,
cursorStyle: 'block',
cursorWidth: 1,
cursorInactiveStyle: 'outline',
@@ -31,6 +32,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
logLevel: 'info',
logger: null,
scrollback: 1000,
scrollbar: { showScrollbar: true },
scrollOnEraseInDisplay: false,
scrollOnUserInput: true,
scrollSensitivity: 1,
@@ -53,7 +55,6 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
altClickMovesCursor: true,
convertEol: false,
termName: 'xterm',
overviewRuler: {},
quirks: {},
vtExtensions: {}
};
@@ -168,6 +169,12 @@ export class OptionsService extends Disposable implements IOptionsService {
}
value = FONT_WEIGHT_OPTIONS.includes(value) ? value : DEFAULT_OPTIONS[key];
break;
case 'blinkIntervalDuration':
value = Math.floor(value);
if (value < 0) {
throw new Error(`${key} cannot be less than 0, value: ${value}`);
}
break;
case 'cursorWidth':
value = Math.floor(value);
// Fall through for bounds check
+2 -1
View File
@@ -9,6 +9,7 @@ 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';
import { MockLogService } from 'common/TestUtils.test';
describe('OscLinkService', () => {
describe('constructor', () => {
@@ -17,7 +18,7 @@ describe('OscLinkService', () => {
let oscLinkService: IOscLinkService;
beforeEach(() => {
optionsService = new OptionsService({ rows: 3, cols: 10 });
bufferService = new BufferService(optionsService);
bufferService = new BufferService(optionsService, new MockLogService());
oscLinkService = new OscLinkService(bufferService);
});
+9 -1
View File
@@ -229,6 +229,7 @@ export interface ITerminalOptions {
cols?: number;
convertEol?: boolean;
cursorBlink?: boolean;
blinkIntervalDuration?: number;
cursorStyle?: CursorStyle;
cursorWidth?: number;
cursorInactiveStyle?: CursorInactiveStyle;
@@ -264,8 +265,8 @@ export interface ITerminalOptions {
windowsPty?: IWindowsPty;
windowOptions?: IWindowOptions;
wordSeparator?: string;
overviewRuler?: IOverviewRulerOptions;
quirks?: ITerminalQuirks;
scrollbar?: IScrollbarOptions;
scrollOnEraseInDisplay?: boolean;
vtExtensions?: IVtExtensions;
@@ -308,6 +309,13 @@ export interface ITerminalQuirks {
allowSetCursorBlink?: boolean;
}
export interface IScrollbarOptions {
showScrollbar?: boolean;
showArrows?: boolean;
width?: number;
overviewRuler?: IOverviewRulerOptions;
}
export interface IVtExtensions {
kittyKeyboard?: boolean;
kittySgrBoldFaintControl?: boolean;
+2 -4
View File
@@ -12,8 +12,7 @@
],
"baseUrl": "../",
"paths": {
"common/*": [ "./common/*" ],
"vs/*": [ "./vs/*" ]
"common/*": [ "./common/*" ]
}
},
"include": [
@@ -22,7 +21,6 @@
"../../typings/xterm-headless.d.ts"
],
"references": [
{ "path": "../common" },
{ "path": "../vs" }
{ "path": "../common" }
]
}
-13
View File
@@ -1,13 +0,0 @@
temp/
# Tests
base/test/
base/**/test/
# Unwanted modules
base/node/
base/parts/
base/worker/
# Binary files
base/browser/ui/codicons/codicon/codicon.ttf
-23
View File
@@ -1,23 +0,0 @@
This folder contains the `base/` module from the [Visual Studio Code repository](https://github.com/microsoft/vscode) which has many helpers that are useful to xterm.js.
Rarely we want to update these sources when an important bug is fixed upstream or when there is a new feature we want to leverage. To update against upstream:
```
./bin/vs_base_update.ps1
```
If new functions are being used from the project then import them from another project.
Before committing we need to clean up the diff so that files that aren't being used are not inlcuded. The following script uses the typescript compiler to find any files that are not being imported into the project:
```
node ./bin/vs_base_find_unused.js
```
The last step is to do a once over of the resulting bundled xterm.js file to ensure it isn't too large:
1. Run `npm run esbuild`
2. Open up `xterm.mjs`
3. Search for `src/vs/base/`
This will show you all the parts of base that will be included in the final minified bundle. Unfortunately tree shaking doesn't find everything, be on the lookout for large arrays or classes that aren't being used. If your editor has find decorations in the scroll bar it's easy to find which parts of base are consuming a lot of lines.
-141
View File
@@ -1,141 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CodeWindow, mainWindow } from 'vs/base/browser/window';
import { Emitter } from 'vs/base/common/event';
class WindowManager {
static readonly INSTANCE = new WindowManager();
// --- Zoom Level
private readonly mapWindowIdToZoomLevel = new Map<number, number>();
private readonly _onDidChangeZoomLevel = new Emitter<number>();
readonly onDidChangeZoomLevel = this._onDidChangeZoomLevel.event;
getZoomLevel(targetWindow: Window): number {
return this.mapWindowIdToZoomLevel.get(this.getWindowId(targetWindow)) ?? 0;
}
setZoomLevel(zoomLevel: number, targetWindow: Window): void {
if (this.getZoomLevel(targetWindow) === zoomLevel) {
return;
}
const targetWindowId = this.getWindowId(targetWindow);
this.mapWindowIdToZoomLevel.set(targetWindowId, zoomLevel);
this._onDidChangeZoomLevel.fire(targetWindowId);
}
// --- Zoom Factor
private readonly mapWindowIdToZoomFactor = new Map<number, number>();
getZoomFactor(targetWindow: Window): number {
return this.mapWindowIdToZoomFactor.get(this.getWindowId(targetWindow)) ?? 1;
}
setZoomFactor(zoomFactor: number, targetWindow: Window): void {
this.mapWindowIdToZoomFactor.set(this.getWindowId(targetWindow), zoomFactor);
}
// --- Fullscreen
private readonly _onDidChangeFullscreen = new Emitter<number>();
readonly onDidChangeFullscreen = this._onDidChangeFullscreen.event;
private readonly mapWindowIdToFullScreen = new Map<number, boolean>();
setFullscreen(fullscreen: boolean, targetWindow: Window): void {
if (this.isFullscreen(targetWindow) === fullscreen) {
return;
}
const windowId = this.getWindowId(targetWindow);
this.mapWindowIdToFullScreen.set(windowId, fullscreen);
this._onDidChangeFullscreen.fire(windowId);
}
isFullscreen(targetWindow: Window): boolean {
return !!this.mapWindowIdToFullScreen.get(this.getWindowId(targetWindow));
}
private getWindowId(targetWindow: Window): number {
return (targetWindow as CodeWindow).vscodeWindowId;
}
}
export function addMatchMediaChangeListener(targetWindow: Window, query: string | MediaQueryList, callback: (this: MediaQueryList, ev: MediaQueryListEvent) => any): void {
if (typeof query === 'string') {
query = targetWindow.matchMedia(query);
}
query.addEventListener('change', callback);
}
/** A zoom index, e.g. 1, 2, 3 */
export function setZoomLevel(zoomLevel: number, targetWindow: Window): void {
WindowManager.INSTANCE.setZoomLevel(zoomLevel, targetWindow);
}
export function getZoomLevel(targetWindow: Window): number {
return WindowManager.INSTANCE.getZoomLevel(targetWindow);
}
export const onDidChangeZoomLevel = WindowManager.INSTANCE.onDidChangeZoomLevel;
/** The zoom scale for an index, e.g. 1, 1.2, 1.4 */
export function getZoomFactor(targetWindow: Window): number {
return WindowManager.INSTANCE.getZoomFactor(targetWindow);
}
export function setZoomFactor(zoomFactor: number, targetWindow: Window): void {
WindowManager.INSTANCE.setZoomFactor(zoomFactor, targetWindow);
}
export function setFullscreen(fullscreen: boolean, targetWindow: Window): void {
WindowManager.INSTANCE.setFullscreen(fullscreen, targetWindow);
}
export function isFullscreen(targetWindow: Window): boolean {
return WindowManager.INSTANCE.isFullscreen(targetWindow);
}
export const onDidChangeFullscreen = WindowManager.INSTANCE.onDidChangeFullscreen;
const userAgent = typeof navigator === 'object' ? navigator.userAgent : '';
export const isFirefox = (userAgent.indexOf('Firefox') >= 0);
export const isWebKit = (userAgent.indexOf('AppleWebKit') >= 0);
export const isChrome = (userAgent.indexOf('Chrome') >= 0);
export const isSafari = (!isChrome && (userAgent.indexOf('Safari') >= 0));
export const isWebkitWebView = (!isChrome && !isSafari && isWebKit);
export const isElectron = (userAgent.indexOf('Electron/') >= 0);
export const isAndroid = (userAgent.indexOf('Android') >= 0);
let standalone = false;
if (typeof mainWindow.matchMedia === 'function') {
const standaloneMatchMedia = mainWindow.matchMedia('(display-mode: standalone) or (display-mode: window-controls-overlay)');
const fullScreenMatchMedia = mainWindow.matchMedia('(display-mode: fullscreen)');
standalone = standaloneMatchMedia.matches;
addMatchMediaChangeListener(mainWindow, standaloneMatchMedia, ({ matches }) => {
// entering fullscreen would change standaloneMatchMedia.matches to false
// if standalone is true (running as PWA) and entering fullscreen, skip this change
if (standalone && fullScreenMatchMedia.matches) {
return;
}
// otherwise update standalone (browser to PWA or PWA to browser)
standalone = matches;
});
}
export function isStandalone(): boolean {
return standalone;
}
// Visible means that the feature is enabled, not necessarily being rendered
// e.g. visible is true even in fullscreen mode where the controls are hidden
// See docs at https://developer.mozilla.org/en-US/docs/Web/API/WindowControlsOverlay/visible
export function isWCOEnabled(): boolean {
return (navigator as any)?.windowControlsOverlay?.visible;
}
// Returns the bounding rect of the titlebar area if it is supported and defined
// See docs at https://developer.mozilla.org/en-US/docs/Web/API/WindowControlsOverlay/getTitlebarAreaRect
export function getWCOBoundingRect(): DOMRect | undefined {
return (navigator as any)?.windowControlsOverlay?.getTitlebarAreaRect();
}
-49
View File
@@ -1,49 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as browser from 'vs/base/browser/browser';
import { mainWindow } from 'vs/base/browser/window';
import * as platform from 'vs/base/common/platform';
export const enum KeyboardSupport {
Always,
FullScreen,
None
}
const safeNavigator = typeof navigator === 'object' ? navigator : {} as { [key: string]: any };
/**
* Browser feature we can support in current platform, browser and environment.
*/
export const BrowserFeatures = {
clipboard: {
writeText: (
platform.isNative
|| (document.queryCommandSupported && document.queryCommandSupported('copy'))
|| !!(safeNavigator && safeNavigator.clipboard && safeNavigator.clipboard.writeText)
),
readText: (
platform.isNative
|| !!(safeNavigator && safeNavigator.clipboard && safeNavigator.clipboard.readText)
)
},
keyboard: (() => {
if (platform.isNative || browser.isStandalone()) {
return KeyboardSupport.Always;
}
if ((<any>safeNavigator).keyboard || browser.isSafari) {
return KeyboardSupport.FullScreen;
}
return KeyboardSupport.None;
})(),
// 'ontouchstart' in window always evaluates to true with typescript's modern typings. This causes `window` to be
// `never` later in `window.navigator`. That's why we need the explicit `window as Window` cast
touch: 'ontouchstart' in mainWindow || safeNavigator.maxTouchPoints > 0,
pointerEvents: mainWindow.PointerEvent && ('ontouchstart' in mainWindow || navigator.maxTouchPoints > 0)
};
File diff suppressed because it is too large Load Diff
-316
View File
@@ -1,316 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export class FastDomNode<T extends HTMLElement> {
private _maxWidth: string = '';
private _width: string = '';
private _height: string = '';
private _top: string = '';
private _left: string = '';
private _bottom: string = '';
private _right: string = '';
private _paddingTop: string = '';
private _paddingLeft: string = '';
private _paddingBottom: string = '';
private _paddingRight: string = '';
private _fontFamily: string = '';
private _fontWeight: string = '';
private _fontSize: string = '';
private _fontStyle: string = '';
private _fontFeatureSettings: string = '';
private _fontVariationSettings: string = '';
private _textDecoration: string = '';
private _lineHeight: string = '';
private _letterSpacing: string = '';
private _className: string = '';
private _display: string = '';
private _position: string = '';
private _visibility: string = '';
private _color: string = '';
private _backgroundColor: string = '';
private _layerHint: boolean = false;
private _contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint' = 'none';
private _boxShadow: string = '';
constructor(
public readonly domNode: T
) { }
public setMaxWidth(_maxWidth: number | string): void {
const maxWidth = numberAsPixels(_maxWidth);
if (this._maxWidth === maxWidth) {
return;
}
this._maxWidth = maxWidth;
this.domNode.style.maxWidth = this._maxWidth;
}
public setWidth(_width: number | string): void {
const width = numberAsPixels(_width);
if (this._width === width) {
return;
}
this._width = width;
this.domNode.style.width = this._width;
}
public setHeight(_height: number | string): void {
const height = numberAsPixels(_height);
if (this._height === height) {
return;
}
this._height = height;
this.domNode.style.height = this._height;
}
public setTop(_top: number | string): void {
const top = numberAsPixels(_top);
if (this._top === top) {
return;
}
this._top = top;
this.domNode.style.top = this._top;
}
public setLeft(_left: number | string): void {
const left = numberAsPixels(_left);
if (this._left === left) {
return;
}
this._left = left;
this.domNode.style.left = this._left;
}
public setBottom(_bottom: number | string): void {
const bottom = numberAsPixels(_bottom);
if (this._bottom === bottom) {
return;
}
this._bottom = bottom;
this.domNode.style.bottom = this._bottom;
}
public setRight(_right: number | string): void {
const right = numberAsPixels(_right);
if (this._right === right) {
return;
}
this._right = right;
this.domNode.style.right = this._right;
}
public setPaddingTop(_paddingTop: number | string): void {
const paddingTop = numberAsPixels(_paddingTop);
if (this._paddingTop === paddingTop) {
return;
}
this._paddingTop = paddingTop;
this.domNode.style.paddingTop = this._paddingTop;
}
public setPaddingLeft(_paddingLeft: number | string): void {
const paddingLeft = numberAsPixels(_paddingLeft);
if (this._paddingLeft === paddingLeft) {
return;
}
this._paddingLeft = paddingLeft;
this.domNode.style.paddingLeft = this._paddingLeft;
}
public setPaddingBottom(_paddingBottom: number | string): void {
const paddingBottom = numberAsPixels(_paddingBottom);
if (this._paddingBottom === paddingBottom) {
return;
}
this._paddingBottom = paddingBottom;
this.domNode.style.paddingBottom = this._paddingBottom;
}
public setPaddingRight(_paddingRight: number | string): void {
const paddingRight = numberAsPixels(_paddingRight);
if (this._paddingRight === paddingRight) {
return;
}
this._paddingRight = paddingRight;
this.domNode.style.paddingRight = this._paddingRight;
}
public setFontFamily(fontFamily: string): void {
if (this._fontFamily === fontFamily) {
return;
}
this._fontFamily = fontFamily;
this.domNode.style.fontFamily = this._fontFamily;
}
public setFontWeight(fontWeight: string): void {
if (this._fontWeight === fontWeight) {
return;
}
this._fontWeight = fontWeight;
this.domNode.style.fontWeight = this._fontWeight;
}
public setFontSize(_fontSize: number | string): void {
const fontSize = numberAsPixels(_fontSize);
if (this._fontSize === fontSize) {
return;
}
this._fontSize = fontSize;
this.domNode.style.fontSize = this._fontSize;
}
public setFontStyle(fontStyle: string): void {
if (this._fontStyle === fontStyle) {
return;
}
this._fontStyle = fontStyle;
this.domNode.style.fontStyle = this._fontStyle;
}
public setFontFeatureSettings(fontFeatureSettings: string): void {
if (this._fontFeatureSettings === fontFeatureSettings) {
return;
}
this._fontFeatureSettings = fontFeatureSettings;
this.domNode.style.fontFeatureSettings = this._fontFeatureSettings;
}
public setFontVariationSettings(fontVariationSettings: string): void {
if (this._fontVariationSettings === fontVariationSettings) {
return;
}
this._fontVariationSettings = fontVariationSettings;
this.domNode.style.fontVariationSettings = this._fontVariationSettings;
}
public setTextDecoration(textDecoration: string): void {
if (this._textDecoration === textDecoration) {
return;
}
this._textDecoration = textDecoration;
this.domNode.style.textDecoration = this._textDecoration;
}
public setLineHeight(_lineHeight: number | string): void {
const lineHeight = numberAsPixels(_lineHeight);
if (this._lineHeight === lineHeight) {
return;
}
this._lineHeight = lineHeight;
this.domNode.style.lineHeight = this._lineHeight;
}
public setLetterSpacing(_letterSpacing: number | string): void {
const letterSpacing = numberAsPixels(_letterSpacing);
if (this._letterSpacing === letterSpacing) {
return;
}
this._letterSpacing = letterSpacing;
this.domNode.style.letterSpacing = this._letterSpacing;
}
public setClassName(className: string): void {
if (this._className === className) {
return;
}
this._className = className;
this.domNode.className = this._className;
}
public toggleClassName(className: string, shouldHaveIt?: boolean): void {
this.domNode.classList.toggle(className, shouldHaveIt);
this._className = this.domNode.className;
}
public setDisplay(display: string): void {
if (this._display === display) {
return;
}
this._display = display;
this.domNode.style.display = this._display;
}
public setPosition(position: string): void {
if (this._position === position) {
return;
}
this._position = position;
this.domNode.style.position = this._position;
}
public setVisibility(visibility: string): void {
if (this._visibility === visibility) {
return;
}
this._visibility = visibility;
this.domNode.style.visibility = this._visibility;
}
public setColor(color: string): void {
if (this._color === color) {
return;
}
this._color = color;
this.domNode.style.color = this._color;
}
public setBackgroundColor(backgroundColor: string): void {
if (this._backgroundColor === backgroundColor) {
return;
}
this._backgroundColor = backgroundColor;
this.domNode.style.backgroundColor = this._backgroundColor;
}
public setLayerHinting(layerHint: boolean): void {
if (this._layerHint === layerHint) {
return;
}
this._layerHint = layerHint;
this.domNode.style.transform = this._layerHint ? 'translate3d(0px, 0px, 0px)' : '';
}
public setBoxShadow(boxShadow: string): void {
if (this._boxShadow === boxShadow) {
return;
}
this._boxShadow = boxShadow;
this.domNode.style.boxShadow = boxShadow;
}
public setContain(contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint'): void {
if (this._contain === contain) {
return;
}
this._contain = contain;
(<any>this.domNode.style).contain = this._contain;
}
public setAttribute(name: string, value: string): void {
this.domNode.setAttribute(name, value);
}
public removeAttribute(name: string): void {
this.domNode.removeAttribute(name);
}
public appendChild(child: FastDomNode<T>): void {
this.domNode.appendChild(child.domNode);
}
public removeChild(child: FastDomNode<T>): void {
this.domNode.removeChild(child.domNode);
}
}
function numberAsPixels(value: number | string): string {
return (typeof value === 'number' ? `${value}px` : value);
}
export function createFastDomNode<T extends HTMLElement>(domNode: T): FastDomNode<T> {
return new FastDomNode(domNode);
}
@@ -1,112 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
export interface IPointerMoveCallback {
(event: PointerEvent): void;
}
export interface IOnStopCallback {
(browserEvent?: PointerEvent | KeyboardEvent): void;
}
export class GlobalPointerMoveMonitor implements IDisposable {
private readonly _hooks = new DisposableStore();
private _pointerMoveCallback: IPointerMoveCallback | null = null;
private _onStopCallback: IOnStopCallback | null = null;
public dispose(): void {
this.stopMonitoring(false);
this._hooks.dispose();
}
public stopMonitoring(invokeStopCallback: boolean, browserEvent?: PointerEvent | KeyboardEvent): void {
if (!this.isMonitoring()) {
// Not monitoring
return;
}
// Unhook
this._hooks.clear();
this._pointerMoveCallback = null;
const onStopCallback = this._onStopCallback;
this._onStopCallback = null;
if (invokeStopCallback && onStopCallback) {
onStopCallback(browserEvent);
}
}
public isMonitoring(): boolean {
return !!this._pointerMoveCallback;
}
public startMonitoring(
initialElement: Element,
pointerId: number,
initialButtons: number,
pointerMoveCallback: IPointerMoveCallback,
onStopCallback: IOnStopCallback
): void {
if (this.isMonitoring()) {
this.stopMonitoring(false);
}
this._pointerMoveCallback = pointerMoveCallback;
this._onStopCallback = onStopCallback;
let eventSource: Element | Window = initialElement;
try {
initialElement.setPointerCapture(pointerId);
this._hooks.add(toDisposable(() => {
try {
initialElement.releasePointerCapture(pointerId);
} catch (err) {
// See https://github.com/microsoft/vscode/issues/161731
//
// `releasePointerCapture` sometimes fails when being invoked with the exception:
// DOMException: Failed to execute 'releasePointerCapture' on 'Element':
// No active pointer with the given id is found.
//
// There's no need to do anything in case of failure
}
}));
} catch (err) {
// See https://github.com/microsoft/vscode/issues/144584
// See https://github.com/microsoft/vscode/issues/146947
// `setPointerCapture` sometimes fails when being invoked
// from a `mousedown` listener on macOS and Windows
// and it always fails on Linux with the exception:
// DOMException: Failed to execute 'setPointerCapture' on 'Element':
// No active pointer with the given id is found.
// In case of failure, we bind the listeners on the window
eventSource = dom.getWindow(initialElement);
}
this._hooks.add(dom.addDisposableListener(
eventSource,
dom.EventType.POINTER_MOVE,
(e) => {
if (e.buttons !== initialButtons) {
// Buttons state has changed in the meantime
this.stopMonitoring(true);
return;
}
e.preventDefault();
this._pointerMoveCallback!(e);
}
));
this._hooks.add(dom.addDisposableListener(
eventSource,
dom.EventType.POINTER_UP,
(e: PointerEvent) => this.stopMonitoring(true)
));
}
}
-135
View File
@@ -1,135 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Represents a window in a possible chain of iframes
*/
interface IWindowChainElement {
/**
* The window object for it
*/
readonly window: WeakRef<Window>;
/**
* The iframe element inside the window.parent corresponding to window
*/
readonly iframeElement: Element | null;
}
const sameOriginWindowChainCache = new WeakMap<Window, IWindowChainElement[] | null>();
function getParentWindowIfSameOrigin(w: Window): Window | null {
if (!w.parent || w.parent === w) {
return null;
}
// Cannot really tell if we have access to the parent window unless we try to access something in it
try {
const location = w.location;
const parentLocation = w.parent.location;
if (location.origin !== 'null' && parentLocation.origin !== 'null' && location.origin !== parentLocation.origin) {
return null;
}
} catch (e) {
return null;
}
return w.parent;
}
export class IframeUtils {
/**
* Returns a chain of embedded windows with the same origin (which can be accessed programmatically).
* Having a chain of length 1 might mean that the current execution environment is running outside of an iframe or inside an iframe embedded in a window with a different origin.
*/
private static getSameOriginWindowChain(targetWindow: Window): IWindowChainElement[] {
let windowChainCache = sameOriginWindowChainCache.get(targetWindow);
if (!windowChainCache) {
windowChainCache = [];
sameOriginWindowChainCache.set(targetWindow, windowChainCache);
let w: Window | null = targetWindow;
let parent: Window | null;
do {
parent = getParentWindowIfSameOrigin(w);
if (parent) {
windowChainCache.push({
window: new WeakRef(w),
iframeElement: w.frameElement || null
});
} else {
windowChainCache.push({
window: new WeakRef(w),
iframeElement: null
});
}
w = parent;
} while (w);
}
return windowChainCache.slice(0);
}
/**
* Returns the position of `childWindow` relative to `ancestorWindow`
*/
public static getPositionOfChildWindowRelativeToAncestorWindow(childWindow: Window, ancestorWindow: Window | null) {
if (!ancestorWindow || childWindow === ancestorWindow) {
return {
top: 0,
left: 0
};
}
let top = 0, left = 0;
const windowChain = this.getSameOriginWindowChain(childWindow);
for (const windowChainEl of windowChain) {
const windowInChain = windowChainEl.window.deref();
top += windowInChain?.scrollY ?? 0;
left += windowInChain?.scrollX ?? 0;
if (windowInChain === ancestorWindow) {
break;
}
if (!windowChainEl.iframeElement) {
break;
}
const boundingRect = windowChainEl.iframeElement.getBoundingClientRect();
top += boundingRect.top;
left += boundingRect.left;
}
return {
top: top,
left: left
};
}
}
/**
* Returns a sha-256 composed of `parentOrigin` and `salt` converted to base 32
*/
export async function parentOriginHash(parentOrigin: string, salt: string): Promise<string> {
// This same code is also inlined at `src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html`
if (!crypto.subtle) {
throw new Error(`'crypto.subtle' is not available so webviews will not work. This is likely because the editor is not running in a secure context (https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).`);
}
const strData = JSON.stringify({ parentOrigin, salt });
const encoder = new TextEncoder();
const arrData = encoder.encode(strData);
const hash = await crypto.subtle.digest('sha-256', arrData);
return sha256AsBase32(hash);
}
function sha256AsBase32(bytes: ArrayBuffer): string {
const array = Array.from(new Uint8Array(bytes));
const hexArray = array.map(b => b.toString(16).padStart(2, '0')).join('');
// sha256 has 256 bits, so we need at most ceil(lg(2^256-1)/lg(32)) = 52 chars to represent it in base 32
return BigInt(`0x${hexArray}`).toString(32).padStart(52, '0');
}
-213
View File
@@ -1,213 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as browser from 'vs/base/browser/browser';
import { EVENT_KEY_CODE_MAP, KeyCode, KeyCodeUtils, KeyMod } from 'vs/base/common/keyCodes';
import { KeyCodeChord } from 'vs/base/common/keybindings';
import * as platform from 'vs/base/common/platform';
function extractKeyCode(e: KeyboardEvent): KeyCode {
if (e.charCode) {
// "keypress" events mostly
const char = String.fromCharCode(e.charCode).toUpperCase();
return KeyCodeUtils.fromString(char);
}
const keyCode = e.keyCode;
// browser quirks
if (keyCode === 3) {
return KeyCode.PauseBreak;
} else if (browser.isFirefox) {
switch (keyCode) {
case 59: return KeyCode.Semicolon;
case 60:
if (platform.isLinux) { return KeyCode.IntlBackslash; }
break;
case 61: return KeyCode.Equal;
// based on: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode#numpad_keys
case 107: return KeyCode.NumpadAdd;
case 109: return KeyCode.NumpadSubtract;
case 173: return KeyCode.Minus;
case 224:
if (platform.isMacintosh) { return KeyCode.Meta; }
break;
}
} else if (browser.isWebKit) {
if (platform.isMacintosh && keyCode === 93) {
// the two meta keys in the Mac have different key codes (91 and 93)
return KeyCode.Meta;
} else if (!platform.isMacintosh && keyCode === 92) {
return KeyCode.Meta;
}
}
// cross browser keycodes:
return EVENT_KEY_CODE_MAP[keyCode] || KeyCode.Unknown;
}
export interface IKeyboardEvent {
readonly _standardKeyboardEventBrand: true;
readonly browserEvent: KeyboardEvent;
readonly target: HTMLElement;
readonly ctrlKey: boolean;
readonly shiftKey: boolean;
readonly altKey: boolean;
readonly metaKey: boolean;
readonly altGraphKey: boolean;
readonly keyCode: KeyCode;
readonly code: string;
/**
* @internal
*/
toKeyCodeChord(): KeyCodeChord;
equals(keybinding: number): boolean;
preventDefault(): void;
stopPropagation(): void;
}
const ctrlKeyMod = (platform.isMacintosh ? KeyMod.WinCtrl : KeyMod.CtrlCmd);
const altKeyMod = KeyMod.Alt;
const shiftKeyMod = KeyMod.Shift;
const metaKeyMod = (platform.isMacintosh ? KeyMod.CtrlCmd : KeyMod.WinCtrl);
export function printKeyboardEvent(e: KeyboardEvent): string {
const modifiers: string[] = [];
if (e.ctrlKey) {
modifiers.push(`ctrl`);
}
if (e.shiftKey) {
modifiers.push(`shift`);
}
if (e.altKey) {
modifiers.push(`alt`);
}
if (e.metaKey) {
modifiers.push(`meta`);
}
return `modifiers: [${modifiers.join(',')}], code: ${e.code}, keyCode: ${e.keyCode}, key: ${e.key}`;
}
export function printStandardKeyboardEvent(e: StandardKeyboardEvent): string {
const modifiers: string[] = [];
if (e.ctrlKey) {
modifiers.push(`ctrl`);
}
if (e.shiftKey) {
modifiers.push(`shift`);
}
if (e.altKey) {
modifiers.push(`alt`);
}
if (e.metaKey) {
modifiers.push(`meta`);
}
return `modifiers: [${modifiers.join(',')}], code: ${e.code}, keyCode: ${e.keyCode} ('${KeyCodeUtils.toString(e.keyCode)}')`;
}
export class StandardKeyboardEvent implements IKeyboardEvent {
readonly _standardKeyboardEventBrand = true;
public readonly browserEvent: KeyboardEvent;
public readonly target: HTMLElement;
public readonly ctrlKey: boolean;
public readonly shiftKey: boolean;
public readonly altKey: boolean;
public readonly metaKey: boolean;
public readonly altGraphKey: boolean;
public readonly keyCode: KeyCode;
public readonly code: string;
private _asKeybinding: number;
private _asKeyCodeChord: KeyCodeChord;
constructor(source: KeyboardEvent) {
const e = source;
this.browserEvent = e;
this.target = <HTMLElement>e.target;
this.ctrlKey = e.ctrlKey;
this.shiftKey = e.shiftKey;
this.altKey = e.altKey;
this.metaKey = e.metaKey;
this.altGraphKey = e.getModifierState?.('AltGraph');
this.keyCode = extractKeyCode(e);
this.code = e.code;
// console.info(e.type + ": keyCode: " + e.keyCode + ", which: " + e.which + ", charCode: " + e.charCode + ", detail: " + e.detail + " ====> " + this.keyCode + ' -- ' + KeyCode[this.keyCode]);
this.ctrlKey = this.ctrlKey || this.keyCode === KeyCode.Ctrl;
this.altKey = this.altKey || this.keyCode === KeyCode.Alt;
this.shiftKey = this.shiftKey || this.keyCode === KeyCode.Shift;
this.metaKey = this.metaKey || this.keyCode === KeyCode.Meta;
this._asKeybinding = this._computeKeybinding();
this._asKeyCodeChord = this._computeKeyCodeChord();
// console.log(`code: ${e.code}, keyCode: ${e.keyCode}, key: ${e.key}`);
}
public preventDefault(): void {
if (this.browserEvent && this.browserEvent.preventDefault) {
this.browserEvent.preventDefault();
}
}
public stopPropagation(): void {
if (this.browserEvent && this.browserEvent.stopPropagation) {
this.browserEvent.stopPropagation();
}
}
public toKeyCodeChord(): KeyCodeChord {
return this._asKeyCodeChord;
}
public equals(other: number): boolean {
return this._asKeybinding === other;
}
private _computeKeybinding(): number {
let key = KeyCode.Unknown;
if (this.keyCode !== KeyCode.Ctrl && this.keyCode !== KeyCode.Shift && this.keyCode !== KeyCode.Alt && this.keyCode !== KeyCode.Meta) {
key = this.keyCode;
}
let result = 0;
if (this.ctrlKey) {
result |= ctrlKeyMod;
}
if (this.altKey) {
result |= altKeyMod;
}
if (this.shiftKey) {
result |= shiftKeyMod;
}
if (this.metaKey) {
result |= metaKeyMod;
}
result |= key;
return result;
}
private _computeKeyCodeChord(): KeyCodeChord {
let key = KeyCode.Unknown;
if (this.keyCode !== KeyCode.Ctrl && this.keyCode !== KeyCode.Shift && this.keyCode !== KeyCode.Alt && this.keyCode !== KeyCode.Meta) {
key = this.keyCode;
}
return new KeyCodeChord(this.ctrlKey, this.shiftKey, this.altKey, this.metaKey, key);
}
}
-229
View File
@@ -1,229 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as browser from 'vs/base/browser/browser';
import { IframeUtils } from 'vs/base/browser/iframe';
import * as platform from 'vs/base/common/platform';
export interface IMouseEvent {
readonly browserEvent: MouseEvent;
readonly leftButton: boolean;
readonly middleButton: boolean;
readonly rightButton: boolean;
readonly buttons: number;
readonly target: HTMLElement;
readonly detail: number;
readonly posx: number;
readonly posy: number;
readonly ctrlKey: boolean;
readonly shiftKey: boolean;
readonly altKey: boolean;
readonly metaKey: boolean;
readonly timestamp: number;
preventDefault(): void;
stopPropagation(): void;
}
export class StandardMouseEvent implements IMouseEvent {
public readonly browserEvent: MouseEvent;
public readonly leftButton: boolean;
public readonly middleButton: boolean;
public readonly rightButton: boolean;
public readonly buttons: number;
public readonly target: HTMLElement;
public detail: number;
public readonly posx: number;
public readonly posy: number;
public readonly ctrlKey: boolean;
public readonly shiftKey: boolean;
public readonly altKey: boolean;
public readonly metaKey: boolean;
public readonly timestamp: number;
constructor(targetWindow: Window, e: MouseEvent) {
this.timestamp = Date.now();
this.browserEvent = e;
this.leftButton = e.button === 0;
this.middleButton = e.button === 1;
this.rightButton = e.button === 2;
this.buttons = e.buttons;
this.target = <HTMLElement>e.target;
this.detail = e.detail || 1;
if (e.type === 'dblclick') {
this.detail = 2;
}
this.ctrlKey = e.ctrlKey;
this.shiftKey = e.shiftKey;
this.altKey = e.altKey;
this.metaKey = e.metaKey;
if (typeof e.pageX === 'number') {
this.posx = e.pageX;
this.posy = e.pageY;
} else {
// Probably hit by MSGestureEvent
this.posx = e.clientX + this.target.ownerDocument.body.scrollLeft + this.target.ownerDocument.documentElement.scrollLeft;
this.posy = e.clientY + this.target.ownerDocument.body.scrollTop + this.target.ownerDocument.documentElement.scrollTop;
}
// Find the position of the iframe this code is executing in relative to the iframe where the event was captured.
const iframeOffsets = IframeUtils.getPositionOfChildWindowRelativeToAncestorWindow(targetWindow, e.view);
this.posx -= iframeOffsets.left;
this.posy -= iframeOffsets.top;
}
public preventDefault(): void {
this.browserEvent.preventDefault();
}
public stopPropagation(): void {
this.browserEvent.stopPropagation();
}
}
export class DragMouseEvent extends StandardMouseEvent {
public readonly dataTransfer: DataTransfer;
constructor(targetWindow: Window, e: MouseEvent) {
super(targetWindow, e);
this.dataTransfer = (<any>e).dataTransfer;
}
}
export interface IMouseWheelEvent extends MouseEvent {
readonly wheelDelta: number;
readonly wheelDeltaX: number;
readonly wheelDeltaY: number;
readonly deltaX: number;
readonly deltaY: number;
readonly deltaZ: number;
readonly deltaMode: number;
}
interface IWebKitMouseWheelEvent {
wheelDeltaY: number;
wheelDeltaX: number;
}
interface IGeckoMouseWheelEvent {
HORIZONTAL_AXIS: number;
VERTICAL_AXIS: number;
axis: number;
detail: number;
}
export class StandardWheelEvent {
public readonly browserEvent: IMouseWheelEvent | null;
public readonly deltaY: number;
public readonly deltaX: number;
public readonly target: Node;
constructor(e: IMouseWheelEvent | null, deltaX: number = 0, deltaY: number = 0) {
this.browserEvent = e || null;
this.target = e ? (e.target || (<any>e).targetNode || e.srcElement) : null;
this.deltaY = deltaY;
this.deltaX = deltaX;
let shouldFactorDPR: boolean = false;
if (browser.isChrome) {
// Chrome version >= 123 contains the fix to factor devicePixelRatio into the wheel event.
// See https://chromium.googlesource.com/chromium/src.git/+/be51b448441ff0c9d1f17e0f25c4bf1ab3f11f61
const chromeVersionMatch = navigator.userAgent.match(/Chrome\/(\d+)/);
const chromeMajorVersion = chromeVersionMatch ? parseInt(chromeVersionMatch[1]) : 123;
shouldFactorDPR = chromeMajorVersion <= 122;
}
if (e) {
// Old (deprecated) wheel events
const e1 = <IWebKitMouseWheelEvent><any>e;
const e2 = <IGeckoMouseWheelEvent><any>e;
const devicePixelRatio = e.view?.devicePixelRatio || 1;
// vertical delta scroll
if (typeof e1.wheelDeltaY !== 'undefined') {
if (shouldFactorDPR) {
// Refs https://github.com/microsoft/vscode/issues/146403#issuecomment-1854538928
this.deltaY = e1.wheelDeltaY / (120 * devicePixelRatio);
} else {
this.deltaY = e1.wheelDeltaY / 120;
}
} else if (typeof e2.VERTICAL_AXIS !== 'undefined' && e2.axis === e2.VERTICAL_AXIS) {
this.deltaY = -e2.detail / 3;
} else if (e.type === 'wheel') {
// Modern wheel event
// https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent
const ev = <WheelEvent><unknown>e;
if (ev.deltaMode === ev.DOM_DELTA_LINE) {
// the deltas are expressed in lines
if (browser.isFirefox && !platform.isMacintosh) {
this.deltaY = -e.deltaY / 3;
} else {
this.deltaY = -e.deltaY;
}
} else {
this.deltaY = -e.deltaY / 40;
}
}
// horizontal delta scroll
if (typeof e1.wheelDeltaX !== 'undefined') {
if (browser.isSafari && platform.isWindows) {
this.deltaX = - (e1.wheelDeltaX / 120);
} else if (shouldFactorDPR) {
// Refs https://github.com/microsoft/vscode/issues/146403#issuecomment-1854538928
this.deltaX = e1.wheelDeltaX / (120 * devicePixelRatio);
} else {
this.deltaX = e1.wheelDeltaX / 120;
}
} else if (typeof e2.HORIZONTAL_AXIS !== 'undefined' && e2.axis === e2.HORIZONTAL_AXIS) {
this.deltaX = -e.detail / 3;
} else if (e.type === 'wheel') {
// Modern wheel event
// https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent
const ev = <WheelEvent><unknown>e;
if (ev.deltaMode === ev.DOM_DELTA_LINE) {
// the deltas are expressed in lines
if (browser.isFirefox && !platform.isMacintosh) {
this.deltaX = -e.deltaX / 3;
} else {
this.deltaX = -e.deltaX;
}
} else {
this.deltaX = -e.deltaX / 40;
}
}
// Assume a vertical scroll if nothing else worked
if (this.deltaY === 0 && this.deltaX === 0 && e.wheelDelta) {
if (shouldFactorDPR) {
// Refs https://github.com/microsoft/vscode/issues/146403#issuecomment-1854538928
this.deltaY = e.wheelDelta / (120 * devicePixelRatio);
} else {
this.deltaY = e.wheelDelta / 120;
}
}
}
}
public preventDefault(): void {
this.browserEvent?.preventDefault();
}
public stopPropagation(): void {
this.browserEvent?.stopPropagation();
}
}

Some files were not shown because too many files have changed in this diff Show More