From 337eae7669b2b0e773c4447636c10608c28bc3e0 Mon Sep 17 00:00:00 2001 From: liuxuezhuo Date: Sat, 10 Oct 2026 22:54:53 +0800 Subject: [PATCH] fix(selection): clear editor highlight when selection chip is deleted Clicking the X on a captured selection chip removes the token from the composer input. The SelectionController noticed this and dropped the stored selection, but it forgot to clear the mock highlight shown in the editor (or preview CSS highlight). The highlight would linger because storedSelection was already null by the time any cleanup ran. Call clearHighlight() before nulling storedSelection so the CM6/CSS highlight is removed together with the chip. --- .../chat/controllers/selection-controller.ts | 1 + .../controllers/selection-controller.test.ts | 26 ++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/features/chat/controllers/selection-controller.ts b/src/features/chat/controllers/selection-controller.ts index 7246076..9084e9b 100644 --- a/src/features/chat/controllers/selection-controller.ts +++ b/src/features/chat/controllers/selection-controller.ts @@ -438,6 +438,7 @@ export class SelectionController { if (this.storedToken) { if (!value.includes(this.storedToken)) { const stale = this.storedToken; + this.clearHighlight(); this.storedToken = null; this.storedSelection = null; this.tokenSink.unregister(stale); diff --git a/tests/unit/features/chat/controllers/selection-controller.test.ts b/tests/unit/features/chat/controllers/selection-controller.test.ts index 6bd13ee..b98f454 100644 --- a/tests/unit/features/chat/controllers/selection-controller.test.ts +++ b/tests/unit/features/chat/controllers/selection-controller.test.ts @@ -157,7 +157,7 @@ describe('SelectionController', () => { expect(showSelectionHighlight).toHaveBeenCalledWith(editorView, 0, 4); }); - it('drops the stored selection when the token is removed from the input', () => { + it('drops the stored selection and clears the highlight when the token is removed from the input', () => { controller.start(); jest.advanceTimersByTime(250); expect(controller.hasSelection()).toBe(true); @@ -167,6 +167,7 @@ describe('SelectionController', () => { expect(controller.hasSelection()).toBe(false); expect(tokenSink.unregister).toHaveBeenCalledWith(SOURCE_TOKEN); + expect(hideSelectionHighlight).toHaveBeenCalledWith(editorView); }); it('collapses the editor selection on send so the chip does not come back', () => { @@ -606,6 +607,29 @@ describe('SelectionController', () => { expect(mockHighlights.delete).toHaveBeenCalledWith('qoderian-selection'); }); + it('clears CSS highlight when the selection chip token is removed in reading mode', () => { + const anchorNode = {}; + (global as any).document = { + activeElement: null, + getSelection: jest.fn().mockReturnValue( + createMockDOMSelection('reading selection', anchorNode), + ), + }; + const mockHighlights = { set: jest.fn(), delete: jest.fn() }; + (global as any).CSS = { highlights: mockHighlights }; + + controller.start(); + jest.advanceTimersByTime(250); + expect(controller.hasSelection()).toBe(true); + + inputEl.value = ''; + inputEl.dispatchEvent(new Event('input', { bubbles: true })); + + expect(controller.hasSelection()).toBe(false); + expect(tokenSink.unregister).toHaveBeenCalledWith('@notes/reading.md'); + expect(mockHighlights.delete).toHaveBeenCalledWith('qoderian-selection'); + }); + it('skips CSS highlight for disconnected DOM ranges', () => { const anchorNode = {}; const mockSel = createMockDOMSelection('reading selection', anchorNode);