mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-30 06:02:11 +00:00
fix(tui): respect selection copy modes (#45758)
This commit is contained in:
parent
96d84626f8
commit
8381153418
6 changed files with 352 additions and 37 deletions
|
|
@ -8,7 +8,7 @@ import { ClipboardProvider, useClipboard } from "./context/clipboard"
|
|||
import { LogProvider, useLog, type LogSink } from "./context/log"
|
||||
import { ExitProvider, useExit } from "./context/exit"
|
||||
import { EpilogueProvider } from "./context/epilogue"
|
||||
import * as Selection from "./util/selection"
|
||||
import { Selection } from "./util/selection"
|
||||
import {
|
||||
CliRenderEvents,
|
||||
createCliRenderer,
|
||||
|
|
@ -558,14 +558,16 @@ function App(props: { pair?: DialogPairCredentials }) {
|
|||
}
|
||||
})
|
||||
|
||||
// Let selection copy/dismiss win ahead of normal bindings when explicit copy is required.
|
||||
const copyOnSelectEnabled = () =>
|
||||
(config.data.terminal?.copy ?? (process.platform === "win32" ? "manual" : "select")) === "select"
|
||||
|
||||
// Selection copy/dismiss must precede both app bindings and the terminal pane's raw key forwarding.
|
||||
const offSelectionKeys = keymap.intercept(
|
||||
"key",
|
||||
({ event }) => {
|
||||
if ((config.data.terminal?.copy ?? (process.platform === "win32" ? "manual" : "select")) === "select") return
|
||||
Selection.handleSelectionKey(renderer, toast, event, clipboard)
|
||||
Selection.handleSelectionKey(renderer, toast, event, clipboard, copyOnSelectEnabled())
|
||||
},
|
||||
{ priority: 1 },
|
||||
{ priority: 101 },
|
||||
)
|
||||
onCleanup(() => {
|
||||
offSelectionKeys()
|
||||
|
|
@ -583,8 +585,6 @@ function App(props: { pair?: DialogPairCredentials }) {
|
|||
renderer.clearSelection()
|
||||
}
|
||||
const terminalTitleEnabled = () => config.data.terminal?.title ?? true
|
||||
const copyOnSelectEnabled = () =>
|
||||
(config.data.terminal?.copy ?? (process.platform === "win32" ? "manual" : "select")) === "select"
|
||||
const pasteSummaryEnabled = () => config.data.prompt?.paste !== "full"
|
||||
const tabsVertical = () =>
|
||||
config.data.tabs.layout === "vertical" && sessionTabsFitVertically(dimensions().width, preferredTabsWidth())
|
||||
|
|
|
|||
|
|
@ -20,10 +20,12 @@ type Renderer = {
|
|||
} | null
|
||||
clearSelection: () => void
|
||||
currentFocusedRenderable?: FocusableSelectionTarget | null
|
||||
currentFocusedEditor?: FocusableSelectionTarget | null
|
||||
}
|
||||
|
||||
type SelectionKeyEvent = {
|
||||
ctrl?: boolean
|
||||
baseCode?: number
|
||||
name: string
|
||||
preventDefault: () => void
|
||||
stopPropagation: () => void
|
||||
|
|
@ -36,25 +38,16 @@ export function copyOnSelectRelease(
|
|||
clipboard: ClipboardService,
|
||||
): boolean {
|
||||
if (!event.isDragging) return false
|
||||
const selection = renderer.getSelection()
|
||||
// Preserve the first click so OpenTUI can recognize the following double/triple click.
|
||||
if (selection?.isStart && selection.behavior === "cell") return false
|
||||
return copy(renderer, toast, clipboard)
|
||||
}
|
||||
|
||||
export function copy(renderer: Renderer, toast: Toast, clipboard: ClipboardService): boolean {
|
||||
const selection = renderer.getSelection()
|
||||
if (!selection) return false
|
||||
if (selection.isStart && selection.behavior === "cell") {
|
||||
renderer.clearSelection()
|
||||
return false
|
||||
}
|
||||
if (selection.isStart && selection.behavior === "cell") return false
|
||||
|
||||
const text = selection.getSelectedText()
|
||||
if (!text) {
|
||||
renderer.clearSelection()
|
||||
return false
|
||||
}
|
||||
if (!text) return false
|
||||
|
||||
const focus = renderer.currentFocusedRenderable
|
||||
const clipboardText =
|
||||
|
|
@ -65,8 +58,7 @@ export function copy(renderer: Renderer, toast: Toast, clipboard: ClipboardServi
|
|||
.then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
|
||||
.catch(toast.error)
|
||||
|
||||
// Keep the highlight. clearSelection() also resets OpenTUI's click
|
||||
// counter, so clearing here would turn a triple-click into a new single-click.
|
||||
// Copy never clears selection, including empty releases: clearing also resets multi-click history.
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -75,12 +67,16 @@ export function handleSelectionKey(
|
|||
toast: Toast,
|
||||
event: SelectionKeyEvent,
|
||||
clipboard: ClipboardService,
|
||||
copyOnSelect: boolean,
|
||||
) {
|
||||
const selection = renderer.getSelection()
|
||||
if (!selection) return
|
||||
const focus = renderer.currentFocusedEditor
|
||||
const editing = focus?.hasSelection() && selection.selectedRenderables.includes(focus)
|
||||
|
||||
if (event.ctrl && event.name === "c") {
|
||||
if (!copy(renderer, toast, clipboard)) {
|
||||
// Kitty can report a non-Latin key name with a Latin base-layout C.
|
||||
if (event.ctrl && (event.name === "c" || event.baseCode === 99 || event.baseCode === 67)) {
|
||||
if ((copyOnSelect && !editing) || !copy(renderer, toast, clipboard)) {
|
||||
renderer.clearSelection()
|
||||
return
|
||||
}
|
||||
|
|
@ -91,14 +87,15 @@ export function handleSelectionKey(
|
|||
}
|
||||
|
||||
if (event.name === "escape") {
|
||||
const text = selection.isStart && selection.behavior === "cell" ? "" : selection.getSelectedText()
|
||||
renderer.clearSelection()
|
||||
if (!text) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
|
||||
const focus = renderer.currentFocusedRenderable
|
||||
if (focus?.hasSelection() && selection.selectedRenderables.includes(focus)) return
|
||||
if (editing) return
|
||||
|
||||
renderer.clearSelection()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { type Renderable, ScrollBoxRenderable } from "@opentui/core"
|
||||
import { EmbeddedTerminalRenderable, type Renderable, ScrollBoxRenderable } from "@opentui/core"
|
||||
import { createTestRenderer } from "@opentui/core/testing"
|
||||
import { Effect, FileSystem } from "effect"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
|
|
@ -851,3 +851,144 @@ test("ctrl+c dismisses autocomplete and shell mode before exiting", async () =>
|
|||
await server.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test.each(["manual", "select"] as const)(
|
||||
"selection copy and dismissal respect %s mode in the prompt and terminal pane",
|
||||
async (copy) => {
|
||||
const setup = await createTestRenderer({ width: 100, height: 30, useThread: false, kittyKeyboard: true })
|
||||
setup.renderer.start()
|
||||
const ready = Promise.withResolvers<void>()
|
||||
const session = {
|
||||
id: "dummy",
|
||||
title: "Selection fixture",
|
||||
projectID: "project",
|
||||
location: { directory },
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 0, updated: 0 },
|
||||
}
|
||||
const pty = {
|
||||
id: "pty_fixture",
|
||||
sessionID: session.id,
|
||||
title: "Terminal",
|
||||
command: "/bin/sh",
|
||||
args: [],
|
||||
cwd: directory,
|
||||
status: "running",
|
||||
pid: 1,
|
||||
foregroundProcess: null,
|
||||
size: { cols: 48, rows: 24 },
|
||||
output: { head: 0, tail: 0 },
|
||||
}
|
||||
const input: string[] = []
|
||||
const calls = createFetch((url, request) => {
|
||||
if (url.pathname === "/api/session") return json({ data: [session], cursor: {} })
|
||||
if (url.pathname === "/api/session/dummy") return json({ data: session })
|
||||
if (url.pathname === "/api/session/dummy/message") return json({ data: [], cursor: {} })
|
||||
if (url.pathname === "/api/session/dummy/inbox") return json({ data: [] })
|
||||
if (url.pathname === "/api/session/dummy/permission") return json({ data: [] })
|
||||
if (url.pathname === "/api/experimental/session/dummy/terminal")
|
||||
return json({ data: request.method === "POST" ? pty : [pty] })
|
||||
if (url.pathname === "/api/experimental/persistent-pty/pty_fixture/snapshot")
|
||||
return json({
|
||||
data: {
|
||||
info: pty,
|
||||
text: "alpha beta gamma",
|
||||
checkpoint: Buffer.from("alpha beta gamma").toString("base64"),
|
||||
cursor: { x: 16, y: 0 },
|
||||
},
|
||||
})
|
||||
if (url.pathname === "/api/experimental/persistent-pty/pty_fixture/connect-token")
|
||||
return json({ data: { ticket: "fixture" } })
|
||||
return undefined
|
||||
}, createEventStream())
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(request, server) {
|
||||
if (new URL(request.url).pathname.endsWith("/connect") && server.upgrade(request)) return undefined
|
||||
return calls.fetch(request)
|
||||
},
|
||||
websocket: {
|
||||
open(socket) {
|
||||
socket.send(JSON.stringify({ type: "attached", inputProtocol: 1, role: "controller", info: pty }))
|
||||
socket.send(JSON.stringify({ type: "replay_complete" }))
|
||||
},
|
||||
message(_socket, message) {
|
||||
const data = Buffer.from(message)
|
||||
if (data[0] === 1) input.push(data.subarray(5).toString())
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
const { run } = await import("../src/app")
|
||||
const task = Effect.runPromise(
|
||||
run({
|
||||
app: { name: "test", version: "test", channel: "test" },
|
||||
server: { endpoint: { url: server.url.toString() } },
|
||||
config: {
|
||||
get: async () => ({
|
||||
animations: false,
|
||||
terminal: { copy },
|
||||
session: { terminal: true },
|
||||
}),
|
||||
update: async () => ({}),
|
||||
},
|
||||
packages: { resolve: async () => undefined },
|
||||
args: { sessionID: session.id },
|
||||
terminalHandoff: async () => ({ renderer: setup.renderer, mode: "dark", complete: ready.resolve }),
|
||||
log: () => {},
|
||||
}).pipe(Effect.provide(AppNodeBuilder.build(Global.node)), Effect.provide(FileSystem.layerNoop({}))),
|
||||
)
|
||||
|
||||
await ready.promise
|
||||
await setup.waitForFrame((frame) => frame.includes("commands"))
|
||||
await setup.mockInput.typeText("selection audit draft")
|
||||
setup.mockInput.pressKey("a", { ctrl: true, shift: true })
|
||||
expect(setup.renderer.getSelection()?.getSelectedText()).toBe("selection audit draft")
|
||||
|
||||
setup.mockInput.pressEscape()
|
||||
expect(setup.renderer.hasSelection).toBeFalse()
|
||||
expect(setup.renderer.currentFocusedEditor?.plainText).toBe("selection audit draft")
|
||||
|
||||
setup.mockInput.pressKey("c", { ctrl: true })
|
||||
await setup.waitForFrame((frame) => !frame.includes("selection audit draft"))
|
||||
expect(setup.renderer.currentFocusedEditor?.plainText).toBe("")
|
||||
expect(setup.renderer.hasSelection).toBeFalse()
|
||||
expect(setup.renderer.isDestroyed).toBeFalse()
|
||||
|
||||
await setup.mockInput.typeText("/terminal")
|
||||
await setup.waitForFrame((frame) => frame.includes("New terminal"))
|
||||
setup.mockInput.pressEnter()
|
||||
await setup.waitForFrame((frame) => frame.includes("alpha beta gamma"))
|
||||
setup.mockInput.pressKey("x", { ctrl: true })
|
||||
setup.mockInput.pressArrow("right")
|
||||
const terminal = setup.renderer.currentFocusedRenderable
|
||||
if (!(terminal instanceof EmbeddedTerminalRenderable)) throw new Error("Terminal was not focused")
|
||||
setup.renderer.startSelection(terminal, terminal.x + 6, terminal.y)
|
||||
setup.renderer.updateSelection(terminal, terminal.x + 9, terminal.y, { finishDragging: true })
|
||||
expect(setup.renderer.getSelection()?.getSelectedText()).toBe("beta")
|
||||
|
||||
setup.mockInput.pressEscape()
|
||||
expect(setup.renderer.hasSelection).toBeFalse()
|
||||
expect(terminal.hasSelection()).toBeFalse()
|
||||
|
||||
await setup.mockMouse.click(terminal.x + 6, terminal.y)
|
||||
if (copy === "select") {
|
||||
setup.renderer.updateSelection(terminal, terminal.x + 9, terminal.y, { finishDragging: true })
|
||||
expect(setup.renderer.getSelection()?.getSelectedText()).toBe("beta")
|
||||
}
|
||||
setup.mockInput.pressKey("c", { ctrl: true })
|
||||
await setup.waitFor(() => input.length > 0)
|
||||
expect(input).toEqual(["\x03"])
|
||||
expect(setup.renderer.hasSelection).toBeFalse()
|
||||
expect(setup.renderer.isDestroyed).toBeFalse()
|
||||
|
||||
setup.renderer.destroy()
|
||||
await task
|
||||
} finally {
|
||||
if (!setup.renderer.isDestroyed) setup.renderer.destroy()
|
||||
await server.stop()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
/** @jsxImportSource @opentui/solid */
|
||||
import { expect, test } from "bun:test"
|
||||
import { ManualClock } from "@opentui/core/testing"
|
||||
import { testRender, useRenderer } from "@opentui/solid"
|
||||
import { useClipboard } from "../../src/context/clipboard"
|
||||
import { copyOnSelectRelease } from "../../src/util/selection"
|
||||
|
|
@ -19,7 +20,10 @@ function CopyOnSelectText() {
|
|||
)
|
||||
}
|
||||
|
||||
test("copy-on-select keeps a word highlight so a third click can select the line", async () => {
|
||||
test.each([
|
||||
{ column: 6, word: "beta" },
|
||||
{ column: 17, word: "" },
|
||||
])("copy-on-select preserves multi-clicks at column $column", async (input) => {
|
||||
const writes: string[] = []
|
||||
const app = await testRender(
|
||||
() => (
|
||||
|
|
@ -36,24 +40,24 @@ test("copy-on-select keeps a word highlight so a third click can select the line
|
|||
<CopyOnSelectText />
|
||||
</TestTuiContexts>
|
||||
),
|
||||
{ width: 20, height: 2 },
|
||||
{ width: 20, height: 2, clock: new ManualClock() },
|
||||
)
|
||||
|
||||
try {
|
||||
app.renderer.start()
|
||||
await app.waitForFrame((frame) => frame.includes("beta"))
|
||||
|
||||
await app.mockMouse.click(6, 0)
|
||||
await app.mockMouse.click(input.column, 0)
|
||||
expect(app.renderer.getSelection()?.getSelectedText() ?? "").toBe("")
|
||||
expect(writes).toEqual([])
|
||||
|
||||
await app.mockMouse.click(6, 0)
|
||||
expect(app.renderer.getSelection()?.getSelectedText()).toBe("beta")
|
||||
expect(writes).toEqual(["beta"])
|
||||
await app.mockMouse.click(input.column, 0)
|
||||
expect(app.renderer.getSelection()?.getSelectedText() ?? "").toBe(input.word)
|
||||
expect(writes).toEqual(input.word ? [input.word] : [])
|
||||
|
||||
await app.mockMouse.click(6, 0)
|
||||
await app.mockMouse.click(input.column, 0)
|
||||
expect(app.renderer.getSelection()?.getSelectedText()).toBe("alpha beta gamma")
|
||||
expect(writes).toEqual(["beta", "alpha beta gamma"])
|
||||
expect(writes).toEqual([...(input.word ? [input.word] : []), "alpha beta gamma"])
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
|
|
|
|||
173
packages/tui/test/util/selection-keys.test.ts
Normal file
173
packages/tui/test/util/selection-keys.test.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { EmbeddedTerminalRenderable, InputRenderable, TextareaRenderable, TextRenderable } from "@opentui/core"
|
||||
import { createTestRenderer, ManualClock } from "@opentui/core/testing"
|
||||
import type { ClipboardService } from "../../src/context/clipboard"
|
||||
import { Selection } from "../../src/util/selection"
|
||||
|
||||
async function setup(copyOnSelect = false) {
|
||||
const clock = new ManualClock()
|
||||
const app = await createTestRenderer({
|
||||
width: 24,
|
||||
height: 3,
|
||||
useThread: false,
|
||||
exitOnCtrlC: false,
|
||||
useKittyKeyboard: {},
|
||||
clock,
|
||||
})
|
||||
const writes: string[] = []
|
||||
const clipboard: ClipboardService = {
|
||||
read: async () => undefined,
|
||||
write: async (text) => {
|
||||
writes.push(text)
|
||||
},
|
||||
}
|
||||
app.renderer.keyInput.on("keypress", (event) =>
|
||||
Selection.handleSelectionKey(app.renderer, { show() {}, error() {} }, event, clipboard, copyOnSelect),
|
||||
)
|
||||
return { ...app, clock, writes }
|
||||
}
|
||||
|
||||
async function terminal(copyOnSelect = false) {
|
||||
const app = await setup(copyOnSelect)
|
||||
const input: string[] = []
|
||||
const terminal = new EmbeddedTerminalRenderable(app.renderer, {
|
||||
width: 24,
|
||||
height: 3,
|
||||
onData(data, source) {
|
||||
if (source === "input") input.push(Buffer.from(data).toString())
|
||||
},
|
||||
})
|
||||
app.renderer.root.add(terminal)
|
||||
terminal.write("alpha beta gamma")
|
||||
terminal.focus()
|
||||
await app.renderOnce()
|
||||
return { ...app, terminal, input }
|
||||
}
|
||||
|
||||
test("terminal selections retain repeated copies until Escape or typing dismisses them", async () => {
|
||||
const app = await terminal()
|
||||
try {
|
||||
await app.mockMouse.drag(6, 0, 9, 0)
|
||||
|
||||
app.mockInput.pressCtrlC()
|
||||
app.mockInput.pressCtrlC()
|
||||
expect(app.writes).toEqual(["beta", "beta"])
|
||||
expect(app.renderer.getSelection()?.getSelectedText()).toBe("beta")
|
||||
expect(app.input).toEqual([])
|
||||
|
||||
app.mockInput.pressEscape()
|
||||
app.clock.advance(20)
|
||||
expect(app.renderer.hasSelection).toBeFalse()
|
||||
expect(app.terminal.hasSelection()).toBeFalse()
|
||||
expect(app.input).toEqual([])
|
||||
|
||||
app.mockInput.pressCtrlC()
|
||||
expect(app.input).toEqual(["\x03"])
|
||||
|
||||
await app.mockMouse.drag(6, 0, 9, 0)
|
||||
app.mockInput.pressKey("x")
|
||||
expect(app.renderer.hasSelection).toBeFalse()
|
||||
expect(app.terminal.hasSelection()).toBeFalse()
|
||||
expect(app.input).toEqual(["\x03", "x"])
|
||||
|
||||
app.mockInput.pressCtrlC()
|
||||
expect(app.input).toEqual(["\x03", "x", "\x03"])
|
||||
expect(app.writes).toEqual(["beta", "beta"])
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("copy-on-select forwards Ctrl+C without recopying the selection", async () => {
|
||||
const app = await terminal(true)
|
||||
try {
|
||||
await app.mockMouse.drag(6, 0, 9, 0)
|
||||
expect(app.renderer.getSelection()?.getSelectedText()).toBe("beta")
|
||||
|
||||
app.mockInput.pressCtrlC()
|
||||
expect(app.writes).toEqual([])
|
||||
expect(app.input).toEqual(["\x03"])
|
||||
expect(app.renderer.hasSelection).toBeFalse()
|
||||
expect(app.terminal.hasSelection()).toBeFalse()
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test.each(["click", "empty drag"])("a terminal %s does not consume Ctrl+C or Escape", async (gesture) => {
|
||||
const app = await terminal()
|
||||
const select = () => (gesture === "click" ? app.mockMouse.click(6, 0) : app.mockMouse.drag(18, 0, 21, 0))
|
||||
try {
|
||||
await select()
|
||||
app.mockInput.pressCtrlC()
|
||||
expect(app.renderer.hasSelection).toBeFalse()
|
||||
expect(app.input).toEqual(["\x03"])
|
||||
|
||||
await select()
|
||||
app.mockInput.pressEscape()
|
||||
app.clock.advance(20)
|
||||
expect(app.renderer.hasSelection).toBeFalse()
|
||||
expect(app.input).toEqual(["\x03", "\x1b"])
|
||||
expect(app.writes).toEqual([])
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test.each(["manual", "select"].flatMap((mode) => ["textarea", "input"].map((kind) => ({ mode, kind }))))(
|
||||
"$kind selections can be copied and edited in $mode mode",
|
||||
async (input) => {
|
||||
const app = await setup(input.mode === "select")
|
||||
const editor =
|
||||
input.kind === "input"
|
||||
? new InputRenderable(app.renderer, { width: 24, value: "draft" })
|
||||
: new TextareaRenderable(app.renderer, { width: 24, height: 3, initialValue: "draft" })
|
||||
app.renderer.root.add(editor)
|
||||
editor.focus()
|
||||
try {
|
||||
await app.renderOnce()
|
||||
app.mockInput.pressKey("END")
|
||||
app.mockInput.pressArrow("left", { shift: true })
|
||||
|
||||
app.mockInput.pressCtrlC()
|
||||
app.mockInput.pressKey("\x1b[1089::99;5u")
|
||||
expect(app.writes).toEqual(["t", "t"])
|
||||
expect(app.renderer.getSelection()?.getSelectedText()).toBe("t")
|
||||
expect(editor.plainText).toBe("draft")
|
||||
|
||||
app.mockInput.pressArrow("left", { shift: true })
|
||||
expect(app.renderer.getSelection()?.getSelectedText()).toBe("ft")
|
||||
app.mockInput.pressKey("x")
|
||||
expect(editor.plainText).toBe("drax")
|
||||
expect(app.renderer.hasSelection).toBeFalse()
|
||||
expect(app.writes).toEqual(["t", "t"])
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
test("copy-on-select does not treat output selection as editor selection when an editor is focused", async () => {
|
||||
const app = await setup(true)
|
||||
const editor = new TextareaRenderable(app.renderer, { width: 24, height: 1, initialValue: "draft" })
|
||||
app.renderer.root.add(new TextRenderable(app.renderer, { width: 24, height: 1, content: "alpha beta gamma" }))
|
||||
app.renderer.root.add(editor)
|
||||
editor.focus()
|
||||
const forwarded: string[] = []
|
||||
app.renderer.keyInput.on("keypress", (event) => {
|
||||
if (!event.defaultPrevented) forwarded.push(event.name)
|
||||
})
|
||||
try {
|
||||
await app.renderOnce()
|
||||
await app.mockMouse.drag(6, 0, 9, 0)
|
||||
expect(app.renderer.currentFocusedEditor === editor).toBeTrue()
|
||||
expect(app.renderer.getSelection()?.getSelectedText()).toBe("beta")
|
||||
|
||||
app.mockInput.pressCtrlC()
|
||||
expect(app.writes).toEqual([])
|
||||
expect(forwarded).toEqual(["c"])
|
||||
expect(app.renderer.hasSelection).toBeFalse()
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
|
@ -92,17 +92,17 @@ test.each(["word", "line"] as const)("copy-on-select copies a %s selection witho
|
|||
expect(value.writes).toEqual(["selected"])
|
||||
})
|
||||
|
||||
test("clears a click-only selection without copying", () => {
|
||||
test("ignores a click-only selection without copying or clearing", () => {
|
||||
const value = setup("x", true)
|
||||
expect(Selection.copy(value.renderer, value.toast, value.clipboard)).toBeFalse()
|
||||
expect(value.clears()).toBe(1)
|
||||
expect(value.clears()).toBe(0)
|
||||
expect(value.writes).toEqual([])
|
||||
})
|
||||
|
||||
test("clears an empty dragged selection without copying", () => {
|
||||
test("ignores an empty dragged selection without copying or clearing", () => {
|
||||
const value = setup("", false)
|
||||
expect(Selection.copy(value.renderer, value.toast, value.clipboard)).toBeFalse()
|
||||
expect(value.clears()).toBe(1)
|
||||
expect(value.clears()).toBe(0)
|
||||
expect(value.writes).toEqual([])
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue