diff --git a/src/browser/services/SelectionService.test.ts b/src/browser/services/SelectionService.test.ts index 67158def..9213e8f8 100644 --- a/src/browser/services/SelectionService.test.ts +++ b/src/browser/services/SelectionService.test.ts @@ -496,5 +496,19 @@ describe('SelectionService', () => { assert.isFalse(selectionService.areCoordsInSelection([2, 1], [2, 0], [2, 1])); }); }); + + describe('onSelectionChange', () => { + it('should not fire when setting model properties directly', () => { + buffer.lines.set(0, stringToRow('foo bar')); + + let eventFired = false; + selectionService.onSelectionChange(() => { eventFired = true; }); + + selectionService.model.selectionStart = [0, 0]; + selectionService.model.selectionEnd = undefined; + + assert.isFalse(eventFired); + }); + }); }); diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index 39d666de..6abd5fb9 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -532,6 +532,9 @@ export class SelectionService extends Disposable implements ISelectionService { * @param event The mouse event. */ private _handleSingleClick(event: MouseEvent): void { + // Track if there was a selection before clearing + const hadSelection = this.hasSelection; + this._model.selectionStartLength = 0; this._model.isSelectAllActive = false; this._activeSelectionMode = this.shouldColumnSelect(event) ? SelectionMode.COLUMN : SelectionMode.NORMAL; @@ -543,6 +546,11 @@ export class SelectionService extends Disposable implements ISelectionService { } this._model.selectionEnd = undefined; + // Fire selection change event if a selection was cleared + if (hadSelection) { + this._fireOnSelectionChange(this._model.finalSelectionStart, this._model.finalSelectionEnd, false); + } + // Ensure the line exists const line = this._bufferService.buffer.lines.get(this._model.selectionStart[1]); if (!line) { diff --git a/test/playwright/Terminal.test.ts b/test/playwright/Terminal.test.ts index f03d687d..5deea6d1 100644 --- a/test/playwright/Terminal.test.ts +++ b/test/playwright/Terminal.test.ts @@ -374,6 +374,53 @@ test.describe('API Integration Tests', () => { await pollFor(ctx.page, `window.callCount`, 2); }); + test('onSelectionChange should fire on mousedown when clearing selection', async () => { + await openTerminal(ctx); + await ctx.proxy.write('foo bar baz'); + await ctx.page.evaluate(` + window.callCount = 0; + window.term.onSelectionChange(() => window.callCount++); + `); + await ctx.page.evaluate(`window.term.selectAll()`); + await pollFor(ctx.page, `window.callCount`, 1); + + const termCoords: any = await ctx.page.evaluate(` + (function() { + const rect = window.term.element.getBoundingClientRect(); + const dim = window.term.dimensions; + return { left: rect.left, top: rect.top, cellWidth: dim.css.cell.width, cellHeight: dim.css.cell.height }; + })(); + `); + const x = termCoords.left + termCoords.cellWidth * 5; + const y = termCoords.top + termCoords.cellHeight * 0.5; + await ctx.page.mouse.click(x, y); + + await pollFor(ctx.page, `window.callCount`, 2); + }); + + test('onSelectionChange should not fire on mousedown when no prior selection', async () => { + await openTerminal(ctx); + await ctx.proxy.write('foo bar baz'); + await ctx.page.evaluate(` + window.callCount = 0; + window.term.onSelectionChange(() => window.callCount++); + `); + await pollFor(ctx.page, `window.callCount`, 0); + + const termCoords: any = await ctx.page.evaluate(` + (function() { + const rect = window.term.element.getBoundingClientRect(); + const dim = window.term.dimensions; + return { left: rect.left, top: rect.top, cellWidth: dim.css.cell.width, cellHeight: dim.css.cell.height }; + })(); + `); + const x = termCoords.left + termCoords.cellWidth * 5; + const y = termCoords.top + termCoords.cellHeight * 0.5; + await ctx.page.mouse.click(x, y); + + await pollFor(ctx.page, `window.callCount`, 0); + }); + test('onRender', async () => { await openTerminal(ctx); await timeout(20); // Ensure all init events are fired