Restructure common into layered folders (issue #5963)
Reorganize src/common into platform (base, encoding, data, buffer, parser, input), integration (services, terminal), and public API layers. - Move core utilities to common/base/ - Move TextDecoder to common/encoding/ - Move Types, CircularList, Color, TaskQueue, SortedList to common/services/ - Move CoreTerminal, InputHandler, WindowsMode to common/terminal/ - Define ICharset in common/data/Charsets.ts (re-exported from Types) - Update imports across src/, addons/, and test/ - Add per-layer tsconfig.json files and REARCHITECTURE.md - Root common/tsconfig.json remains the composite build entry Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
This commit is contained in:
co-authored by
Daniel Imms
parent
43e8365f0e
commit
1d125ac715
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 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/base/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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a single runner on the microtask queue. Unlike {@link TimeoutTimer}, a scheduled
|
||||
* microtask cannot be unqueued; {@link cancel} prevents the runner from executing if it has not
|
||||
* run yet.
|
||||
*/
|
||||
export class MicrotaskTimer implements IDisposable {
|
||||
private _isScheduled = false;
|
||||
private _isDisposed = false;
|
||||
|
||||
public dispose(): void {
|
||||
this.cancel();
|
||||
this._isDisposed = true;
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this._isScheduled = false;
|
||||
}
|
||||
|
||||
public set(runner: () => void): void {
|
||||
if (this._isDisposed) {
|
||||
throw new Error('Calling set on a disposed MicrotaskTimer');
|
||||
}
|
||||
if (this._isScheduled) {
|
||||
return;
|
||||
}
|
||||
this._isScheduled = true;
|
||||
queueMicrotask(() => {
|
||||
if (!this._isScheduled) {
|
||||
return;
|
||||
}
|
||||
this._isScheduled = false;
|
||||
runner();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { Emitter } from 'common/base/Event';
|
||||
|
||||
describe('Emitter', () => {
|
||||
it('should fire with 0 listeners without error', () => {
|
||||
const emitter = new Emitter<number>();
|
||||
emitter.fire(42);
|
||||
});
|
||||
|
||||
it('should fire with 1 listener', () => {
|
||||
const emitter = new Emitter<number>();
|
||||
let received: number | undefined;
|
||||
emitter.event(e => { received = e; });
|
||||
emitter.fire(42);
|
||||
assert.strictEqual(received, 42);
|
||||
});
|
||||
|
||||
it('should fire with 1 listener using thisArgs', () => {
|
||||
const emitter = new Emitter<number>();
|
||||
const obj = { value: 0, handler(e: number) { this.value = e; } };
|
||||
emitter.event(obj.handler, obj);
|
||||
emitter.fire(42);
|
||||
assert.strictEqual(obj.value, 42);
|
||||
});
|
||||
|
||||
it('should fire with multiple listeners', () => {
|
||||
const emitter = new Emitter<number>();
|
||||
const results: number[] = [];
|
||||
emitter.event(e => results.push(e * 1));
|
||||
emitter.event(e => results.push(e * 2));
|
||||
emitter.event(e => results.push(e * 3));
|
||||
emitter.fire(10);
|
||||
assert.deepEqual(results, [10, 20, 30]);
|
||||
});
|
||||
|
||||
it('should handle listener removal during fire', () => {
|
||||
const emitter = new Emitter<number>();
|
||||
const results: string[] = [];
|
||||
emitter.event(() => results.push('first'));
|
||||
const disposable = emitter.event(() => {
|
||||
results.push('second');
|
||||
disposable.dispose();
|
||||
});
|
||||
emitter.event(() => results.push('third'));
|
||||
emitter.fire(1);
|
||||
assert.deepEqual(results, ['first', 'second', 'third']);
|
||||
});
|
||||
|
||||
it('should not fire after dispose', () => {
|
||||
const emitter = new Emitter<number>();
|
||||
let called = false;
|
||||
emitter.event(() => { called = true; });
|
||||
emitter.dispose();
|
||||
emitter.fire(42);
|
||||
assert.strictEqual(called, false);
|
||||
});
|
||||
|
||||
it('should allow disposing a listener', () => {
|
||||
const emitter = new Emitter<number>();
|
||||
let count = 0;
|
||||
const disposable = emitter.event(() => { count++; });
|
||||
emitter.fire(1);
|
||||
disposable.dispose();
|
||||
emitter.fire(2);
|
||||
assert.strictEqual(count, 1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* Minimal event utilities for xterm.js core.
|
||||
* Simplified from VS Code's event.ts - no leak detection/profiling.
|
||||
*/
|
||||
|
||||
import { IDisposable, DisposableStore, toDisposable } from 'common/base/Lifecycle';
|
||||
|
||||
export interface IEvent<T> {
|
||||
(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable;
|
||||
}
|
||||
|
||||
export class Emitter<T> {
|
||||
private _listeners: { fn: (e: T) => any, thisArgs: any }[] = [];
|
||||
private _disposed = false;
|
||||
private _event: IEvent<T> | undefined;
|
||||
|
||||
public get event(): IEvent<T> {
|
||||
if (this._event) {
|
||||
return this._event;
|
||||
}
|
||||
this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {
|
||||
if (this._disposed) {
|
||||
return toDisposable(() => {});
|
||||
}
|
||||
|
||||
const entry = { fn: listener, thisArgs };
|
||||
this._listeners.push(entry);
|
||||
|
||||
const result = toDisposable(() => {
|
||||
const idx = this._listeners.indexOf(entry);
|
||||
if (idx !== -1) {
|
||||
this._listeners.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
|
||||
if (disposables) {
|
||||
if (Array.isArray(disposables)) {
|
||||
disposables.push(result);
|
||||
} else {
|
||||
disposables.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
return this._event;
|
||||
}
|
||||
|
||||
public fire(event: T): void {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
switch (this._listeners.length) {
|
||||
case 0: return;
|
||||
case 1: {
|
||||
const { fn, thisArgs } = this._listeners[0];
|
||||
fn.call(thisArgs, event);
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
// Snapshot listeners to allow modifications during iteration (2+ listeners)
|
||||
const listeners = this._listeners.slice();
|
||||
for (const { fn, thisArgs } of listeners) {
|
||||
fn.call(thisArgs, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
this._disposed = true;
|
||||
this._listeners.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace EventUtils {
|
||||
export function forward<T>(from: IEvent<T>, to: Emitter<T>): IDisposable {
|
||||
return from(e => to.fire(e));
|
||||
}
|
||||
|
||||
export function map<I, O>(event: IEvent<I>, map: (i: I) => O): IEvent<O> {
|
||||
return (listener: (e: O) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {
|
||||
return event(i => listener.call(thisArgs, map(i)), undefined, disposables);
|
||||
};
|
||||
}
|
||||
|
||||
export function any<T>(...events: IEvent<T>[]): IEvent<T>;
|
||||
export function any(...events: IEvent<any>[]): IEvent<void>;
|
||||
export function any<T>(...events: IEvent<T>[]): IEvent<T> {
|
||||
return (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {
|
||||
const store = new DisposableStore();
|
||||
for (const event of events) {
|
||||
store.add(event(e => listener.call(thisArgs, e)));
|
||||
}
|
||||
if (disposables) {
|
||||
if (Array.isArray(disposables)) {
|
||||
disposables.push(store);
|
||||
} else {
|
||||
disposables.add(store);
|
||||
}
|
||||
}
|
||||
return store;
|
||||
};
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* Minimal lifecycle utilities for xterm.js core.
|
||||
* Simplified from VS Code's lifecycle.ts - no tracking/leak detection.
|
||||
*/
|
||||
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export function toDisposable(fn: () => void): IDisposable {
|
||||
return { dispose: fn };
|
||||
}
|
||||
|
||||
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(arg)) {
|
||||
for (const d of arg) {
|
||||
d.dispose();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
arg.dispose();
|
||||
return arg;
|
||||
}
|
||||
|
||||
export function combinedDisposable(...disposables: IDisposable[]): IDisposable {
|
||||
return toDisposable(() => dispose(disposables));
|
||||
}
|
||||
|
||||
export class DisposableStore implements IDisposable {
|
||||
private readonly _disposables = new Set<IDisposable>();
|
||||
private _isDisposed = false;
|
||||
|
||||
public get isDisposed(): boolean {
|
||||
return this._isDisposed;
|
||||
}
|
||||
|
||||
public add<T extends IDisposable>(o: T): T {
|
||||
if (this._isDisposed) {
|
||||
o.dispose();
|
||||
} else {
|
||||
this._disposables.add(o);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
this._isDisposed = true;
|
||||
for (const d of this._disposables) {
|
||||
d.dispose();
|
||||
}
|
||||
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 {
|
||||
this._store.dispose();
|
||||
}
|
||||
|
||||
protected _register<T extends IDisposable>(o: T): T {
|
||||
return this._store.add(o);
|
||||
}
|
||||
}
|
||||
|
||||
export class MutableDisposable<T extends IDisposable> implements IDisposable {
|
||||
private _value: T | undefined;
|
||||
private _isDisposed = false;
|
||||
|
||||
public get value(): T | undefined {
|
||||
return this._isDisposed ? undefined : this._value;
|
||||
}
|
||||
|
||||
public set value(value: T | undefined) {
|
||||
if (this._isDisposed || value === this._value) {
|
||||
return;
|
||||
}
|
||||
this._value?.dispose();
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.value = undefined;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._isDisposed = true;
|
||||
this._value?.dispose();
|
||||
this._value = undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { FourKeyMap, TwoKeyMap } from 'common/base/MultiKeyMap';
|
||||
|
||||
const strictEqual = assert.strictEqual;
|
||||
|
||||
describe('TwoKeyMap', () => {
|
||||
let map: TwoKeyMap<number | string, number | string, string>;
|
||||
|
||||
beforeEach(() => {
|
||||
map = new TwoKeyMap();
|
||||
});
|
||||
|
||||
it('set, get', () => {
|
||||
strictEqual(map.get(1, 2), undefined);
|
||||
map.set(1, 2, 'foo');
|
||||
strictEqual(map.get(1, 2), 'foo');
|
||||
map.set(1, 3, 'bar');
|
||||
strictEqual(map.get(1, 2), 'foo');
|
||||
strictEqual(map.get(1, 3), 'bar');
|
||||
map.set(2, 2, 'foo2');
|
||||
map.set(2, 3, 'bar2');
|
||||
strictEqual(map.get(1, 2), 'foo');
|
||||
strictEqual(map.get(1, 3), 'bar');
|
||||
strictEqual(map.get(2, 2), 'foo2');
|
||||
strictEqual(map.get(2, 3), 'bar2');
|
||||
});
|
||||
it('clear', () => {
|
||||
strictEqual(map.get(1, 2), undefined);
|
||||
map.set(1, 2, 'foo');
|
||||
strictEqual(map.get(1, 2), 'foo');
|
||||
map.clear();
|
||||
strictEqual(map.get(1, 2), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FourKeyMap', () => {
|
||||
let map: FourKeyMap<number | string, number | string, number | string, number | string, string>;
|
||||
|
||||
beforeEach(() => {
|
||||
map = new FourKeyMap();
|
||||
});
|
||||
|
||||
it('set, get', () => {
|
||||
strictEqual(map.get(1, 2, 3, 4), undefined);
|
||||
map.set(1, 2, 3, 4, 'foo');
|
||||
strictEqual(map.get(1, 2, 3, 4), 'foo');
|
||||
map.set(1, 3, 3, 4, 'bar');
|
||||
strictEqual(map.get(1, 2, 3, 4), 'foo');
|
||||
strictEqual(map.get(1, 3, 3, 4), 'bar');
|
||||
map.set(2, 2, 3, 4, 'foo2');
|
||||
map.set(2, 3, 3, 4, 'bar2');
|
||||
strictEqual(map.get(1, 2, 3, 4), 'foo');
|
||||
strictEqual(map.get(1, 3, 3, 4), 'bar');
|
||||
strictEqual(map.get(2, 2, 3, 4), 'foo2');
|
||||
strictEqual(map.get(2, 3, 3, 4), 'bar2');
|
||||
});
|
||||
it('clear', () => {
|
||||
strictEqual(map.get(1, 2, 3, 4), undefined);
|
||||
map.set(1, 2, 3, 4, 'foo');
|
||||
strictEqual(map.get(1, 2, 3, 4), 'foo');
|
||||
map.clear();
|
||||
strictEqual(map.get(1, 2, 3, 4), undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export class TwoKeyMap<TFirst extends string | number, TSecond extends string | number, TValue> {
|
||||
private _data: { [bg: string | number]: { [fg: string | number]: TValue | undefined } | undefined } = {};
|
||||
|
||||
public set(first: TFirst, second: TSecond, value: TValue): void {
|
||||
if (!this._data[first]) {
|
||||
this._data[first] = {};
|
||||
}
|
||||
this._data[first as string | number]![second] = value;
|
||||
}
|
||||
|
||||
public get(first: TFirst, second: TSecond): TValue | undefined {
|
||||
return this._data[first as string | number] ? this._data[first as string | number]![second] : undefined;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._data = {};
|
||||
}
|
||||
}
|
||||
|
||||
export class FourKeyMap<TFirst extends string | number, TSecond extends string | number, TThird extends string | number, TFourth extends string | number, TValue> {
|
||||
private _data: TwoKeyMap<TFirst, TSecond, TwoKeyMap<TThird, TFourth, TValue>> = new TwoKeyMap();
|
||||
|
||||
public set(first: TFirst, second: TSecond, third: TThird, fourth: TFourth, value: TValue): void {
|
||||
if (!this._data.get(first, second)) {
|
||||
this._data.set(first, second, new TwoKeyMap());
|
||||
}
|
||||
this._data.get(first, second)!.set(third, fourth, value);
|
||||
}
|
||||
|
||||
public get(first: TFirst, second: TSecond, third: TThird, fourth: TFourth): TValue | undefined {
|
||||
return this._data.get(first, second)?.get(third, fourth);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._data.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
interface INavigator {
|
||||
userAgent: string;
|
||||
language: string;
|
||||
platform: string;
|
||||
}
|
||||
|
||||
// We're declaring a navigator global here as we expect it in all runtimes (node and browser), but
|
||||
// we want this module to live in common.
|
||||
declare const navigator: INavigator;
|
||||
declare const process: unknown;
|
||||
|
||||
// navigator.userAgent is also checked here because bundling with the process module can cause
|
||||
// issues otherwise. Note that navigator exists in Node.js 21+ but the userAgent is
|
||||
// "Node.js/<version>".
|
||||
export const isNode = (typeof process !== 'undefined' && 'title' in (process as any) && (typeof navigator === 'undefined' || navigator.userAgent.startsWith('Node.js/'))) ? true : false;
|
||||
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;
|
||||
}
|
||||
const majorVersion = userAgent.match(/Version\/(\d+)/);
|
||||
if (majorVersion === null || majorVersion.length < 2) {
|
||||
return 0;
|
||||
}
|
||||
return parseInt(majorVersion[1]);
|
||||
}
|
||||
|
||||
// Find the users platform. We use this to interpret the meta key
|
||||
// and ISO third level shifts.
|
||||
// http://stackoverflow.com/q/19877924/577598
|
||||
export const isMac = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'].includes(platform);
|
||||
export const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].includes(platform);
|
||||
export const isLinux = platform.indexOf('Linux') >= 0;
|
||||
// Note that when this is true, isLinux will also be true.
|
||||
export const isChromeOS = /\bCrOS\b/.test(userAgent);
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { LimitedStringBuilder, StringBuilder } from 'common/base/StringBuilder';
|
||||
|
||||
describe('StringBuilder', () => {
|
||||
it('should start empty', () => {
|
||||
const builder = new StringBuilder();
|
||||
assert.strictEqual(builder.length, 0);
|
||||
assert.strictEqual(builder.toString(), '');
|
||||
});
|
||||
|
||||
it('should append a single chunk', () => {
|
||||
const builder = new StringBuilder();
|
||||
builder.append('hello');
|
||||
assert.strictEqual(builder.length, 5);
|
||||
assert.strictEqual(builder.toString(), 'hello');
|
||||
});
|
||||
|
||||
it('should join multiple chunks in order', () => {
|
||||
const builder = new StringBuilder();
|
||||
builder.append('foo');
|
||||
builder.append('bar');
|
||||
builder.append('baz');
|
||||
assert.strictEqual(builder.length, 9);
|
||||
assert.strictEqual(builder.toString(), 'foobarbaz');
|
||||
});
|
||||
|
||||
it('should handle empty chunks', () => {
|
||||
const builder = new StringBuilder();
|
||||
builder.append('');
|
||||
builder.append('a');
|
||||
builder.append('');
|
||||
assert.strictEqual(builder.length, 1);
|
||||
assert.strictEqual(builder.toString(), 'a');
|
||||
});
|
||||
|
||||
it('should reset accumulated data', () => {
|
||||
const builder = new StringBuilder();
|
||||
builder.append('hello');
|
||||
builder.reset();
|
||||
assert.strictEqual(builder.length, 0);
|
||||
assert.strictEqual(builder.toString(), '');
|
||||
});
|
||||
|
||||
it('should allow appending after reset', () => {
|
||||
const builder = new StringBuilder();
|
||||
builder.append('old');
|
||||
builder.reset();
|
||||
builder.append('new');
|
||||
assert.strictEqual(builder.toString(), 'new');
|
||||
});
|
||||
|
||||
it('should accumulate many small chunks without quadratic concatenation', () => {
|
||||
const builder = new StringBuilder();
|
||||
const chunk = 'x';
|
||||
const count = 10000;
|
||||
for (let i = 0; i < count; i++) {
|
||||
builder.append(chunk);
|
||||
}
|
||||
assert.strictEqual(builder.length, count);
|
||||
assert.strictEqual(builder.toString(), 'x'.repeat(count));
|
||||
});
|
||||
});
|
||||
|
||||
describe('LimitedStringBuilder', () => {
|
||||
it('should expose the configured limit', () => {
|
||||
const builder = new LimitedStringBuilder(42);
|
||||
assert.strictEqual(builder.limit, 42);
|
||||
});
|
||||
|
||||
it('should start empty', () => {
|
||||
const builder = new LimitedStringBuilder(10);
|
||||
assert.strictEqual(builder.length, 0);
|
||||
assert.strictEqual(builder.toString(), '');
|
||||
});
|
||||
|
||||
it('should accept data up to the limit', () => {
|
||||
const builder = new LimitedStringBuilder(10);
|
||||
assert.strictEqual(builder.append('12345'), false);
|
||||
assert.strictEqual(builder.append('67890'), false);
|
||||
assert.strictEqual(builder.length, 10);
|
||||
assert.strictEqual(builder.toString(), '1234567890');
|
||||
});
|
||||
|
||||
it('should accept a single chunk exactly at the limit', () => {
|
||||
const builder = new LimitedStringBuilder(5);
|
||||
assert.strictEqual(builder.append('abcde'), false);
|
||||
assert.strictEqual(builder.length, 5);
|
||||
assert.strictEqual(builder.toString(), 'abcde');
|
||||
});
|
||||
|
||||
it('should reject data exceeding the limit and clear the buffer', () => {
|
||||
const builder = new LimitedStringBuilder(5);
|
||||
builder.append('abc');
|
||||
assert.strictEqual(builder.append('def'), true);
|
||||
assert.strictEqual(builder.length, 0);
|
||||
assert.strictEqual(builder.toString(), '');
|
||||
});
|
||||
|
||||
it('should reject a single chunk larger than the limit', () => {
|
||||
const builder = new LimitedStringBuilder(3);
|
||||
assert.strictEqual(builder.append('toolong'), true);
|
||||
assert.strictEqual(builder.length, 0);
|
||||
assert.strictEqual(builder.toString(), '');
|
||||
});
|
||||
|
||||
it('should allow appending again after reset following a limit breach', () => {
|
||||
const builder = new LimitedStringBuilder(3);
|
||||
assert.strictEqual(builder.append('abcd'), true);
|
||||
builder.reset();
|
||||
assert.strictEqual(builder.append('ab'), false);
|
||||
assert.strictEqual(builder.toString(), 'ab');
|
||||
});
|
||||
|
||||
it('should accumulate many chunks before hitting the limit', () => {
|
||||
const limit = 100;
|
||||
const builder = new LimitedStringBuilder(limit);
|
||||
const chunk = 'A';
|
||||
for (let i = 0; i < limit; i++) {
|
||||
assert.strictEqual(builder.append(chunk), false);
|
||||
}
|
||||
assert.strictEqual(builder.toString(), 'A'.repeat(limit));
|
||||
assert.strictEqual(builder.append('B'), true);
|
||||
assert.strictEqual(builder.toString(), '');
|
||||
});
|
||||
|
||||
it('should reject when limit is zero and any data is appended', () => {
|
||||
const builder = new LimitedStringBuilder(0);
|
||||
assert.strictEqual(builder.append('a'), true);
|
||||
assert.strictEqual(builder.length, 0);
|
||||
});
|
||||
|
||||
it('should allow zero-length appends at the limit', () => {
|
||||
const builder = new LimitedStringBuilder(0);
|
||||
assert.strictEqual(builder.append(''), false);
|
||||
assert.strictEqual(builder.length, 0);
|
||||
assert.strictEqual(builder.toString(), '');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Accumulates string data from multiple chunks without O(n²) string concatenation.
|
||||
*/
|
||||
export class StringBuilder {
|
||||
private _chunks: string[] = [];
|
||||
private _length = 0;
|
||||
|
||||
public get length(): number {
|
||||
return this._length;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._chunks.length = 0;
|
||||
this._length = 0;
|
||||
}
|
||||
|
||||
public append(chunk: string): void {
|
||||
this._chunks.push(chunk);
|
||||
this._length += chunk.length;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this._chunks.join('');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* String builder that rejects payloads larger than a fixed limit.
|
||||
*/
|
||||
export class LimitedStringBuilder {
|
||||
private readonly _builder = new StringBuilder();
|
||||
|
||||
constructor(private readonly _limit: number) { }
|
||||
|
||||
public get length(): number {
|
||||
return this._builder.length;
|
||||
}
|
||||
|
||||
public get limit(): number {
|
||||
return this._limit;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._builder.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if the limit was exceeded (buffer is cleared in that case)
|
||||
*/
|
||||
public append(chunk: string): boolean {
|
||||
this._builder.append(chunk);
|
||||
if (this._builder.length > this._limit) {
|
||||
this._builder.reset();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this._builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* The xterm.js version. This is updated by the publish script from package.json.
|
||||
*/
|
||||
export const XTERM_VERSION = '6.0.0';
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": ["../tsconfig.json"],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["./**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user