From c727240f16305660217651ee76a573c4c29f6990 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:23:38 +0900 Subject: [PATCH] Fix cyclic dependency Linkifier -> MouseService -> SelectionService -> Linkifier --- .../components/window/cellInspectorWindow.ts | 2 +- src/browser/CoreBrowserTerminal.ts | 7 ++- src/browser/Linkifier.ts | 16 +++---- src/browser/TestUtils.test.ts | 4 ++ src/browser/services/MouseCoordsService.ts | 47 +++++++++++++++++++ src/browser/services/MouseService.ts | 37 ++------------- src/browser/services/SelectionService.ts | 8 ++-- src/browser/services/Services.ts | 20 +++++--- 8 files changed, 87 insertions(+), 54 deletions(-) create mode 100644 src/browser/services/MouseCoordsService.ts diff --git a/demo/client/components/window/cellInspectorWindow.ts b/demo/client/components/window/cellInspectorWindow.ts index b7a50bf6d..8bc28a974 100644 --- a/demo/client/components/window/cellInspectorWindow.ts +++ b/demo/client/components/window/cellInspectorWindow.ts @@ -85,7 +85,7 @@ export class CellInspectorWindow extends BaseWindow implements IControlWindow { terminal.element.addEventListener('mousemove', (e: MouseEvent) => { const core = (terminal as any)._core; - const coords = core._mouseService?.getCoords(e, core.screenElement, terminal.cols, terminal.rows); + const coords = core._mouseCoordsService?.getCoords(e, core.screenElement, terminal.cols, terminal.rows); if (!coords) { this._clearDisplay(); return; diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index 207e469d3..62ff36c29 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -36,10 +36,11 @@ import { CharSizeService } from 'browser/services/CharSizeService'; import { CharacterJoinerService } from 'browser/services/CharacterJoinerService'; import { CoreBrowserService } from 'browser/services/CoreBrowserService'; import { LinkProviderService } from 'browser/services/LinkProviderService'; +import { MouseCoordsService } from 'browser/services/MouseCoordsService'; import { MouseService } from 'browser/services/MouseService'; import { RenderService } from 'browser/services/RenderService'; import { SelectionService } from 'browser/services/SelectionService'; -import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services'; +import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseCoordsService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services'; import { ThemeService } from 'browser/services/ThemeService'; import { KeyboardService } from 'browser/services/KeyboardService'; import { channels, color, rgb } from 'common/Color'; @@ -87,6 +88,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { // Optional browser services private _charSizeService: ICharSizeService | undefined; private _coreBrowserService: ICoreBrowserService | undefined; + private _mouseCoordsService: IMouseCoordsService | undefined; private _mouseService: IMouseService | undefined; private _renderService: IRenderService | undefined; private _themeService: IThemeService | undefined; @@ -543,6 +545,9 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView); this._helperContainer.appendChild(this._compositionView); + this._mouseCoordsService = this._instantiationService.createInstance(MouseCoordsService); + this._instantiationService.setService(IMouseCoordsService, this._mouseCoordsService); + const linkifier = this._linkifier.value = this._register(this._instantiationService.createInstance(Linkifier, this.screenElement)); // Performance: Add viewport and helper elements from the fragment diff --git a/src/browser/Linkifier.ts b/src/browser/Linkifier.ts index 5e43782d4..31c91daee 100644 --- a/src/browser/Linkifier.ts +++ b/src/browser/Linkifier.ts @@ -7,7 +7,7 @@ import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifie import { Disposable, dispose, toDisposable } from 'common/Lifecycle'; import { IDisposable } from 'common/Types'; import { IBufferService } from 'common/services/Services'; -import { ILinkProviderService, IMouseService, IRenderService } from './services/Services'; +import { ILinkProviderService, IMouseCoordsService, IRenderService } from './services/Services'; import { Emitter } from 'common/Event'; import { addDisposableListener } from 'browser/Dom'; @@ -30,7 +30,7 @@ export class Linkifier extends Disposable implements ILinkifier2 { constructor( private readonly _element: HTMLElement, - @IMouseService private readonly _mouseService: IMouseService, + @IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService, @IRenderService private readonly _renderService: IRenderService, @IBufferService private readonly _bufferService: IBufferService, @ILinkProviderService private readonly _linkProviderService: ILinkProviderService @@ -60,7 +60,7 @@ export class Linkifier extends Disposable implements ILinkifier2 { private _handleMouseMove(event: MouseEvent): void { this._lastMouseEvent = event; - const position = this._positionFromMouseEvent(event, this._element, this._mouseService); + const position = this._positionFromMouseEvent(event, this._element); if (!position) { return; } @@ -222,7 +222,7 @@ export class Linkifier extends Disposable implements ILinkifier2 { return; } - const position = this._positionFromMouseEvent(event, this._element, this._mouseService); + const position = this._positionFromMouseEvent(event, this._element); if (!position) { return; } @@ -251,7 +251,7 @@ export class Linkifier extends Disposable implements ILinkifier2 { return; } - const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService); + const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element); if (!position) { return; @@ -312,7 +312,7 @@ export class Linkifier extends Disposable implements ILinkifier2 { this._clearCurrentLink(start, end); if (this._lastMouseEvent) { // re-eval previously active link after changes - const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!); + const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element); if (position) { this._askForLink(position, false); } @@ -378,8 +378,8 @@ export class Linkifier extends Disposable implements ILinkifier2 { * Get the buffer position from a mouse event * @param event */ - private _positionFromMouseEvent(event: MouseEvent, element: HTMLElement, mouseService: IMouseService): IBufferCellPosition | undefined { - const coords = mouseService.getCoords(event, element, this._bufferService.cols, this._bufferService.rows); + private _positionFromMouseEvent(event: MouseEvent, element: HTMLElement): IBufferCellPosition | undefined { + const coords = this._mouseCoordsService.getCoords(event, element, this._bufferService.cols, this._bufferService.rows); if (!coords) { return; } diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index a9acf889d..a682913e2 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -393,6 +393,10 @@ export class MockMouseService implements IMouseService { throw new Error('Not implemented'); } + public setCustomWheelEventHandler(): void { + throw new Error('Not implemented'); + } + public bindMouse(): void { throw new Error('Not implemented'); } diff --git a/src/browser/services/MouseCoordsService.ts b/src/browser/services/MouseCoordsService.ts new file mode 100644 index 000000000..e2455fd2b --- /dev/null +++ b/src/browser/services/MouseCoordsService.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2026 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { getWindow } from 'browser/Dom'; +import { getCoords, getCoordsRelativeToElement } from 'browser/input/Mouse'; +import { ICharSizeService, IMouseCoordsService, IRenderService } from 'browser/services/Services'; + +export class MouseCoordsService implements IMouseCoordsService { + public serviceBrand: undefined; + + constructor( + @ICharSizeService private readonly _charSizeService: ICharSizeService, + @IRenderService private readonly _renderService: IRenderService + ) { + } + + public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined { + return getCoords( + window, + event, + element, + colCount, + rowCount, + this._charSizeService.hasValidSize, + this._renderService.dimensions.css.cell.width, + this._renderService.dimensions.css.cell.height, + isSelection + ); + } + + public getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined { + const coords = getCoordsRelativeToElement(getWindow(element), event, element); + if (!this._charSizeService.hasValidSize) { + return undefined; + } + coords[0] = Math.min(Math.max(coords[0], 0), this._renderService.dimensions.css.canvas.width - 1); + coords[1] = Math.min(Math.max(coords[1], 0), this._renderService.dimensions.css.canvas.height - 1); + return { + col: Math.floor(coords[0] / this._renderService.dimensions.css.cell.width), + row: Math.floor(coords[1] / this._renderService.dimensions.css.cell.height), + x: Math.floor(coords[0]), + y: Math.floor(coords[1]) + }; + } +} diff --git a/src/browser/services/MouseService.ts b/src/browser/services/MouseService.ts index 005dd9e45..f53f22a57 100644 --- a/src/browser/services/MouseService.ts +++ b/src/browser/services/MouseService.ts @@ -3,14 +3,13 @@ * @license MIT */ -import { addDisposableListener, getWindow } from 'browser/Dom'; -import { getCoords, getCoordsRelativeToElement } from 'browser/input/Mouse'; +import { addDisposableListener } from 'browser/Dom'; import { IBufferService, ICoreMouseService, ICoreService, ILogService, IOptionsService } from 'common/services/Services'; import { CoreMouseAction, CoreMouseButton, CoreMouseEventType, IDisposable } from 'common/Types'; import { C0 } from 'common/data/EscapeSequences'; import { toDisposable } from 'common/Lifecycle'; import { CustomWheelEventHandler } from 'browser/Types'; -import { ICharSizeService, ICoreBrowserService, IMouseService, IMouseServiceTarget, IRenderService, ISelectionService } from './Services'; +import { ICoreBrowserService, IMouseCoordsService, IMouseService, IMouseServiceTarget, IRenderService, ISelectionService } from './Services'; type RequestedMouseEvents = Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener | null>; @@ -27,7 +26,7 @@ export class MouseService implements IMouseService { constructor( @IRenderService private readonly _renderService: IRenderService, - @ICharSizeService private readonly _charSizeService: ICharSizeService, + @IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService, @ICoreMouseService private readonly _coreMouseService: ICoreMouseService, @ICoreService private readonly _coreService: ICoreService, @IBufferService private readonly _bufferService: IBufferService, @@ -87,7 +86,7 @@ export class MouseService implements IMouseService { private _sendEvent(ctx: IMouseBindContext, ev: MouseEvent | WheelEvent): boolean { // Get mouse coordinates - const pos = this.getMouseReportCoords(ev as MouseEvent, ctx.target.screenElement); + const pos = this._mouseCoordsService.getMouseReportCoords(ev as MouseEvent, ctx.target.screenElement); if (!pos) { return false; } @@ -324,32 +323,4 @@ export class MouseService implements IMouseService { public setCustomWheelEventHandler(customWheelEventHandler: CustomWheelEventHandler | undefined): void { this._customWheelEventHandler = customWheelEventHandler; } - public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined { - return getCoords( - window, - event, - element, - colCount, - rowCount, - this._charSizeService.hasValidSize, - this._renderService.dimensions.css.cell.width, - this._renderService.dimensions.css.cell.height, - isSelection - ); - } - - public getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined { - const coords = getCoordsRelativeToElement(getWindow(element), event, element); - if (!this._charSizeService.hasValidSize) { - return undefined; - } - coords[0] = Math.min(Math.max(coords[0], 0), this._renderService.dimensions.css.canvas.width - 1); - coords[1] = Math.min(Math.max(coords[1], 0), this._renderService.dimensions.css.canvas.height - 1); - return { - col: Math.floor(coords[0] / this._renderService.dimensions.css.cell.width), - row: Math.floor(coords[1] / this._renderService.dimensions.css.cell.height), - x: Math.floor(coords[0]), - y: Math.floor(coords[1]) - }; - } } diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index a5d1d759b..e1b8510e9 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -8,7 +8,7 @@ import { getCoordsRelativeToElement } from 'browser/input/Mouse'; import { moveToCellSequence } from 'browser/input/MoveToCell'; import { SelectionModel } from 'browser/selection/SelectionModel'; import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; -import { ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services'; +import { ICoreBrowserService, IMouseCoordsService, IRenderService, ISelectionService } from 'browser/services/Services'; import { Disposable, toDisposable } from 'common/Lifecycle'; import * as Browser from 'common/Platform'; import { IBufferLine, ICellData, IDisposable } from 'common/Types'; @@ -126,7 +126,7 @@ export class SelectionService extends Disposable implements ISelectionService { private readonly _linkifier: ILinkifier2, @IBufferService private readonly _bufferService: IBufferService, @ICoreService private readonly _coreService: ICoreService, - @IMouseService private readonly _mouseService: IMouseService, + @IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService, @IOptionsService private readonly _optionsService: IOptionsService, @IRenderService private readonly _renderService: IRenderService, @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService @@ -395,7 +395,7 @@ export class SelectionService extends Disposable implements ISelectionService { * @param event The mouse event. */ private _getMouseBufferCoords(event: MouseEvent): [number, number] | undefined { - const coords = this._mouseService.getCoords(event, this._screenElement, this._bufferService.cols, this._bufferService.rows, true); + const coords = this._mouseCoordsService.getCoords(event, this._screenElement, this._bufferService.cols, this._bufferService.rows, true); if (!coords) { return undefined; } @@ -715,7 +715,7 @@ export class SelectionService extends Disposable implements ISelectionService { if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME && event.altKey && this._optionsService.rawOptions.altClickMovesCursor) { if (this._bufferService.buffer.ybase === this._bufferService.buffer.ydisp) { - const coordinates = this._mouseService.getCoords( + const coordinates = this._mouseCoordsService.getCoords( event, this._element, this._bufferService.cols, diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 1c75df61e..7f1c10576 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -49,20 +49,26 @@ export interface ICoreBrowserService { readonly dpr: number; } -export const IMouseService = createDecorator('MouseService'); -export interface IMouseServiceTarget { - element: HTMLElement; - screenElement: HTMLElement; - document: Document; -} -export interface IMouseService { +export const IMouseCoordsService = createDecorator('MouseCoordsService'); +export interface IMouseCoordsService { serviceBrand: undefined; getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined; getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined; +} + +export const IMouseService = createDecorator('MouseService'); +export interface IMouseService { + serviceBrand: undefined; + setCustomWheelEventHandler(customWheelEventHandler: CustomWheelEventHandler | undefined): void; bindMouse(target: IMouseServiceTarget, register: (disposable: IDisposable) => void, focus: () => void): void; } +export interface IMouseServiceTarget { + element: HTMLElement; + screenElement: HTMLElement; + document: Document; +} export const IRenderService = createDecorator('RenderService'); export interface IRenderService extends IDisposable {