-
Notifications
You must be signed in to change notification settings - Fork 11
fix(terminal): copy selected web terminal text #382
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,9 @@ import { | |
| forceTerminalSelectionModifier, | ||
| shouldForceBrowserTerminalSelection, | ||
| shouldForceTerminalSelectionContext, | ||
| shouldLetBrowserHandleTerminalCopyShortcut, | ||
| type TerminalCopyInteractionTerminal, | ||
| type TerminalCopyKeyboardEvent, | ||
| type TerminalMouseTrackingMode, | ||
| writeTerminalSelectionToClipboardData | ||
| } from "../../src/web/terminal-copy-interaction.js" | ||
|
|
@@ -27,6 +29,26 @@ const terminalWithSelection = ( | |
| modes: { mouseTrackingMode } | ||
| }) | ||
|
|
||
| const keyboardEvent = ( | ||
| key: string, | ||
| options: Partial<Pick<TerminalCopyKeyboardEvent, "altKey" | "ctrlKey" | "metaKey" | "type">> = {} | ||
| ): TerminalCopyKeyboardEvent => ({ | ||
| altKey: options.altKey ?? false, | ||
| ctrlKey: options.ctrlKey ?? false, | ||
| key, | ||
| metaKey: options.metaKey ?? false, | ||
| type: options.type ?? "keydown" | ||
| }) | ||
|
|
||
| const requireKeyHandler = ( | ||
| keyHandler: ((event: TerminalCopyKeyboardEvent) => boolean) | null | ||
| ): (event: TerminalCopyKeyboardEvent) => boolean => { | ||
| if (keyHandler === null) { | ||
| return expect.fail("Expected terminal copy key handler to be registered.") | ||
| } | ||
| return keyHandler | ||
| } | ||
|
|
||
| describe("terminal copy interaction", () => { | ||
| it("forces browser selection for primary mouse input while terminal mouse tracking is active", () => { | ||
| expect(shouldForceBrowserTerminalSelection({ button: 0 }, terminalWithSelection("any", ""))).toBe(true) | ||
|
|
@@ -42,6 +64,49 @@ describe("terminal copy interaction", () => { | |
| expect(shouldForceTerminalSelectionContext({ button: 0 }, terminalWithSelection("any", "selected"))).toBe(false) | ||
| }) | ||
|
|
||
| it("lets browser copy handle Ctrl/Cmd+C only for selected terminal text", () => { | ||
| const selectedTerminal = terminalWithSelection("any", "selected") | ||
| const emptyTerminal = terminalWithSelection("any", "") | ||
|
|
||
| expect(shouldLetBrowserHandleTerminalCopyShortcut(keyboardEvent("c", { ctrlKey: true }), selectedTerminal)) | ||
| .toBe(true) | ||
| expect(shouldLetBrowserHandleTerminalCopyShortcut(keyboardEvent("C", { metaKey: true }), selectedTerminal)) | ||
| .toBe(true) | ||
| expect(shouldLetBrowserHandleTerminalCopyShortcut(keyboardEvent("c", { ctrlKey: true }), emptyTerminal)) | ||
| .toBe(false) | ||
| expect( | ||
| shouldLetBrowserHandleTerminalCopyShortcut(keyboardEvent("c", { altKey: true, ctrlKey: true }), selectedTerminal) | ||
| ) | ||
| .toBe(false) | ||
| expect(shouldLetBrowserHandleTerminalCopyShortcut(keyboardEvent("v", { ctrlKey: true }), selectedTerminal)) | ||
| .toBe(false) | ||
| expect( | ||
| shouldLetBrowserHandleTerminalCopyShortcut(keyboardEvent("c", { ctrlKey: true, type: "keyup" }), selectedTerminal) | ||
| ).toBe(false) | ||
| }) | ||
|
Comment on lines
+67
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win Отсутствует property-based тест для инварианта copy shortcut. Согласно coding guidelines, математические инварианты должны проверяться через fast-check. Инвариант ♻️ Пример property-based тестаimport * as fc from "fast-check"
const keyArbitrary = fc.constantFrom("c", "C")
const modifierArbitrary = fc.record({
altKey: fc.boolean(),
ctrlKey: fc.boolean(),
metaKey: fc.boolean()
})
it("shouldLetBrowserHandleTerminalCopyShortcut returns true iff copy shortcut with selection", () => {
fc.assert(
fc.property(keyArbitrary, modifierArbitrary, fc.boolean(), (key, mods, hasSelection) => {
const event = keyboardEvent(key, { ...mods, type: "keydown" })
const terminal = terminalWithSelection("any", hasSelection ? "text" : "")
const isCopyShortcut = !mods.altKey && (mods.ctrlKey || mods.metaKey)
const expected = isCopyShortcut && hasSelection
return shouldLetBrowserHandleTerminalCopyShortcut(event, terminal) === expected
})
)
})As per coding guidelines: "Property-based tests (fast-check) must verify mathematical invariants". 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| it("registers a custom key handler that bypasses xterm input for selected text copy", () => { | ||
| let keyHandler: ((event: TerminalCopyKeyboardEvent) => boolean) | null = null | ||
| const terminal: TerminalCopyInteractionTerminal = { | ||
| attachCustomKeyEventHandler: (handler) => { | ||
| keyHandler = handler | ||
| }, | ||
| getSelection: () => "selected", | ||
| hasSelection: () => true, | ||
| modes: { mouseTrackingMode: "none" } | ||
| } | ||
| const host = new FakeTerminalCopyHost(null) | ||
|
|
||
| const disposable = attachTerminalCopyInteraction({ host, terminal }) | ||
| const handleKey = requireKeyHandler(keyHandler) | ||
|
|
||
| expect(handleKey(keyboardEvent("c", { ctrlKey: true }))).toBe(false) | ||
| expect(handleKey(keyboardEvent("c", { ctrlKey: true, type: "keyup" }))).toBe(true) | ||
| expect(handleKey(keyboardEvent("c"))).toBe(true) | ||
|
|
||
| disposable.dispose() | ||
| }) | ||
|
|
||
| it("uses Shift as the forced selection modifier on non-Mac platforms", () => { | ||
| const event = { altKey: false, shiftKey: false } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: ProverCoderAI/docker-git
Length of output: 9008
🏁 Script executed:
Repository: ProverCoderAI/docker-git
Length of output: 11451
🏁 Script executed:
Repository: ProverCoderAI/docker-git
Length of output: 11461
🏁 Script executed:
Repository: ProverCoderAI/docker-git
Length of output: 11461
🏁 Script executed:
Repository: ProverCoderAI/docker-git
Length of output: 4178
🏁 Script executed:
Repository: ProverCoderAI/docker-git
Length of output: 28278
🏁 Script executed:
Repository: ProverCoderAI/docker-git
Length of output: 8326
🏁 Script executed:
Repository: ProverCoderAI/docker-git
Length of output: 2126
Подправить оба замечания: длина selection тут не лишняя, а про key-handler важно контекстно
terminal.getSelection().length > 0вshouldLetBrowserHandleTerminalCopyShortcutне выглядит избыточной: в этом же модулеwriteTerminalSelectionToClipboardDataпослеterminal.hasSelection()дополнительно делаетselection.length === 0и отклоняет кейс рассогласования, значит контрактhasSelection() ⇒ non-empty getSelection()не гарантируется типом/инвариантом.dispose()вterminal-copy-interaction.tsдействительно не “отсоединяет”attachCustomKeyEventHandler, но при cleanup терминала вcleanupTerminalResourcesвызываетсяargs.terminal.dispose()(послеcopyInteractionDisposable.dispose()), поэтому key-handler cleanup фактически делегируетсяterminal.dispose(). Лучше документировать это ожидание/контрактом вызовов либо (если возможно) добавить явную detachment-операцию, еслиcopyInteractionDisposable.dispose()может вызываться без последующегоterminal.dispose().🤖 Prompt for AI Agents