diff --git a/addons/addon-ligatures/yarn.lock b/addons/addon-ligatures/yarn.lock index 9608e035..2c910bca 100644 --- a/addons/addon-ligatures/yarn.lock +++ b/addons/addon-ligatures/yarn.lock @@ -63,6 +63,14 @@ buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -79,6 +87,42 @@ diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" @@ -115,22 +159,77 @@ font-ligatures@^1.4.1: opentype.js "^0.8.0" form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + version "4.0.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4" + integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + hasown "^2.0.2" mime-types "^2.1.12" +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.2.6: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + get-system-fonts@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/get-system-fonts/-/get-system-fonts-2.0.0.tgz#a43b9a33f05c0715a60176d2aad5ce6e98f0a3c6" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -168,6 +267,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" diff --git a/addons/addon-search/src/SearchAddon.ts b/addons/addon-search/src/SearchAddon.ts index 72ca483c..3ea5d9ad 100644 --- a/addons/addon-search/src/SearchAddon.ts +++ b/addons/addon-search/src/SearchAddon.ts @@ -5,7 +5,7 @@ import type { Terminal, IDisposable, ITerminalAddon, IDecoration } from '@xterm/xterm'; import type { SearchAddon as ISearchApi, ISearchOptions, ISearchDecorationOptions } from '@xterm/addon-search'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { combinedDisposable, Disposable, dispose, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; interface IInternalSearchOptions { @@ -17,6 +17,11 @@ export interface ISearchPosition { startRow: number; } +export interface ISearchResultChangeEvent { + resultIndex: number; + resultCount: number; +} + export interface ISearchAddonOptions { highlightLimit: number; } @@ -49,11 +54,32 @@ interface IMultiHighlight extends IDisposable { match: ISearchResult; } -const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?'; -const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs -const DEFAULT_HIGHLIGHT_LIMIT = 1000; +/** + * Configuration constants for the search addon functionality. + */ +const enum Constants { + /** + * Characters that are considered non-word characters for search boundary detection. These + * characters are used to determine word boundaries when performing whole-word searches. Includes + * common punctuation, symbols, and whitespace characters. + */ + NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?', -export class SearchAddon extends Disposable implements ITerminalAddon , ISearchApi { + /** + * Time-to-live for cached search results in milliseconds. After this duration, cached search + * results will be invalidated to ensure they remain consistent with terminal content changes. + */ + LINES_CACHE_TIME_TO_LIVE = 15000, + + /** + * Default maximum number of search results to highlight simultaneously. This limit prevents + * performance degradation when searching for very common terms that would result in excessive + * highlighting decorations. + */ + DEFAULT_HIGHLIGHT_LIMIT = 1000 +} + +export class SearchAddon extends Disposable implements ITerminalAddon, ISearchApi { private _terminal: Terminal | undefined; private _cachedSearchTerm: string | undefined; private _highlightedLines: Set = new Set(); @@ -72,13 +98,13 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA private _linesCacheTimeoutId = 0; private _linesCacheDisposables = new MutableDisposable(); - private readonly _onDidChangeResults = this._register(new Emitter<{ resultIndex: number, resultCount: number }>()); - public readonly onDidChangeResults = this._onDidChangeResults.event; + private readonly _onDidChangeResults = this._register(new Emitter()); + public get onDidChangeResults(): Event { return this._onDidChangeResults.event; } constructor(options?: Partial) { super(); - this._highlightLimit = options?.highlightLimit ?? DEFAULT_HIGHLIGHT_LIMIT; + this._highlightLimit = options?.highlightLimit ?? Constants.DEFAULT_HIGHLIGHT_LIMIT; } public activate(terminal: Terminal): void { @@ -435,7 +461,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA } window.clearTimeout(this._linesCacheTimeoutId); - this._linesCacheTimeoutId = window.setTimeout(() => this._destroyLinesCache(), LINES_CACHE_TIME_TO_LIVE); + this._linesCacheTimeoutId = window.setTimeout(() => this._destroyLinesCache(), Constants.LINES_CACHE_TIME_TO_LIVE); } private _destroyLinesCache(): void { @@ -455,8 +481,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA * @param term the substring that starts at searchIndex */ private _isWholeWord(searchIndex: number, line: string, term: string): boolean { - return ((searchIndex === 0) || (NON_WORD_CHARACTERS.includes(line[searchIndex - 1]))) && - (((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.includes(line[searchIndex + term.length]))); + return ((searchIndex === 0) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex - 1]))) && + (((searchIndex + term.length) === line.length) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex + term.length]))); } /** diff --git a/addons/addon-search/test/SearchAddon.test.ts b/addons/addon-search/test/SearchAddon.test.ts index 232686b4..7b551e0c 100644 --- a/addons/addon-search/test/SearchAddon.test.ts +++ b/addons/addon-search/test/SearchAddon.test.ts @@ -19,8 +19,8 @@ test.afterAll(async () => await ctx.page.close()); test.describe('Search Tests', () => { test.beforeEach(async () => { + await ctx.proxy.reset(); await ctx.page.evaluate(` - window.term.reset() window.search?.dispose(); window.search = new SearchAddon(); window.term.loadAddon(window.search); diff --git a/addons/addon-search/typings/addon-search.d.ts b/addons/addon-search/typings/addon-search.d.ts index 282004a2..4c0df5a6 100644 --- a/addons/addon-search/typings/addon-search.d.ts +++ b/addons/addon-search/typings/addon-search.d.ts @@ -75,6 +75,21 @@ declare module '@xterm/addon-search' { activeMatchColorOverviewRuler: string; } + /** + * Event data fired when search results change. + */ + export interface ISearchResultChangeEvent { + /** + * The index of the currently active result, -1 when the threshold of matches is exceeded. + */ + resultIndex: number; + + /** + * The total number of search results found. + */ + resultCount: number; + } + /** * Options for the search addon. */ @@ -139,8 +154,7 @@ declare module '@xterm/addon-search' { /** * When decorations are enabled, fires when * the search results change. - * @returns -1 for resultIndex when the threshold of matches is exceeded. */ - readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number }>; + readonly onDidChangeResults: IEvent; } } diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index 14ff9433..c9349760 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -646,6 +646,14 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { if (deltaY === 0) { return false; } + const lines = self.coreMouseService.consumeWheelEvent( + ev as WheelEvent, + self._renderService?.dimensions?.device?.cell?.height, + self._coreBrowserService?.dpr + ); + if (lines === 0) { + return false; + } action = deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN; but = CoreMouseButton.WHEEL; break; @@ -817,6 +825,15 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { return false; } + const lines = self.coreMouseService.consumeWheelEvent( + ev as WheelEvent, + self._renderService?.dimensions?.device?.cell?.height, + self._coreBrowserService?.dpr + ); + if (lines === 0) { + return false; + } + // Construct and send sequences const sequence = C0.ESC + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B'); this.coreService.triggerDataEvent(sequence, true); diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 127e1f24..8e3d02f9 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -20,6 +20,7 @@ export class MockBufferService implements IBufferService { public buffers: IBufferSet = {} as any; public onResize: Event<{ cols: number, rows: number }> = new Emitter<{ cols: number, rows: number }>().event; public onScroll: Event = new Emitter().event; + private readonly _onScroll = new Emitter(); public isUserScrolling: boolean = false; constructor( public cols: number, @@ -27,6 +28,10 @@ export class MockBufferService implements IBufferService { optionsService: IOptionsService = new MockOptionsService() ) { this.buffers = new BufferSet(optionsService, this); + // Listen to buffer activation events and automatically fire scroll events + this.buffers.onBufferActivate(e => { + this._onScroll.fire(e.activeBuffer.ydisp); + }); } public scrollPages(pageCount: number): void { throw new Error('Method not implemented.'); @@ -66,6 +71,9 @@ export class MockCoreMouseService implements ICoreMouseService { public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } { throw new Error('Method not implemented.'); } + public consumeWheelEvent(ev: WheelEvent, cellHeight: number, dpr: number): number { + return 1; + } } export class MockCharsetService implements ICharsetService { diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index c4698e68..d8d8d6b6 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -37,6 +37,9 @@ export class BufferService extends Disposable implements IBufferService { 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._register(this.buffers.onBufferActivate(e => { + this._onScroll.fire(e.activeBuffer.ydisp); + })); } public resize(cols: number, rows: number): void { diff --git a/src/common/services/CoreMouseService.test.ts b/src/common/services/CoreMouseService.test.ts index 34710897..3b79596d 100644 --- a/src/common/services/CoreMouseService.test.ts +++ b/src/common/services/CoreMouseService.test.ts @@ -3,13 +3,14 @@ * @license MIT */ import { CoreMouseService } from 'common/services/CoreMouseService'; -import { MockCoreService, MockBufferService } from 'common/TestUtils.test'; +import { MockCoreService, MockBufferService, MockOptionsService } from 'common/TestUtils.test'; import { assert } from 'chai'; import { ICoreMouseEvent, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; // needed mock services const bufferService = new MockBufferService(300, 100); const coreService = new MockCoreService(); +const optionsService = new MockOptionsService(); function toBytes(s: string | undefined): number[] { if (!s) { @@ -24,20 +25,20 @@ function toBytes(s: string | undefined): number[] { describe('CoreMouseService', () => { it('init', () => { - const cms = new CoreMouseService(bufferService, coreService); + const cms = new CoreMouseService(bufferService, coreService, optionsService); assert.equal(cms.activeEncoding, 'DEFAULT'); assert.equal(cms.activeProtocol, 'NONE'); }); it('default protocols - NONE, X10, VT200, DRAG, ANY', () => { - const cms = new CoreMouseService(bufferService, coreService); + const cms = new CoreMouseService(bufferService, coreService, optionsService); assert.deepEqual(Object.keys((cms as any)._protocols), ['NONE', 'X10', 'VT200', 'DRAG', 'ANY']); }); it('default encodings - DEFAULT, SGR', () => { - const cms = new CoreMouseService(bufferService, coreService); + const cms = new CoreMouseService(bufferService, coreService, optionsService); assert.deepEqual(Object.keys((cms as any)._encodings), ['DEFAULT', 'SGR', 'SGR_PIXELS']); }); it('protocol/encoding setter, reset', () => { - const cms = new CoreMouseService(bufferService, coreService); + const cms = new CoreMouseService(bufferService, coreService, optionsService); cms.activeEncoding = 'SGR'; cms.activeProtocol = 'ANY'; assert.equal(cms.activeEncoding, 'SGR'); @@ -49,19 +50,19 @@ describe('CoreMouseService', () => { assert.throws(() => { cms.activeProtocol = 'xyz'; }, 'unknown protocol "xyz"'); }); it('addEncoding', () => { - const cms = new CoreMouseService(bufferService, coreService); + const cms = new CoreMouseService(bufferService, coreService, optionsService); cms.addEncoding('XYZ', (e: ICoreMouseEvent) => ''); cms.activeEncoding = 'XYZ'; assert.equal(cms.activeEncoding, 'XYZ'); }); it('addProtocol', () => { - const cms = new CoreMouseService(bufferService, coreService); + const cms = new CoreMouseService(bufferService, coreService, optionsService); cms.addProtocol('XYZ', { events: CoreMouseEventType.NONE, restrict: (e: ICoreMouseEvent) => false }); cms.activeProtocol = 'XYZ'; assert.equal(cms.activeProtocol, 'XYZ'); }); it('onProtocolChange', () => { - const cms = new CoreMouseService(bufferService, coreService); + const cms = new CoreMouseService(bufferService, coreService, optionsService); const wantedEvents: CoreMouseEventType[] = []; cms.onProtocolChange(events => wantedEvents.push(events)); cms.activeProtocol = 'NONE'; @@ -76,7 +77,7 @@ describe('CoreMouseService', () => { let cms: CoreMouseService; let reports: string[]; beforeEach(() => { - cms = new CoreMouseService(bufferService, coreService); + cms = new CoreMouseService(bufferService, coreService, optionsService); reports = []; coreService.triggerDataEvent = (data: string, userInput?: boolean) => reports.push(data); coreService.triggerBinaryEvent = (data: string) => reports.push(data); diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index f2f02379..a10ddafb 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -2,7 +2,7 @@ * Copyright (c) 2019 The xterm.js authors. All rights reserved. * @license MIT */ -import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services'; +import { IBufferService, ICoreService, ICoreMouseService, IOptionsService } from 'common/services/Services'; import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; @@ -174,13 +174,15 @@ export class CoreMouseService extends Disposable implements ICoreMouseService { private _activeProtocol: string = ''; private _activeEncoding: string = ''; private _lastEvent: ICoreMouseEvent | null = null; + private _wheelPartialScroll: number = 0; private readonly _onProtocolChange = this._register(new Emitter()); - public readonly onProtocolChange = this._onProtocolChange.event; + public readonly onProtocolChange = this._onProtocolChange.event; constructor( @IBufferService private readonly _bufferService: IBufferService, - @ICoreService private readonly _coreService: ICoreService + @ICoreService private readonly _coreService: ICoreService, + @IOptionsService private readonly _optionsService: IOptionsService ) { super(); // register default protocols and encodings @@ -229,6 +231,49 @@ export class CoreMouseService extends Disposable implements ICoreMouseService { this.activeProtocol = 'NONE'; this.activeEncoding = 'DEFAULT'; this._lastEvent = null; + this._wheelPartialScroll = 0; + } + + /** + * Processes a wheel event, accounting for partial scrolls for trackpad, mouse scrolls. + * This prevents hyper-sensitive scrolling in alt buffer. + */ + public consumeWheelEvent(ev: WheelEvent, cellHeight?: number, dpr?: number): number { + // Do nothing if it's not a vertical scroll event + if (ev.deltaY === 0 || ev.shiftKey) { + return 0; + } + + if (cellHeight === undefined || dpr === undefined) { + return 0; + } + + const targetWheelEventPixels = cellHeight / dpr; + let amount = this._applyScrollModifier(ev.deltaY, ev); + + if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) { + amount /= (targetWheelEventPixels + 0.0); // Prevent integer division + + const isLikelyTrackpad = Math.abs(ev.deltaY) < 50; + if (isLikelyTrackpad) { + amount *= 0.3; + } + + this._wheelPartialScroll += amount; + amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1); + this._wheelPartialScroll %= 1; + } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) { + amount *= this._bufferService.rows; + } + return amount; + } + + private _applyScrollModifier(amount: number, ev: WheelEvent): number { + // Multiply the scroll speed when the modifier key is pressed + if (ev.altKey || ev.ctrlKey || ev.shiftKey) { + return amount * this._optionsService.rawOptions.fastScrollSensitivity * this._optionsService.rawOptions.scrollSensitivity; + } + return amount * this._optionsService.rawOptions.scrollSensitivity; } /** diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 9c3aebf7..d47c3bbc 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -58,6 +58,11 @@ export interface ICoreMouseService { * Human readable version of mouse events. */ explainEvents(events: CoreMouseEventType): { [event: string]: boolean }; + + /** + * Process wheel event taking partial scroll into account. + */ + consumeWheelEvent(ev: WheelEvent, cellHeight?: number, dpr?: number): number; } export const ICoreService = createDecorator('CoreService');