Skip to content

Commit 59f6195

Browse files
committed
fix(app): preserve terminal selection copy
1 parent 08042da commit 59f6195

3 files changed

Lines changed: 82 additions & 0 deletions

File tree

packages/app/src/web/terminal-copy-interaction.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,11 @@ const suppressOriginalTerminalMouseUp = (event: TerminalCopyMouseEvent): void =>
208208
event.stopImmediatePropagation?.()
209209
}
210210

211+
const suppressTerminalMouseReport = (event: TerminalCopyMouseEvent): void => {
212+
event.stopPropagation?.()
213+
event.stopImmediatePropagation?.()
214+
}
215+
211216
const replayForcedTerminalMouseUp = (
212217
target: TerminalSelectionDragTarget,
213218
event: TerminalCopyMouseEvent
@@ -283,6 +288,10 @@ export const attachTerminalCopyInteraction = (
283288
return
284289
}
285290
forceTerminalSelectionModifier(event)
291+
if (forceSelectionContext) {
292+
suppressTerminalMouseReport(event)
293+
return
294+
}
286295
if (forceBrowserSelection) {
287296
selectionDrag.start()
288297
}

packages/app/tests/docker-git/fixtures/terminal-copy-interaction.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,24 @@ export class FakeTerminalCopyMouseEvent extends Event {
135135
}
136136
}
137137

138+
export class FakeTerminalCopyClipboardEvent {
139+
readonly clipboardData: TerminalCopyTestClipboardData | null
140+
preventDefaultCalls = 0
141+
stopPropagationCalls = 0
142+
143+
constructor(clipboardData: TerminalCopyTestClipboardData | null) {
144+
this.clipboardData = clipboardData
145+
}
146+
147+
preventDefault(): void {
148+
this.preventDefaultCalls += 1
149+
}
150+
151+
stopPropagation(): void {
152+
this.stopPropagationCalls += 1
153+
}
154+
}
155+
138156
const isPropagationStopped = (event: TerminalCopyTestMouseEvent): boolean =>
139157
event instanceof FakeTerminalCopyMouseEvent &&
140158
(event.stopPropagationCalls > 0 || event.stopImmediatePropagationCalls > 0)
@@ -199,6 +217,14 @@ export class FakeTerminalCopyEventTarget {
199217
this.dispatchMousePhase(type, event, "bubble")
200218
}
201219

220+
dispatchCopy(event: TerminalCopyTestClipboardEvent): void {
221+
for (const entry of this.listeners) {
222+
if (entry.type === "copy") {
223+
entry.listener(event)
224+
}
225+
}
226+
}
227+
202228
dispatchEvent(event: Event): boolean {
203229
this.dispatchedEvents.push(event)
204230
if (isMouseTestEventType(event.type) && isTerminalCopyTestMouseEvent(event)) {
@@ -247,6 +273,10 @@ export const mouseEvent = (
247273
>
248274
): FakeTerminalCopyMouseEvent => new FakeTerminalCopyMouseEvent(type, button, options)
249275

276+
export const copyEvent = (
277+
clipboardData: TerminalCopyTestClipboardData | null
278+
): FakeTerminalCopyClipboardEvent => new FakeTerminalCopyClipboardEvent(clipboardData)
279+
250280
export const expectNoDragListeners = (target: FakeTerminalCopyEventTarget): void => {
251281
expect(target.captureListenerCount("mousemove")).toBe(0)
252282
expect(target.captureListenerCount("mouseup")).toBe(0)

packages/app/tests/docker-git/terminal-copy-interaction.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
writeTerminalSelectionToClipboardData
1111
} from "../../src/web/terminal-copy-interaction.js"
1212
import {
13+
copyEvent,
1314
expectNoDragListeners,
1415
expectSingleMouseEvent,
1516
FakeTerminalCopyEventTarget,
@@ -204,6 +205,48 @@ describe("terminal copy interaction", () => {
204205
disposable.dispose()
205206
})
206207

208+
it("keeps selected terminal text copyable after right-click while mouse tracking is active", () => {
209+
const documentTarget = new FakeTerminalCopyEventTarget()
210+
const host = new FakeTerminalCopyHost(documentTarget)
211+
const selectedText = "line one\nline two"
212+
let terminalSelection = selectedText
213+
const terminal: TerminalCopyInteractionTerminal = {
214+
getSelection: () => terminalSelection,
215+
hasSelection: () => terminalSelection.length > 0,
216+
modes: { mouseTrackingMode: "any" }
217+
}
218+
const terminalMouseReports: Array<TerminalCopyTestMouseEvent> = []
219+
const clipboardWrites: Array<{ readonly data: string; readonly format: string }> = []
220+
const disposable = attachTerminalCopyInteraction({ host, terminal })
221+
host.addBubbleMouseListener("mousedown", (event) => {
222+
terminalMouseReports.push(event)
223+
terminalSelection = ""
224+
})
225+
const rightClick = mouseEvent(2)
226+
const clipboardData = {
227+
setData: (format: string, data: string) => {
228+
clipboardWrites.push({ data, format })
229+
}
230+
}
231+
const copy = copyEvent(clipboardData)
232+
233+
host.dispatchMouse("mousedown", rightClick)
234+
host.dispatchCopy(copy)
235+
236+
expect(rightClick.shiftKey).toBe(true)
237+
expect(rightClick.preventDefaultCalls).toBe(0)
238+
expect(rightClick.stopImmediatePropagationCalls).toBe(1)
239+
expect(rightClick.stopPropagationCalls).toBeGreaterThanOrEqual(1)
240+
expect(terminalMouseReports).toEqual([])
241+
expect(terminalSelection).toBe(selectedText)
242+
expectNoDragListeners(documentTarget)
243+
expect(clipboardWrites).toEqual([{ data: selectedText, format: "text/plain" }])
244+
expect(copy.preventDefaultCalls).toBe(1)
245+
expect(copy.stopPropagationCalls).toBe(1)
246+
247+
disposable.dispose()
248+
})
249+
207250
it("falls back to host drag listeners when ownerDocument is unavailable", () => {
208251
const host = new FakeTerminalCopyHost(null)
209252
const disposable = attachTerminalCopyInteraction({ host, terminal: terminalWithSelection("drag", "") })

0 commit comments

Comments
 (0)