Add oxlint import/no-cycle and break existing src/common cycles

Enable oxlint's import/no-cycle rule (import plugin) so circular module
dependencies are caught in CI, similar to eslint-plugin-import/no-cycle
and biome no-import-cycles discussed in the issue.

Relocate shared types and dependencies to remove cycles under src/common:

- Types.ts: host IInsertEvent, IDeleteEvent, IParams, UnderlineStyle,
  IBuffer, and IBufferSet; drop imports from CircularList, buffer/Types,
  parser/Types, and Services
- CoreTerminal.ts: define and export ICoreTerminal
- ServiceRegistry.ts: define IServiceIdentifier (break Services registry loop)
- UnicodeService: stop constructing UnicodeV6; CoreTerminal registers the
  default v6 provider after service creation
- buffer/Types.ts and parser/Types.ts: re-export moved types for callers

Fixes https://github.com/xtermjs/xterm.js/issues/5897

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Daniel Imms
2026-05-31 10:08:04 -07:00
co-authored by Cursor
parent 4aa04c6b3f
commit 56a7195487
18 changed files with 139 additions and 137 deletions
+3 -1
View File
@@ -1,6 +1,7 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": [
"import",
"typescript"
],
"categories": {
@@ -104,7 +105,8 @@
"typescript/no-wrapper-object-types": "error",
"typescript/prefer-as-const": "error",
"typescript/prefer-namespace-keyword": "error",
"typescript/triple-slash-reference": "error"
"typescript/triple-slash-reference": "error",
"import/no-cycle": "error"
},
"overrides": [
{
+2 -1
View File
@@ -3,7 +3,8 @@
* @license MIT
*/
import { CharData, IColor, ICoreTerminal, ITerminalOptions } from '../common/Types';
import { CharData, IColor, ITerminalOptions } from '../common/Types';
import { ICoreTerminal } from '../common/CoreTerminal';
import { IBuffer } from '../common/buffer/Types';
import { IDisposable, IRenderDimensions as IRenderDimensionsApi, Terminal as ITerminalApi } from '@xterm/xterm';
import { channels, css } from '../common/Color';
+2 -10
View File
@@ -3,19 +3,11 @@
* @license MIT
*/
import { ICircularList } from './Types';
import { ICircularList, IDeleteEvent, IInsertEvent } from './Types';
import { Disposable } from './Lifecycle';
import { Emitter } from './Event';
export interface IInsertEvent {
index: number;
amount: number;
}
export interface IDeleteEvent {
index: number;
amount: number;
}
export type { IDeleteEvent, IInsertEvent } from './Types';
/**
* Represents a circular list; a list with a maximum size that wraps around when push is called,
+19 -3
View File
@@ -21,18 +21,19 @@
* http://linux.die.net/man/7/urxvt
*/
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, IMouseStateService, IUnicodeService, LogLevelEnum, ITerminalOptions, IOscLinkService } from './services/Services';
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, IMouseStateService, IUnicodeService, LogLevelEnum, IOscLinkService } from './services/Services';
import { InstantiationService } from './services/InstantiationService';
import { LogService } from './services/LogService';
import { BufferService, BufferServiceConstants } from './services/BufferService';
import { OptionsService } from './services/OptionsService';
import { IDisposable, IAttributeData, ICoreTerminal, IScrollEvent } from './Types';
import { IDisposable, IAttributeData, IScrollEvent, ITerminalOptions, IParams } from './Types';
import { CoreService } from './services/CoreService';
import { MouseStateService } from './services/MouseStateService';
import { UnicodeV6 } from './input/UnicodeV6';
import { UnicodeService } from './services/UnicodeService';
import { CharsetService } from './services/CharsetService';
import { updateWindowsModeWrappedState } from './WindowsMode';
import { IFunctionIdentifier, IParams } from './parser/Types';
import { IFunctionIdentifier } from './parser/Types';
import { IBufferSet } from './buffer/Types';
import { InputHandler } from './InputHandler';
import { WriteBuffer } from './input/WriteBuffer';
@@ -43,6 +44,20 @@ import { Disposable, MutableDisposable, toDisposable } from './Lifecycle';
// Only trigger this warning a single time per session
let hasWriteSyncWarnHappened = false;
export interface ICoreTerminal {
mouseStateService: IMouseStateService;
coreService: ICoreService;
optionsService: IOptionsService;
unicodeService: IUnicodeService;
buffers: IBufferSet;
options: Required<ITerminalOptions>;
registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable;
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable;
registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable;
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
}
export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
protected readonly _instantiationService: IInstantiationService;
protected readonly _bufferService: IBufferService;
@@ -116,6 +131,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this.mouseStateService = this._register(this._instantiationService.createInstance(MouseStateService));
this._instantiationService.setService(IMouseStateService, this.mouseStateService);
this.unicodeService = this._register(this._instantiationService.createInstance(UnicodeService));
this.unicodeService.register(new UnicodeV6());
this._instantiationService.setService(IUnicodeService, this.unicodeService);
this._charsetService = this._instantiationService.createInstance(CharsetService);
this._instantiationService.setService(ICharsetService, this._charsetService);
+89 -17
View File
@@ -3,26 +3,51 @@
* @license MIT
*/
import { IDeleteEvent, IInsertEvent } from './CircularList';
import { UnderlineStyle } from './buffer/Constants';
import { IBufferSet } from './buffer/Types';
import { IParams } from './parser/Types';
import { IMouseStateService, ICoreService, IOptionsService, IUnicodeService } from './services/Services';
import { IFunctionIdentifier, ITerminalOptions as IPublicTerminalOptions } from '@xterm/xterm';
import type { Emitter, IEvent } from './Event';
export interface ICoreTerminal {
mouseStateService: IMouseStateService;
coreService: ICoreService;
optionsService: IOptionsService;
unicodeService: IUnicodeService;
buffers: IBufferSet;
options: Required<ITerminalOptions>;
registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable;
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable;
registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable;
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
export interface IInsertEvent {
index: number;
amount: number;
}
export interface IDeleteEvent {
index: number;
amount: number;
}
/** sequence params serialized to js arrays */
export type ParamsArray = (number | number[])[];
/** Interface of Params storage class. */
export interface IParams {
/** from ctor */
maxLength: number;
maxSubParamsLength: number;
/** param values and its length */
params: Int32Array;
length: number;
/** methods */
clone(): IParams;
toArray(): ParamsArray;
reset(): void;
resetZdm(): void;
addParam(value: number): void;
addSubParam(value: number): void;
hasSubParams(idx: number): boolean;
getSubParams(idx: number): Int32Array | null;
getSubParamsAll(): {[idx: number]: Int32Array};
}
export const enum UnderlineStyle {
NONE = 0,
SINGLE = 1,
DOUBLE = 2,
CURLY = 3,
DOTTED = 4,
DASHED = 5
}
export interface IDisposable {
@@ -260,6 +285,53 @@ export interface IMarker extends IDisposable {
readonly line: number;
onDispose: IEvent<void>;
}
export interface IBuffer {
readonly lines: ICircularList<IBufferLine>;
ydisp: number;
ybase: number;
y: number;
x: number;
tabs: any;
scrollBottom: number;
scrollTop: number;
hasScrollback: boolean;
savedY: number;
savedX: number;
savedCharset: ICharset | undefined;
savedCharsets: (ICharset | undefined)[];
savedGlevel: number;
savedOriginMode: boolean;
savedWraparoundMode: boolean;
savedCurAttrData: IAttributeData;
isCursorInViewport: boolean;
markers: IMarker[];
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
getWrappedRangeForLine(y: number): { first: number, last: number };
nextStop(x?: number): number;
prevStop(x?: number): number;
getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine;
getNullCell(attr?: IAttributeData): ICellData;
getWhitespaceCell(attr?: IAttributeData): ICellData;
addMarker(y: number): IMarker;
clearMarkers(y: number): void;
clearAllMarkers(): void;
}
export interface IBufferSet extends IDisposable {
alt: IBuffer;
normal: IBuffer;
active: IBuffer;
onBufferActivate: IEvent<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>;
activateNormalBuffer(): void;
activateAltBuffer(fillAttr?: IAttributeData): void;
reset(): void;
resize(newCols: number, newRows: number): void;
setupTabStops(i?: number): void;
}
export interface IModes {
insertMode: boolean;
}
+1 -8
View File
@@ -147,11 +147,4 @@ export const enum ExtFlags {
VARIANT_OFFSET = 0xE0000000
}
export const enum UnderlineStyle {
NONE = 0,
SINGLE = 1,
DOUBLE = 2,
CURLY = 3,
DOTTED = 4,
DASHED = 5
}
export { UnderlineStyle } from '../Types';
+1 -48
View File
@@ -3,54 +3,7 @@
* @license MIT
*/
import { IAttributeData, ICircularList, IBufferLine, ICellData, IMarker, ICharset, IDisposable } from '../Types';
import type { IEvent } from '../Event';
export type { IBuffer, IBufferSet } from '../Types';
// BufferIndex denotes a position in the buffer: [rowIndex, colIndex]
export type BufferIndex = [number, number];
export interface IBuffer {
readonly lines: ICircularList<IBufferLine>;
ydisp: number;
ybase: number;
y: number;
x: number;
tabs: any;
scrollBottom: number;
scrollTop: number;
hasScrollback: boolean;
savedY: number;
savedX: number;
savedCharset: ICharset | undefined;
savedCharsets: (ICharset | undefined)[];
savedGlevel: number;
savedOriginMode: boolean;
savedWraparoundMode: boolean;
savedCurAttrData: IAttributeData;
isCursorInViewport: boolean;
markers: IMarker[];
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
getWrappedRangeForLine(y: number): { first: number, last: number };
nextStop(x?: number): number;
prevStop(x?: number): number;
getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine;
getNullCell(attr?: IAttributeData): ICellData;
getWhitespaceCell(attr?: IAttributeData): ICellData;
addMarker(y: number): IMarker;
clearMarkers(y: number): void;
clearAllMarkers(): void;
}
export interface IBufferSet extends IDisposable {
alt: IBuffer;
normal: IBuffer;
active: IBuffer;
onBufferActivate: IEvent<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>;
activateNormalBuffer(): void;
activateAltBuffer(fillAttr?: IAttributeData): void;
reset(): void;
resize(newCols: number, newRows: number): void;
setupTabStops(i?: number): void;
}
+1
View File
@@ -132,6 +132,7 @@ export class UnicodeV6 implements IUnicodeVersionProvider {
public charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties {
let width = this.wcwidth(codepoint);
let shouldJoin = width === 0 && preceding !== 0;
// HACK: Ideally this file would not depend on the service which uses it
if (shouldJoin) {
const oldWidth = UnicodeService.extractWidth(preceding);
if (oldWidth === 0) {
+2 -26
View File
@@ -3,12 +3,10 @@
* @license MIT
*/
import { IDisposable } from '../Types';
import { IDisposable, IParams, ParamsArray } from '../Types';
import { ParserState } from './Constants';
/** sequence params serialized to js arrays */
export type ParamsArray = (number | number[])[];
export type { IParams, ParamsArray };
/** Params constructor type. */
export interface IParamsConstructor {
@@ -18,28 +16,6 @@ export interface IParamsConstructor {
fromArray(values: ParamsArray): IParams;
}
/** Interface of Params storage class. */
export interface IParams {
/** from ctor */
maxLength: number;
maxSubParamsLength: number;
/** param values and its length */
params: Int32Array;
length: number;
/** methods */
clone(): IParams;
toArray(): ParamsArray;
reset(): void;
resetZdm(): void;
addParam(value: number): void;
addSubParam(value: number): void;
hasSubParams(idx: number): boolean;
getSubParams(idx: number): Int32Array | null;
getSubParamsAll(): {[idx: number]: Int32Array};
}
/**
* Internal state of EscapeSequenceParser.
* Used as argument of the error handler to allow
+1 -1
View File
@@ -5,7 +5,7 @@
import { IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi } from '@xterm/xterm';
import { BufferApiView } from './BufferApiView';
import { ICoreTerminal } from '../Types';
import { ICoreTerminal } from '../CoreTerminal';
import { Disposable } from '../Lifecycle';
import { Emitter } from '../Event';
+1 -1
View File
@@ -5,7 +5,7 @@
import { IParams } from '../parser/Types';
import { IDisposable, IFunctionIdentifier, IParser } from '@xterm/xterm';
import { ICoreTerminal } from '../Types';
import { ICoreTerminal } from '../CoreTerminal';
export class ParserApi implements IParser {
constructor(private _core: ICoreTerminal) { }
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ICoreTerminal } from '../Types';
import { ICoreTerminal } from '../CoreTerminal';
import { IUnicodeHandling, IUnicodeVersionProvider } from '@xterm/xterm';
export class UnicodeApi implements IUnicodeHandling {
+2 -2
View File
@@ -9,8 +9,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IInstantiationService, IServiceIdentifier } from './Services';
import { getServiceDependencies } from './ServiceRegistry';
import { IInstantiationService } from './Services';
import { IServiceIdentifier, getServiceDependencies } from './ServiceRegistry';
export class ServiceCollection {
+5 -1
View File
@@ -9,7 +9,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IServiceIdentifier } from './Services';
export interface IServiceIdentifier<T> {
(...args: any[]): void;
type: T;
_id: string;
}
const enum Constants {
DI_TARGET = 'di$target',
+1 -7
View File
@@ -6,7 +6,7 @@
import type { IDecoration, IDecorationOptions, ILinkHandler, ILogger, IWindowsPty, IOverviewRulerOptions } from '@xterm/xterm';
import { CoreMouseEncoding, CoreMouseEventType, CursorInactiveStyle, CursorStyle, IAttributeData, ICharset, IColor, ICoreMouseEvent, ICoreMouseProtocol, IDecPrivateModes, IDisposable, IKittyKeyboardState, IModes, IOscLinkData, IWindowOptions } from '../Types';
import { IBuffer, IBufferSet } from '../buffer/Types';
import { createDecorator } from './ServiceRegistry';
import { createDecorator, IServiceIdentifier } from './ServiceRegistry';
import type { Emitter, IEvent } from '../Event';
export const IBufferService = createDecorator<IBufferService>('BufferService');
@@ -119,12 +119,6 @@ export interface ICharsetService {
setgCharset(g: number, charset: ICharset | undefined): void;
}
export interface IServiceIdentifier<T> {
(...args: any[]): void;
type: T;
_id: string;
}
export interface IBrandedService {
serviceBrand: undefined;
}
@@ -4,6 +4,7 @@
*/
import { assert } from 'chai';
import { UnicodeV6 } from '../input/UnicodeV6';
import { UnicodeService } from './UnicodeService';
import { IUnicodeVersionProvider } from './Services';
@@ -21,6 +22,7 @@ describe('unicode provider', () => {
let us: UnicodeService;
beforeEach(() => {
us = new UnicodeService();
us.register(new UnicodeV6());
});
it('default to V6', () => {
assert.equal(us.activeVersion, '6');
+4 -9
View File
@@ -3,7 +3,6 @@
* @license MIT
*/
import { UnicodeV6 } from '../input/UnicodeV6';
import { IUnicodeService, IUnicodeVersionProvider, UnicodeCharProperties, UnicodeCharWidth } from './Services';
import { Emitter } from '../Event';
@@ -12,7 +11,7 @@ export class UnicodeService implements IUnicodeService {
private _providers: {[key: string]: IUnicodeVersionProvider} = Object.create(null);
private _active: string = '';
private _activeProvider: IUnicodeVersionProvider;
private _activeProvider!: IUnicodeVersionProvider;
private readonly _onChange = new Emitter<string>();
public readonly onChange = this._onChange.event;
@@ -30,13 +29,6 @@ export class UnicodeService implements IUnicodeService {
return ((state & 0xffffff) << 3) | ((width & 3) << 1) | (shouldJoin?1:0);
}
constructor() {
const defaultProvider = new UnicodeV6();
this.register(defaultProvider);
this._active = defaultProvider.version;
this._activeProvider = defaultProvider;
}
public dispose(): void {
this._onChange.dispose();
}
@@ -60,6 +52,9 @@ export class UnicodeService implements IUnicodeService {
public register(provider: IUnicodeVersionProvider): void {
this._providers[provider.version] = provider;
if (!this._active) {
this.activeVersion = provider.version;
}
}
/**
+2 -1
View File
@@ -7,7 +7,8 @@ import { Browser, JSHandle, Page } from '@playwright/test';
import { deepStrictEqual, strictEqual } from 'assert';
import type { IRenderDimensions as IRenderDimensionsInternal } from '../../src/browser/renderer/shared/Types';
import type { IRenderService } from '../../src/browser/services/Services';
import type { ICoreTerminal, IDisposable, IMarker } from '../../src/common/Types';
import type { IDisposable, IMarker } from '../../src/common/Types';
import type { ICoreTerminal } from '../../src/common/CoreTerminal';
import * as playwright from '@playwright/test';
import { IBuffer, IBufferCell, IBufferLine, IBufferNamespace, IBufferRange, IDecoration, IDecorationOptions, IModes, IRenderDimensions, ITerminalInitOnlyOptions, ITerminalOptions, Terminal } from '@xterm/xterm';