Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@
"@ai-sdk/groq@3.0.31": "patches/@ai-sdk%2Fgroq@3.0.31.patch",
"@ai-sdk/anthropic@3.0.111": "patches/@ai-sdk%2Fanthropic@3.0.111.patch",
"@ai-sdk/amazon-bedrock@4.0.166": "patches/@ai-sdk%2Famazon-bedrock@4.0.166.patch",
"@ai-sdk/openai@3.0.88": "patches/@ai-sdk%2Fopenai@3.0.88.patch"
"@ai-sdk/openai@3.0.88": "patches/@ai-sdk%2Fopenai@3.0.88.patch",
"@opentui/core@0.4.5": "patches/@opentui%2Fcore@0.4.5.patch"
}
}
64 changes: 64 additions & 0 deletions packages/tui/test/component/prompt-placeholder-delete.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/** @jsxImportSource @opentui/solid */
import { afterAll, beforeAll, describe, expect, test } from "bun:test"
import type { TextareaRenderable } from "@opentui/core"
import { testRender } from "@opentui/solid"

// The prompt marks a paste placeholder with a virtual extmark. Word and line deletes go
// through deleteRange, which @opentui/core 0.4.5 is patched to widen over a virtual
// extmark it cuts into (anomalyco/opentui#1628).
const placeholder = "[Pasted ~5 lines]"
const text = `see ${placeholder} x`
const start = "see ".length
const end = start + placeholder.length

let app: Awaited<ReturnType<typeof testRender>>
let input: TextareaRenderable

beforeAll(async () => {
const textarea = Promise.withResolvers<TextareaRenderable>()
app = await testRender(
() => (
<box width="100%">
<textarea width="100%" maxHeight={6} ref={textarea.resolve} />
</box>
),
{ width: 60, height: 10 },
)
await app.flush()
input = await textarea.promise
})

afterAll(() => app.renderer.destroy())

function setup(cursor: number) {
input.setText(text)
const id = input.extmarks.create({ start, end, virtual: true })
input.cursorOffset = cursor
return id
}

describe("deleting a paste placeholder", () => {
test.each([
["delete word backward", end, () => input.deleteWordBackward()],
["delete word forward", start, () => input.deleteWordForward()],
] as const)("%s removes the whole placeholder", (_, cursor, run) => {
const id = setup(cursor)
run()
expect(input.plainText).toBe("see x")
expect(input.extmarks.get(id)).toBeNull()
})

test("delete to line end from inside the line removes the placeholder it reaches", () => {
const id = setup(2)
input.deleteToLineEnd()
expect(input.plainText).toBe("se")
expect(input.extmarks.get(id)).toBeNull()
})

test("a range ending inside the placeholder widens to the whole placeholder", () => {
const id = setup(0)
input.editBuffer.deleteRange(0, 2, 0, start + 3)
expect(input.plainText).toBe("se x")
expect(input.extmarks.get(id)).toBeNull()
})
})
72 changes: 72 additions & 0 deletions patches/@opentui%2Fcore@0.4.5.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
diff --git a/chunk-bun-t2myhmwd.js b/chunk-bun-t2myhmwd.js
index 701d70dea2174e98ab56f4d614e678e868561479..c4b73d0825c4b46a99e5ec4f2811a40a3bc4e3ae 100644
--- a/chunk-bun-t2myhmwd.js
+++ b/chunk-bun-t2myhmwd.js
@@ -9647,11 +9647,11 @@ class ExtmarksController {
return;
}
this.saveSnapshot();
- const startOffset = this.positionToOffset(startLine, startCol);
- const endOffset = this.positionToOffset(endLine, endCol);
- const length = endOffset - startOffset;
- this.originalDeleteRange(startLine, startCol, endLine, endCol);
- this.adjustExtmarksAfterDeletion(startOffset, length);
+ const [startOffset, endOffset] = this.expandToVirtualExtmarks(this.positionToOffset(startLine, startCol), this.positionToOffset(endLine, endCol));
+ const start = this.offsetToPosition(startOffset);
+ const end = this.offsetToPosition(endOffset);
+ this.originalDeleteRange(start.row, start.col, end.row, end.col);
+ this.adjustExtmarksAfterDeletion(startOffset, endOffset - startOffset);
};
this.editBuffer.deleteLine = () => {
if (this.destroyed) {
@@ -9781,6 +9781,14 @@ class ExtmarksController {
this.metadata.delete(id);
}
}
+ expandToVirtualExtmarks(start, end) {
+ if (start >= end)
+ return [start, end];
+ const cut = [...this.extmarks.values()].find((extmark) => extmark.virtual && extmark.start < end && extmark.end > start && (extmark.start < start || extmark.end > end));
+ if (!cut)
+ return [start, end];
+ return this.expandToVirtualExtmarks(Math.min(start, cut.start), Math.max(end, cut.end));
+ }
findVirtualExtmarkContaining(offset) {
for (const extmark of this.extmarks.values()) {
if (extmark.virtual && offset >= extmark.start && offset < extmark.end) {
diff --git a/chunk-node-q0cwyvm9.js b/chunk-node-q0cwyvm9.js
index be7df0e11bc6eac9ac8aacacdf2a4686670f09e1..39a9a428700d041c109f4a53bb3a4fd0094e53ee 100644
--- a/chunk-node-q0cwyvm9.js
+++ b/chunk-node-q0cwyvm9.js
@@ -9626,11 +9626,11 @@ class ExtmarksController {
return;
}
this.saveSnapshot();
- const startOffset = this.positionToOffset(startLine, startCol);
- const endOffset = this.positionToOffset(endLine, endCol);
- const length = endOffset - startOffset;
- this.originalDeleteRange(startLine, startCol, endLine, endCol);
- this.adjustExtmarksAfterDeletion(startOffset, length);
+ const [startOffset, endOffset] = this.expandToVirtualExtmarks(this.positionToOffset(startLine, startCol), this.positionToOffset(endLine, endCol));
+ const start = this.offsetToPosition(startOffset);
+ const end = this.offsetToPosition(endOffset);
+ this.originalDeleteRange(start.row, start.col, end.row, end.col);
+ this.adjustExtmarksAfterDeletion(startOffset, endOffset - startOffset);
};
this.editBuffer.deleteLine = () => {
if (this.destroyed) {
@@ -9760,6 +9760,14 @@ class ExtmarksController {
this.metadata.delete(id);
}
}
+ expandToVirtualExtmarks(start, end) {
+ if (start >= end)
+ return [start, end];
+ const cut = [...this.extmarks.values()].find((extmark) => extmark.virtual && extmark.start < end && extmark.end > start && (extmark.start < start || extmark.end > end));
+ if (!cut)
+ return [start, end];
+ return this.expandToVirtualExtmarks(Math.min(start, cut.start), Math.max(end, cut.end));
+ }
findVirtualExtmarkContaining(offset) {
for (const extmark of this.extmarks.values()) {
if (extmark.virtual && offset >= extmark.start && offset < extmark.end) {
Loading