From 9c3eb170d2369b5d0d0e9bb83afb6ee33e27636f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 23:07:47 +0000 Subject: [PATCH 01/22] Bump form-data from 4.0.0 to 4.0.4 in /addons/addon-ligatures Bumps [form-data](https://github.com/form-data/form-data) from 4.0.0 to 4.0.4. - [Release notes](https://github.com/form-data/form-data/releases) - [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md) - [Commits](https://github.com/form-data/form-data/compare/v4.0.0...v4.0.4) --- updated-dependencies: - dependency-name: form-data dependency-version: 4.0.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- addons/addon-ligatures/yarn.lock | 110 ++++++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 3 deletions(-) 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" From a3ab942ea157c1981847f1467be7f5379f14e6c0 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 27 Aug 2025 20:45:52 -0700 Subject: [PATCH 02/22] Fix teleport issue when coming back from alt buffer --- src/common/TestUtils.test.ts | 3 +++ src/common/buffer/BufferSet.ts | 2 ++ src/common/services/BufferService.ts | 7 +++++++ src/common/services/Services.ts | 1 + 4 files changed, 13 insertions(+) diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 127e1f24..ccdb54ae 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -46,6 +46,9 @@ export class MockBufferService implements IBufferService { public scrollLines(disp: number, suppressScrollEvent?: boolean): void { throw new Error('Method not implemented.'); } + public syncScrollPosition(): void { + // Mock implementation - no-op for tests + } public resize(cols: number, rows: number): void { this.cols = cols; this.rows = rows; diff --git a/src/common/buffer/BufferSet.ts b/src/common/buffer/BufferSet.ts index 8f3a6aec..54390f2a 100644 --- a/src/common/buffer/BufferSet.ts +++ b/src/common/buffer/BufferSet.ts @@ -91,6 +91,8 @@ export class BufferSet extends Disposable implements IBufferSet { activeBuffer: this._normal, inactiveBuffer: this._alt }); + // Prevent scrollbar "teleport" to top of the terminal, from previous alt buffer. + this._bufferService.syncScrollPosition(); } /** diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index c4698e68..85699f6f 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -148,4 +148,11 @@ export class BufferService extends Disposable implements IBufferService { this._onScroll.fire(buffer.ydisp); } } + + /** + * Synchronize the scroll position by firing a scroll event with the current buffer's ydisp. + */ + public syncScrollPosition(): void { + this._onScroll.fire(this.buffer.ydisp); + } } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 9c3aebf7..58ee113f 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -22,6 +22,7 @@ export interface IBufferService { onScroll: Event; scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void; scrollLines(disp: number, suppressScrollEvent?: boolean): void; + syncScrollPosition(): void; resize(cols: number, rows: number): void; reset(): void; } From 86745e1320f95b0c4f71330acc2d6a56ed0344a4 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 27 Aug 2025 21:12:15 -0700 Subject: [PATCH 03/22] Add some tests from copilot --- src/common/buffer/BufferSet.test.ts | 67 +++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/common/buffer/BufferSet.test.ts b/src/common/buffer/BufferSet.test.ts index 944b8c15..4c937729 100644 --- a/src/common/buffer/BufferSet.test.ts +++ b/src/common/buffer/BufferSet.test.ts @@ -7,6 +7,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 { Emitter } from 'vs/base/common/event'; describe('BufferSet', () => { let bufferSet: BufferSet; @@ -81,4 +82,70 @@ describe('BufferSet', () => { assert.equal(bufferSet.alt.markers.length, 0); }); }); + + describe('scroll position synchronization', () => { + it('should sync scroll position when switching from alt back to normal buffer', () => { + bufferSet.activateNormalBuffer(); + + const originalYDisp = 50; + bufferSet.normal.ydisp = originalYDisp; + bufferSet.normal.ybase = 100; + + // Track onScroll events fired by the buffer service + let scrollEventFired = false; + let scrollEventPosition = -1; + + const mockBufferService = (bufferSet as any)._bufferService as MockBufferService; + const originalOnScroll = mockBufferService.onScroll; + + // Mock the onScroll event to track when it's called + const scrollEmitter = new Emitter(); + mockBufferService.onScroll = scrollEmitter.event; + + // Override syncScrollPosition to actually fire the event + mockBufferService.syncScrollPosition = () => { + scrollEventFired = true; + scrollEventPosition = bufferSet.normal.ydisp; + scrollEmitter.fire(bufferSet.normal.ydisp); + }; + + + bufferSet.activateAltBuffer(); + assert.equal(bufferSet.normal.ydisp, originalYDisp, 'Normal buffer ydisp should be preserved'); + + // Reset scroll event tracking + scrollEventFired = false; + scrollEventPosition = -1; + + bufferSet.activateNormalBuffer(); + assert.equal(bufferSet.normal.ydisp, originalYDisp, 'Normal buffer ydisp should be restored'); + assert.equal(scrollEventFired, true, 'syncScrollPosition should have fired scroll event'); + assert.equal(scrollEventPosition, originalYDisp, 'Scroll event should contain correct position'); + assert.equal(bufferSet.active, bufferSet.normal, 'Normal buffer should be active'); + }); + + it('should preserve normal buffer scroll position even when alt buffer has different position', () => { + bufferSet.activateNormalBuffer(); + const normalScrollPos = 80; + bufferSet.normal.ydisp = normalScrollPos; + bufferSet.normal.ybase = 150; + + bufferSet.activateAltBuffer(); + bufferSet.alt.ydisp = 0; + bufferSet.alt.ybase = 0; + + const mockBufferService = (bufferSet as any)._bufferService as MockBufferService; + let syncedPosition = -1; + + // Track the position that gets synced + mockBufferService.syncScrollPosition = () => { + syncedPosition = bufferSet.normal.ydisp; + }; + + bufferSet.activateNormalBuffer(); + assert.equal(bufferSet.normal.ydisp, normalScrollPos, 'Normal buffer should maintain its scroll position'); + assert.equal(syncedPosition, normalScrollPos, 'syncScrollPosition should sync with normal buffer position'); + assert.notEqual(syncedPosition, bufferSet.alt.ydisp, 'Sync position should not match alt buffer position'); + }); + }); }); From 675b2c3a70844d4a2a655a6e54a38125000498cc Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 27 Aug 2025 21:31:34 -0700 Subject: [PATCH 04/22] Improve test --- src/common/TestUtils.test.ts | 9 ++-- src/common/buffer/BufferSet.test.ts | 65 ++++++++++++++--------------- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index ccdb54ae..041481b5 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -18,8 +18,10 @@ export class MockBufferService implements IBufferService { public serviceBrand: any; public get buffer(): IBuffer { return this.buffers.active; } 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 _onResize = new Emitter<{ cols: number, rows: number }>(); + public readonly onResize: Event<{ cols: number, rows: number }> = this._onResize.event; + private readonly _onScroll = new Emitter(); + public readonly onScroll: Event = this._onScroll.event; public isUserScrolling: boolean = false; constructor( public cols: number, @@ -47,7 +49,8 @@ export class MockBufferService implements IBufferService { throw new Error('Method not implemented.'); } public syncScrollPosition(): void { - // Mock implementation - no-op for tests + // Fire scroll event with current buffer position + this._onScroll.fire(this.buffer.ydisp); } public resize(cols: number, rows: number): void { this.cols = cols; diff --git a/src/common/buffer/BufferSet.test.ts b/src/common/buffer/BufferSet.test.ts index 4c937729..449c40b8 100644 --- a/src/common/buffer/BufferSet.test.ts +++ b/src/common/buffer/BufferSet.test.ts @@ -84,47 +84,32 @@ describe('BufferSet', () => { }); describe('scroll position synchronization', () => { - it('should sync scroll position when switching from alt back to normal buffer', () => { + it('should call syncScrollPosition when switching from alt back to normal buffer', () => { bufferSet.activateNormalBuffer(); const originalYDisp = 50; bufferSet.normal.ydisp = originalYDisp; bufferSet.normal.ybase = 100; - - // Track onScroll events fired by the buffer service - let scrollEventFired = false; - let scrollEventPosition = -1; - const mockBufferService = (bufferSet as any)._bufferService as MockBufferService; - const originalOnScroll = mockBufferService.onScroll; - // Mock the onScroll event to track when it's called - const scrollEmitter = new Emitter(); - mockBufferService.onScroll = scrollEmitter.event; - - // Override syncScrollPosition to actually fire the event + let syncScrollPositionCalled = false; + const originalSyncScrollPosition = mockBufferService.syncScrollPosition.bind(mockBufferService); mockBufferService.syncScrollPosition = () => { - scrollEventFired = true; - scrollEventPosition = bufferSet.normal.ydisp; - scrollEmitter.fire(bufferSet.normal.ydisp); + syncScrollPositionCalled = true; + originalSyncScrollPosition(); }; - bufferSet.activateAltBuffer(); assert.equal(bufferSet.normal.ydisp, originalYDisp, 'Normal buffer ydisp should be preserved'); - - // Reset scroll event tracking - scrollEventFired = false; - scrollEventPosition = -1; - + syncScrollPositionCalled = false; bufferSet.activateNormalBuffer(); + assert.equal(bufferSet.normal.ydisp, originalYDisp, 'Normal buffer ydisp should be restored'); - assert.equal(scrollEventFired, true, 'syncScrollPosition should have fired scroll event'); - assert.equal(scrollEventPosition, originalYDisp, 'Scroll event should contain correct position'); + assert.equal(syncScrollPositionCalled, true, 'activateNormalBuffer should call syncScrollPosition'); assert.equal(bufferSet.active, bufferSet.normal, 'Normal buffer should be active'); }); - it('should preserve normal buffer scroll position even when alt buffer has different position', () => { + it('should preserve normal buffer scroll position when switching back from alt buffer', () => { bufferSet.activateNormalBuffer(); const normalScrollPos = 80; bufferSet.normal.ydisp = normalScrollPos; @@ -133,19 +118,33 @@ describe('BufferSet', () => { bufferSet.activateAltBuffer(); bufferSet.alt.ydisp = 0; bufferSet.alt.ybase = 0; + bufferSet.activateNormalBuffer(); + assert.equal(bufferSet.normal.ydisp, normalScrollPos, 'Normal buffer should maintain its scroll position'); + assert.equal(bufferSet.active, bufferSet.normal, 'Normal buffer should be active'); + assert.notEqual(bufferSet.normal.ydisp, bufferSet.alt.ydisp, 'Normal and alt buffer should have different scroll positions'); + }); + + it('should fire scroll event with correct position when syncScrollPosition is called', () => { const mockBufferService = (bufferSet as any)._bufferService as MockBufferService; - let syncedPosition = -1; - - // Track the position that gets synced - mockBufferService.syncScrollPosition = () => { - syncedPosition = bufferSet.normal.ydisp; - }; bufferSet.activateNormalBuffer(); - assert.equal(bufferSet.normal.ydisp, normalScrollPos, 'Normal buffer should maintain its scroll position'); - assert.equal(syncedPosition, normalScrollPos, 'syncScrollPosition should sync with normal buffer position'); - assert.notEqual(syncedPosition, bufferSet.alt.ydisp, 'Sync position should not match alt buffer position'); + const testScrollPosition = 42; + + mockBufferService.buffer.ydisp = testScrollPosition; + assert.equal(mockBufferService.buffer.ydisp, testScrollPosition, 'Active buffer ydisp should be set correctly'); + + let scrollEventFired = false; + let scrollEventPosition = -1; + + mockBufferService.onScroll((position: number) => { + scrollEventFired = true; + scrollEventPosition = position; + }); + + mockBufferService.syncScrollPosition(); + assert.equal(scrollEventFired, true, 'syncScrollPosition should fire scroll event'); + assert.equal(scrollEventPosition, testScrollPosition, `Scroll event should contain current buffer ydisp`); }); }); }); From 753ea81fc08661b4c661338461c81b3a06178d8c Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 28 Aug 2025 08:46:27 -0700 Subject: [PATCH 05/22] BufferService listen to onBufferActivate, remove tests --- src/common/TestUtils.test.ts | 8 ++-- src/common/buffer/BufferSet.test.ts | 64 ---------------------------- src/common/buffer/BufferSet.ts | 2 - src/common/services/BufferService.ts | 9 ++-- src/common/services/Services.ts | 1 - 5 files changed, 7 insertions(+), 77 deletions(-) diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 041481b5..d9d3b24e 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -29,6 +29,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.'); @@ -48,10 +52,6 @@ export class MockBufferService implements IBufferService { public scrollLines(disp: number, suppressScrollEvent?: boolean): void { throw new Error('Method not implemented.'); } - public syncScrollPosition(): void { - // Fire scroll event with current buffer position - this._onScroll.fire(this.buffer.ydisp); - } public resize(cols: number, rows: number): void { this.cols = cols; this.rows = rows; diff --git a/src/common/buffer/BufferSet.test.ts b/src/common/buffer/BufferSet.test.ts index 449c40b8..b6136094 100644 --- a/src/common/buffer/BufferSet.test.ts +++ b/src/common/buffer/BufferSet.test.ts @@ -83,68 +83,4 @@ describe('BufferSet', () => { }); }); - describe('scroll position synchronization', () => { - it('should call syncScrollPosition when switching from alt back to normal buffer', () => { - bufferSet.activateNormalBuffer(); - - const originalYDisp = 50; - bufferSet.normal.ydisp = originalYDisp; - bufferSet.normal.ybase = 100; - const mockBufferService = (bufferSet as any)._bufferService as MockBufferService; - - let syncScrollPositionCalled = false; - const originalSyncScrollPosition = mockBufferService.syncScrollPosition.bind(mockBufferService); - mockBufferService.syncScrollPosition = () => { - syncScrollPositionCalled = true; - originalSyncScrollPosition(); - }; - - bufferSet.activateAltBuffer(); - assert.equal(bufferSet.normal.ydisp, originalYDisp, 'Normal buffer ydisp should be preserved'); - syncScrollPositionCalled = false; - bufferSet.activateNormalBuffer(); - - assert.equal(bufferSet.normal.ydisp, originalYDisp, 'Normal buffer ydisp should be restored'); - assert.equal(syncScrollPositionCalled, true, 'activateNormalBuffer should call syncScrollPosition'); - assert.equal(bufferSet.active, bufferSet.normal, 'Normal buffer should be active'); - }); - - it('should preserve normal buffer scroll position when switching back from alt buffer', () => { - bufferSet.activateNormalBuffer(); - const normalScrollPos = 80; - bufferSet.normal.ydisp = normalScrollPos; - bufferSet.normal.ybase = 150; - - bufferSet.activateAltBuffer(); - bufferSet.alt.ydisp = 0; - bufferSet.alt.ybase = 0; - bufferSet.activateNormalBuffer(); - - assert.equal(bufferSet.normal.ydisp, normalScrollPos, 'Normal buffer should maintain its scroll position'); - assert.equal(bufferSet.active, bufferSet.normal, 'Normal buffer should be active'); - assert.notEqual(bufferSet.normal.ydisp, bufferSet.alt.ydisp, 'Normal and alt buffer should have different scroll positions'); - }); - - it('should fire scroll event with correct position when syncScrollPosition is called', () => { - const mockBufferService = (bufferSet as any)._bufferService as MockBufferService; - - bufferSet.activateNormalBuffer(); - const testScrollPosition = 42; - - mockBufferService.buffer.ydisp = testScrollPosition; - assert.equal(mockBufferService.buffer.ydisp, testScrollPosition, 'Active buffer ydisp should be set correctly'); - - let scrollEventFired = false; - let scrollEventPosition = -1; - - mockBufferService.onScroll((position: number) => { - scrollEventFired = true; - scrollEventPosition = position; - }); - - mockBufferService.syncScrollPosition(); - assert.equal(scrollEventFired, true, 'syncScrollPosition should fire scroll event'); - assert.equal(scrollEventPosition, testScrollPosition, `Scroll event should contain current buffer ydisp`); - }); - }); }); diff --git a/src/common/buffer/BufferSet.ts b/src/common/buffer/BufferSet.ts index 54390f2a..8f3a6aec 100644 --- a/src/common/buffer/BufferSet.ts +++ b/src/common/buffer/BufferSet.ts @@ -91,8 +91,6 @@ export class BufferSet extends Disposable implements IBufferSet { activeBuffer: this._normal, inactiveBuffer: this._alt }); - // Prevent scrollbar "teleport" to top of the terminal, from previous alt buffer. - this._bufferService.syncScrollPosition(); } /** diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index 85699f6f..616e5619 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 { @@ -149,10 +152,4 @@ export class BufferService extends Disposable implements IBufferService { } } - /** - * Synchronize the scroll position by firing a scroll event with the current buffer's ydisp. - */ - public syncScrollPosition(): void { - this._onScroll.fire(this.buffer.ydisp); - } } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 58ee113f..9c3aebf7 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -22,7 +22,6 @@ export interface IBufferService { onScroll: Event; scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void; scrollLines(disp: number, suppressScrollEvent?: boolean): void; - syncScrollPosition(): void; resize(cols: number, rows: number): void; reset(): void; } From 4d59eca5f9171e67a3e4d4daf28242746e6d7b28 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 28 Aug 2025 08:52:46 -0700 Subject: [PATCH 06/22] Clean up --- src/common/buffer/BufferSet.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/common/buffer/BufferSet.test.ts b/src/common/buffer/BufferSet.test.ts index b6136094..944b8c15 100644 --- a/src/common/buffer/BufferSet.test.ts +++ b/src/common/buffer/BufferSet.test.ts @@ -7,7 +7,6 @@ import { assert } from 'chai'; import { BufferSet } from 'common/buffer/BufferSet'; import { Buffer } from 'common/buffer/Buffer'; import { MockOptionsService, MockBufferService } from 'common/TestUtils.test'; -import { Emitter } from 'vs/base/common/event'; describe('BufferSet', () => { let bufferSet: BufferSet; @@ -82,5 +81,4 @@ describe('BufferSet', () => { assert.equal(bufferSet.alt.markers.length, 0); }); }); - }); From 287702610c81ab346202aa53494847ed2addbfdd Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 28 Aug 2025 08:54:01 -0700 Subject: [PATCH 07/22] More clean up after removing test --- src/common/TestUtils.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index d9d3b24e..f504cbe9 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -18,10 +18,9 @@ export class MockBufferService implements IBufferService { public serviceBrand: any; public get buffer(): IBuffer { return this.buffers.active; } public buffers: IBufferSet = {} as any; - private readonly _onResize = new Emitter<{ cols: number, rows: number }>(); - public readonly onResize: Event<{ cols: number, rows: number }> = this._onResize.event; + 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 readonly onScroll: Event = this._onScroll.event; public isUserScrolling: boolean = false; constructor( public cols: number, From a6fdf043cedc1a09fefd675cb7246314ff2c9138 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 28 Aug 2025 08:57:25 -0700 Subject: [PATCH 08/22] Respect original formatting --- src/common/services/BufferService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index 616e5619..d8d8d6b6 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -151,5 +151,4 @@ export class BufferService extends Disposable implements IBufferService { this._onScroll.fire(buffer.ydisp); } } - } From 5bd7b4aaecc10caacec50dc481ec5ac19b1f0cce Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 28 Aug 2025 10:28:19 -0700 Subject: [PATCH 09/22] Bring back partial wheel tracking Part of microsoft/vscode#224750 --- src/browser/CoreBrowserTerminal.ts | 41 ++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index 14ff9433..37b33f7e 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -646,6 +646,10 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { if (deltaY === 0) { return false; } + const lines = self._consumeWheelEvent(ev as WheelEvent); + if (lines === 0) { + return false; + } action = deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN; but = CoreMouseButton.WHEEL; break; @@ -817,6 +821,11 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { return false; } + const lines = self._consumeWheelEvent(ev as WheelEvent); + 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); @@ -825,6 +834,38 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { }, { passive: false })); } + // Stores a partial line amount when scrolling, this is used to keep track of how much of a line + // is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a + // quick fix and could have a more robust solution in place that reset the value when needed. + private _wheelPartialScroll: number = 0; + + /** + * Gets the number of pixels scrolled by the mouse event taking into account what type of delta + * is being used. + * @param ev The mouse wheel event. + */ + private _consumeWheelEvent(ev: WheelEvent): number { + // Do nothing if it's not a vertical scroll event + if (ev.deltaY === 0 || ev.shiftKey) { + return 0; + } + + if (!this._coreBrowserService || !this._renderService) { + return 0; + } + // Fallback to WheelEvent.DOM_DELTA_LINE + const targetWheelEventPixels = this._renderService.dimensions.device.cell.height / this._coreBrowserService.dpr; + let amount = 1; + if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) { + amount /= targetWheelEventPixels + 0.0; // Prevent integer division + 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; + } /** * Tells the renderer to refresh terminal content between two rows (inclusive) at the next From ebddd5d3465b63aaae8379484694ef4a012db4f7 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 28 Aug 2025 22:54:50 -0700 Subject: [PATCH 10/22] Move from CoreBrowerTerminal to CoreMouseService. Still slow though --- src/browser/CoreBrowserTerminal.ts | 43 ++++++------------------- src/common/TestUtils.test.ts | 3 ++ src/common/services/CoreMouseService.ts | 30 +++++++++++++++++ src/common/services/Services.ts | 5 +++ 4 files changed, 48 insertions(+), 33 deletions(-) diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index 37b33f7e..6ba27b66 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -646,7 +646,11 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { if (deltaY === 0) { return false; } - const lines = self._consumeWheelEvent(ev as WheelEvent); + const lines = self.coreMouseService.consumeWhellEvent( + ev as WheelEvent, + self._renderService?.dimensions?.device?.cell?.height, + self._coreBrowserService?.dpr + ); if (lines === 0) { return false; } @@ -821,7 +825,11 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { return false; } - const lines = self._consumeWheelEvent(ev as WheelEvent); + const lines = self.coreMouseService.consumeWhellEvent( + ev as WheelEvent, + self._renderService?.dimensions?.device?.cell?.height, + self._coreBrowserService?.dpr + ); if (lines === 0) { return false; } @@ -834,38 +842,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { }, { passive: false })); } - // Stores a partial line amount when scrolling, this is used to keep track of how much of a line - // is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a - // quick fix and could have a more robust solution in place that reset the value when needed. - private _wheelPartialScroll: number = 0; - /** - * Gets the number of pixels scrolled by the mouse event taking into account what type of delta - * is being used. - * @param ev The mouse wheel event. - */ - private _consumeWheelEvent(ev: WheelEvent): number { - // Do nothing if it's not a vertical scroll event - if (ev.deltaY === 0 || ev.shiftKey) { - return 0; - } - - if (!this._coreBrowserService || !this._renderService) { - return 0; - } - // Fallback to WheelEvent.DOM_DELTA_LINE - const targetWheelEventPixels = this._renderService.dimensions.device.cell.height / this._coreBrowserService.dpr; - let amount = 1; - if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) { - amount /= targetWheelEventPixels + 0.0; // Prevent integer division - 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; - } /** * Tells the renderer to refresh terminal content between two rows (inclusive) at the next diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 127e1f24..b25a2182 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -66,6 +66,9 @@ export class MockCoreMouseService implements ICoreMouseService { public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } { throw new Error('Method not implemented.'); } + public consumeWhellEvent(ev: WheelEvent, cellHeight: number, dpr: number): number { + return 1; // Return a simple mock value + } } export class MockCharsetService implements ICharsetService { diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index f2f02379..a1135a8a 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -174,6 +174,7 @@ 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; @@ -229,6 +230,35 @@ 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 consumeWhellEvent(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; + } + + // Fallback to WheelEvent.DOM_DELTA_LINE + const targetWheelEventPixels = cellHeight / dpr; + let amount = 1; + if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) { + amount /= targetWheelEventPixels + 0.0; // Prevent integer division + 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; } /** diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 9c3aebf7..e7ccc241 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. + */ + consumeWhellEvent(ev: WheelEvent, cellHeight?: number, dpr?: number): number; } export const ICoreService = createDecorator('CoreService'); From b104a5b953a22329917b5d22378465279e5b9eaf Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 28 Aug 2025 22:56:15 -0700 Subject: [PATCH 11/22] Respect original formatting --- src/browser/CoreBrowserTerminal.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index 6ba27b66..bd513573 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -843,7 +843,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { } - /** * Tells the renderer to refresh terminal content between two rows (inclusive) at the next * opportunity. From 26ecba8de1f5205c8a8f577c4ebce687ac23be84 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Mon, 8 Sep 2025 21:03:05 -0700 Subject: [PATCH 12/22] Fix sluggish mouse problem. Now similar to trackpad --- src/browser/CoreBrowserTerminal.ts | 4 ++-- src/common/TestUtils.test.ts | 2 +- src/common/services/CoreMouseService.ts | 5 +++-- src/common/services/Services.ts | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index bd513573..c9349760 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -646,7 +646,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { if (deltaY === 0) { return false; } - const lines = self.coreMouseService.consumeWhellEvent( + const lines = self.coreMouseService.consumeWheelEvent( ev as WheelEvent, self._renderService?.dimensions?.device?.cell?.height, self._coreBrowserService?.dpr @@ -825,7 +825,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { return false; } - const lines = self.coreMouseService.consumeWhellEvent( + const lines = self.coreMouseService.consumeWheelEvent( ev as WheelEvent, self._renderService?.dimensions?.device?.cell?.height, self._coreBrowserService?.dpr diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index b25a2182..1ee79e75 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -66,7 +66,7 @@ export class MockCoreMouseService implements ICoreMouseService { public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } { throw new Error('Method not implemented.'); } - public consumeWhellEvent(ev: WheelEvent, cellHeight: number, dpr: number): number { + public consumeWheelEvent(ev: WheelEvent, cellHeight: number, dpr: number): number { return 1; // Return a simple mock value } } diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index a1135a8a..7f3d5f39 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -237,7 +237,7 @@ export class CoreMouseService extends Disposable implements ICoreMouseService { * Processes a wheel event, accounting for partial scrolls for trackpad, mouse scrolls. * This prevents hyper-sensitive scrolling in alt buffer. */ - public consumeWhellEvent(ev: WheelEvent, cellHeight?: number, dpr?: number): number { + 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; @@ -251,7 +251,8 @@ export class CoreMouseService extends Disposable implements ICoreMouseService { const targetWheelEventPixels = cellHeight / dpr; let amount = 1; if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) { - amount /= targetWheelEventPixels + 0.0; // Prevent integer division + const pixelAmount = ev.deltaY; + amount = pixelAmount / (targetWheelEventPixels + 0.0); // Prevent integer division this._wheelPartialScroll += amount; amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1); this._wheelPartialScroll %= 1; diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index e7ccc241..d47c3bbc 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -62,7 +62,7 @@ export interface ICoreMouseService { /** * Process wheel event taking partial scroll into account. */ - consumeWhellEvent(ev: WheelEvent, cellHeight?: number, dpr?: number): number; + consumeWheelEvent(ev: WheelEvent, cellHeight?: number, dpr?: number): number; } export const ICoreService = createDecorator('CoreService'); From 50df7a69e61a6e49ec8c082231339bbb40bbc026 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Mon, 8 Sep 2025 21:06:37 -0700 Subject: [PATCH 13/22] remove unused var --- src/common/services/CoreMouseService.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index 7f3d5f39..bf01d73c 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -251,8 +251,7 @@ export class CoreMouseService extends Disposable implements ICoreMouseService { const targetWheelEventPixels = cellHeight / dpr; let amount = 1; if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) { - const pixelAmount = ev.deltaY; - amount = pixelAmount / (targetWheelEventPixels + 0.0); // Prevent integer division + amount = ev.deltaY / (targetWheelEventPixels + 0.0); // Prevent integer division this._wheelPartialScroll += amount; amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1); this._wheelPartialScroll %= 1; From d862c823861ce04ef304bec2b1f028f69e3c98da Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Mon, 8 Sep 2025 21:14:20 -0700 Subject: [PATCH 14/22] no stale comments --- src/common/TestUtils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 1ee79e75..499fb787 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -67,7 +67,7 @@ export class MockCoreMouseService implements ICoreMouseService { throw new Error('Method not implemented.'); } public consumeWheelEvent(ev: WheelEvent, cellHeight: number, dpr: number): number { - return 1; // Return a simple mock value + return 1; } } From cf6b868dfbf1f10885e53b3c841f188ff3204037 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 9 Sep 2025 11:51:27 -0700 Subject: [PATCH 15/22] fastScrollModifier seems deprecated --- src/common/services/CoreMouseService.ts | 31 +++++++++++++++++++++---- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index bf01d73c..e777f007 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'; @@ -181,7 +181,8 @@ export class CoreMouseService extends Disposable implements ICoreMouseService { 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 @@ -247,11 +248,17 @@ export class CoreMouseService extends Disposable implements ICoreMouseService { return 0; } - // Fallback to WheelEvent.DOM_DELTA_LINE const targetWheelEventPixels = cellHeight / dpr; - let amount = 1; + let amount = this._applyScrollModifier(ev.deltaY, ev); + if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) { - amount = ev.deltaY / (targetWheelEventPixels + 0.0); // Prevent integer division + 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; @@ -261,6 +268,20 @@ export class CoreMouseService extends Disposable implements ICoreMouseService { return amount; } + private _applyScrollModifier(amount: number, ev: WheelEvent): number { + const modifier = this._optionsService.rawOptions.fastScrollModifier; // QUESTION: This is always alt. Seems deprecated via: https://github.com/xtermjs/xterm.js/blob/5.5.0/src/browser/Viewport.ts + console.log('what is this modifier: ', modifier); + + // Multiply the scroll speed when the modifier key is pressed + if ((modifier === 'alt' && ev.altKey) || + (modifier === 'ctrl' && ev.ctrlKey) || + (modifier === 'shift' && ev.shiftKey)) { + return amount * this._optionsService.rawOptions.fastScrollSensitivity * this._optionsService.rawOptions.scrollSensitivity; + } + + return amount * this._optionsService.rawOptions.scrollSensitivity; + } + /** * Triggers a mouse event to be sent. * From 03ce26c6975ad2f71276a87e984bf9bec0503db5 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 9 Sep 2025 11:56:28 -0700 Subject: [PATCH 16/22] Questions, possible concern --- src/common/services/CoreMouseService.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index e777f007..da94ed97 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -177,7 +177,7 @@ export class CoreMouseService extends Disposable implements ICoreMouseService { 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, @@ -270,12 +270,16 @@ export class CoreMouseService extends Disposable implements ICoreMouseService { private _applyScrollModifier(amount: number, ev: WheelEvent): number { const modifier = this._optionsService.rawOptions.fastScrollModifier; // QUESTION: This is always alt. Seems deprecated via: https://github.com/xtermjs/xterm.js/blob/5.5.0/src/browser/Viewport.ts - console.log('what is this modifier: ', modifier); + // Problem is that modifier always seem 'alt' even when ev.ctrlKey is true, for example. + // Maybe this doesnt matter because all of alt, ctrl, shift does same operation? // Multiply the scroll speed when the modifier key is pressed if ((modifier === 'alt' && ev.altKey) || - (modifier === 'ctrl' && ev.ctrlKey) || - (modifier === 'shift' && ev.shiftKey)) { + (modifier === 'ctrl' && ev.ctrlKey) || + (modifier === 'shift' && ev.shiftKey)) { + + console.log('fast scroll sensitivity: ', this._optionsService.rawOptions.fastScrollSensitivity); + console.log('regular scroll sensitivity: ', this._optionsService.rawOptions.scrollSensitivity); return amount * this._optionsService.rawOptions.fastScrollSensitivity * this._optionsService.rawOptions.scrollSensitivity; } From 1a6a807f6816de4d14a5138817afa8dcc5df595d Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 9 Sep 2025 11:59:29 -0700 Subject: [PATCH 17/22] Remove stale comment --- src/common/services/CoreMouseService.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index da94ed97..99cd1fc5 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -278,8 +278,6 @@ export class CoreMouseService extends Disposable implements ICoreMouseService { (modifier === 'ctrl' && ev.ctrlKey) || (modifier === 'shift' && ev.shiftKey)) { - console.log('fast scroll sensitivity: ', this._optionsService.rawOptions.fastScrollSensitivity); - console.log('regular scroll sensitivity: ', this._optionsService.rawOptions.scrollSensitivity); return amount * this._optionsService.rawOptions.fastScrollSensitivity * this._optionsService.rawOptions.scrollSensitivity; } From 38bee71630a4680cb86b1df00adfd1b9a28973c7 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 9 Sep 2025 12:00:19 -0700 Subject: [PATCH 18/22] More clear comments --- src/common/services/CoreMouseService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index 99cd1fc5..9b74daeb 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -271,7 +271,7 @@ export class CoreMouseService extends Disposable implements ICoreMouseService { private _applyScrollModifier(amount: number, ev: WheelEvent): number { const modifier = this._optionsService.rawOptions.fastScrollModifier; // QUESTION: This is always alt. Seems deprecated via: https://github.com/xtermjs/xterm.js/blob/5.5.0/src/browser/Viewport.ts // Problem is that modifier always seem 'alt' even when ev.ctrlKey is true, for example. - // Maybe this doesnt matter because all of alt, ctrl, shift does same operation? + // May need to tweak conditional to: if any of `ev.altKey || ev.ctrlKey || ev.shiftKey` is true // Multiply the scroll speed when the modifier key is pressed if ((modifier === 'alt' && ev.altKey) || From e4f222f6b00ed853895b154f65c86d0a2a742312 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 9 Sep 2025 12:03:51 -0700 Subject: [PATCH 19/22] Make test happy --- src/common/services/CoreMouseService.test.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) 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); From 9c42fb348b38a9fa7a1cc817edcb63059652dfc0 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 9 Sep 2025 13:21:37 -0700 Subject: [PATCH 20/22] ignore fastScrollModifier --- src/common/services/CoreMouseService.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index 9b74daeb..a10ddafb 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -269,18 +269,10 @@ export class CoreMouseService extends Disposable implements ICoreMouseService { } private _applyScrollModifier(amount: number, ev: WheelEvent): number { - const modifier = this._optionsService.rawOptions.fastScrollModifier; // QUESTION: This is always alt. Seems deprecated via: https://github.com/xtermjs/xterm.js/blob/5.5.0/src/browser/Viewport.ts - // Problem is that modifier always seem 'alt' even when ev.ctrlKey is true, for example. - // May need to tweak conditional to: if any of `ev.altKey || ev.ctrlKey || ev.shiftKey` is true - // Multiply the scroll speed when the modifier key is pressed - if ((modifier === 'alt' && ev.altKey) || - (modifier === 'ctrl' && ev.ctrlKey) || - (modifier === 'shift' && ev.shiftKey)) { - + if (ev.altKey || ev.ctrlKey || ev.shiftKey) { return amount * this._optionsService.rawOptions.fastScrollSensitivity * this._optionsService.rawOptions.scrollSensitivity; } - return amount * this._optionsService.rawOptions.scrollSensitivity; } From 6e833e5d5d958642117eca457be7e18d61b799a5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 17 Sep 2025 07:33:43 -0700 Subject: [PATCH 21/22] Add proper typing for search results change event - Export ISearchResultChangeEvent interface in addon-search - Use proper Event typing instead of inline object - Improve API consistency and type safety for consumers --- addons/addon-search/src/SearchAddon.ts | 11 ++++++++--- addons/addon-search/test/SearchAddon.test.ts | 2 +- addons/addon-search/typings/addon-search.d.ts | 18 ++++++++++++++++-- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/addons/addon-search/src/SearchAddon.ts b/addons/addon-search/src/SearchAddon.ts index 8395e437..b65cfdd6 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 } 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'; export interface ISearchOptions { @@ -31,6 +31,11 @@ export interface ISearchPosition { startRow: number; } +export interface ISearchResultChangeEvent { + resultIndex: number; + resultCount: number; +} + export interface ISearchAddonOptions { highlightLimit: number; } @@ -86,8 +91,8 @@ 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(); 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; } } From 87a45acae77b0147e01ff477eade1a6a93c9eec0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 17 Sep 2025 08:03:37 -0700 Subject: [PATCH 22/22] Refactor search constants and improve documentation - Convert standalone constants to Constants enum with detailed docs - Fix spacing issue in SearchAddon class declaration - Update all references to use Constants enum for consistency --- addons/addon-search/src/SearchAddon.ts | 37 ++++++++++++++++++++------ 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/addons/addon-search/src/SearchAddon.ts b/addons/addon-search/src/SearchAddon.ts index 8395e437..5e7c276c 100644 --- a/addons/addon-search/src/SearchAddon.ts +++ b/addons/addon-search/src/SearchAddon.ts @@ -63,11 +63,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(); @@ -92,7 +113,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA constructor(options?: Partial) { super(); - this._highlightLimit = options?.highlightLimit ?? DEFAULT_HIGHLIGHT_LIMIT; + this._highlightLimit = options?.highlightLimit ?? Constants.DEFAULT_HIGHLIGHT_LIMIT; } public activate(terminal: Terminal): void { @@ -446,7 +467,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 { @@ -466,8 +487,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]))); } /**