Stop the clipboard picker freezing on huge pastes (#6568)
Every keystroke in the search box scanned, lowercased, and split the full text of every history entry, and the preview pane laid out the entire selection with WrapAnywhere. A single 1.6MB paste (or a large file selection) turned that into hundreds of megabytes of work on the shell thread and stalled the render thread — freezing the whole desktop. Cap each entry once as it enters the display, so searching, previewing, and rendering all work on a bounded prefix. Pasting reads the full entry back from history by index, so nothing is actually lost. The cut lands on a line break, keeping a file:// URI from truncating into a bogus path. Co-authored-by: markbusking <marcosbustos.dev@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
markbusking
Claude Opus 5
parent
dd61d4a75b
commit
c4dda58ba2
@@ -157,6 +157,20 @@ function fullText(entry) {
|
|||||||
return String(entry.text || "")
|
return String(entry.text || "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The picker only ever searches and renders a prefix of an entry, so scan and
|
||||||
|
// render just that much. A single huge paste otherwise costs hundreds of
|
||||||
|
// megabytes of string work on every keystroke and stalls the whole shell.
|
||||||
|
// Pasting reads the full entry back from history by index, so nothing is lost.
|
||||||
|
var displayTextLimit = 8192
|
||||||
|
|
||||||
|
function cappedEntry(entry) {
|
||||||
|
if (!entry || entry.type !== "text" || entry.text.length <= displayTextLimit) return entry
|
||||||
|
|
||||||
|
// Cut on a line break so a file:// URI never truncates into a bogus path.
|
||||||
|
var cut = entry.text.lastIndexOf("\n", displayTextLimit)
|
||||||
|
return { type: "text", text: entry.text.slice(0, cut > 0 ? cut : displayTextLimit) }
|
||||||
|
}
|
||||||
|
|
||||||
function displayRows(history, query, limit) {
|
function displayRows(history, query, limit) {
|
||||||
var values = Array.isArray(history) ? history : []
|
var values = Array.isArray(history) ? history : []
|
||||||
var needle = String(query || "").trim().toLowerCase()
|
var needle = String(query || "").trim().toLowerCase()
|
||||||
@@ -168,7 +182,7 @@ function displayRows(history, query, limit) {
|
|||||||
var rows = []
|
var rows = []
|
||||||
|
|
||||||
for (var i = 0; i < values.length; i++) {
|
for (var i = 0; i < values.length; i++) {
|
||||||
var entry = normalizeEntry(values[i])
|
var entry = cappedEntry(normalizeEntry(values[i]))
|
||||||
if (!entry) continue
|
if (!entry) continue
|
||||||
if (needle && searchableText(entry).toLowerCase().indexOf(needle) < 0) continue
|
if (needle && searchableText(entry).toLowerCase().indexOf(needle) < 0) continue
|
||||||
|
|
||||||
|
|||||||
@@ -179,6 +179,32 @@ assertEqual(
|
|||||||
2,
|
2,
|
||||||
'clipboard respawns both watchers when they die'
|
'clipboard respawns both watchers when they die'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assertDeepEqual(
|
||||||
|
clipboard.displayRows([{ type: 'text', text: 'a'.repeat(8192) + 'needle' }], 'needle', 50),
|
||||||
|
[],
|
||||||
|
'clipboard display rows do not search text past the cap'
|
||||||
|
)
|
||||||
|
|
||||||
|
assertDeepEqual(
|
||||||
|
clipboard.displayRows([{ type: 'text', text: 'needle' + 'a'.repeat(100000) }], 'needle', 50).map(row => row.index),
|
||||||
|
[0],
|
||||||
|
'clipboard display rows search text up to the cap'
|
||||||
|
)
|
||||||
|
|
||||||
|
const hugeTextRow = clipboard.displayRows([{ type: 'text', text: 'z'.repeat(100000) }], '', 50)[0]
|
||||||
|
assert(
|
||||||
|
hugeTextRow.fullText.length === 8192 && hugeTextRow.previewText.length === 8192,
|
||||||
|
'clipboard display rows cap what a huge text entry renders'
|
||||||
|
)
|
||||||
|
|
||||||
|
const hugeFileList = []
|
||||||
|
for (let i = 0; i < 5000; i++) hugeFileList.push('file:///home/dhh/clip-' + i + '.mp4')
|
||||||
|
const hugeFileRow = clipboard.displayRows([{ type: 'text', text: hugeFileList.join('\n') + '\n' }], '', 50)[0]
|
||||||
|
assert(
|
||||||
|
hugeFileRow.entryType === 'file' && hugeFileRow.fullText.split('\n').every(path => path.endsWith('.mp4')),
|
||||||
|
'clipboard display rows cap a huge file list without truncating a path'
|
||||||
|
)
|
||||||
JS
|
JS
|
||||||
|
|
||||||
TMPDIR=$(mktemp -d)
|
TMPDIR=$(mktemp -d)
|
||||||
|
|||||||
Reference in New Issue
Block a user