Skip to content

Commit 9233ecb

Browse files
committed
improvement(ui): reduce large-paste admission overhead
1 parent 1289be8 commit 9233ecb

5 files changed

Lines changed: 79 additions & 48 deletions

File tree

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/paste-admission.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,14 @@ export function createRichMarkdownPasteAdmission({
3434
const currentText = getCurrentText()
3535
const { from, to } = view.state.selection
3636
const replacedText = view.state.doc.textBetween(from, to, '\n')
37+
const replacesWholeDocument = from <= 1 && to >= view.state.doc.content.size - 1
38+
const projectedCharacters = replacesWholeDocument
39+
? pastedText.length
40+
: Math.max(0, currentText.length - replacedText.length) + pastedText.length
41+
if (projectedCharacters <= Math.floor(maxResultBytes / 3)) return false
42+
3743
const currentBytes = utf8ByteLength(currentText, maxResultBytes)
3844
const pastedBytes = utf8ByteLength(pastedText, maxResultBytes)
39-
const replacesWholeDocument = from <= 1 && to >= view.state.doc.content.size - 1
4045
const replacedBytes = replacesWholeDocument
4146
? currentBytes
4247
: utf8ByteLength(replacedText, maxResultBytes)

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-paste.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ describe('parseBoundedTsv', () => {
2222
it('preserves an intentional empty final cell', () => {
2323
expect(parseBoundedTsv('a\t', 2)).toEqual({ rows: [['a', '']], maxColumns: 2 })
2424
})
25+
26+
it('preserves interior blank rows and classic Mac row separators', () => {
27+
expect(parseBoundedTsv('a\r\rb', 2)).toEqual({
28+
rows: [['a'], [''], ['b']],
29+
maxColumns: 1,
30+
})
31+
})
2532
})
2633

2734
describe('exceedsTablePasteRowLimit', () => {

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-paste.ts

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -21,47 +21,31 @@ export function parseBoundedTsv(text: string, columnLimit: number): ParsedTableP
2121
if (!text || columnLimit < 1) return { rows: [], maxColumns: 0 }
2222

2323
const rows: string[][] = []
24-
let row: string[] = []
25-
let column = 0
26-
let cellStart = 0
2724
let maxColumns = 0
28-
29-
const finishCell = (end: number) => {
30-
if (column < columnLimit) row.push(text.slice(cellStart, end))
31-
column += 1
32-
}
33-
34-
const finishRow = () => {
35-
if (row.length > 0) {
36-
maxColumns = Math.max(maxColumns, row.length)
37-
rows.push(row)
38-
}
39-
row = []
40-
column = 0
41-
}
42-
43-
for (let index = 0; index <= text.length; index++) {
44-
if (index === text.length) {
45-
if (cellStart < text.length || column > 0) {
46-
finishCell(index)
47-
finishRow()
25+
const pushRow = (rowStart: number, rowEnd: number) => {
26+
const row: string[] = []
27+
let cellStart = rowStart
28+
while (row.length < columnLimit) {
29+
const tab = text.indexOf('\t', cellStart)
30+
if (tab < 0 || tab >= rowEnd) {
31+
row.push(text.slice(cellStart, rowEnd))
32+
break
4833
}
49-
break
50-
}
51-
52-
const code = text.charCodeAt(index)
53-
if (code === 9) {
54-
finishCell(index)
55-
cellStart = index + 1
56-
continue
34+
row.push(text.slice(cellStart, tab))
35+
cellStart = tab + 1
5736
}
58-
if (code !== 10 && code !== 13) continue
37+
maxColumns = Math.max(maxColumns, row.length)
38+
rows.push(row)
39+
}
5940

60-
finishCell(index)
61-
finishRow()
62-
if (code === 13 && text.charCodeAt(index + 1) === 10) index += 1
63-
cellStart = index + 1
41+
let rowStart = 0
42+
const rowBreak = /\r\n|\r|\n/g
43+
let match: RegExpExecArray | null
44+
while ((match = rowBreak.exec(text))) {
45+
pushRow(rowStart, match.index)
46+
rowStart = rowBreak.lastIndex
6447
}
48+
if (rowStart < text.length) pushRow(rowStart, text.length)
6549

6650
return { rows, maxColumns }
6751
}

packages/utils/src/paste.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ describe('assessTextPaste', () => {
5252
})
5353
})
5454

55+
it('accepts payloads with worst-case UTF-8 headroom without measuring exact bytes', () => {
56+
expect(assessTextPaste({ pastedText: '💡'.repeat(100), maxPastedBytes: 1_000 })).toEqual({
57+
accepted: true,
58+
})
59+
})
60+
5561
it('rejects a projected result above its character limit', () => {
5662
expect(
5763
assessTextPaste({

packages/utils/src/paste.ts

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ export function utf8ByteLength(value: string, stopAfter = Number.POSITIVE_INFINI
100100
return utf8ByteLengthRange(value, 0, value.length, stopAfter)
101101
}
102102

103+
function isGuaranteedWithinUtf8Limit(characters: number, limit: number): boolean {
104+
return characters <= Math.floor(limit / 3)
105+
}
106+
103107
function normalizedSelection(input: TextPasteAdmissionInput): {
104108
currentText: string
105109
start: number
@@ -139,6 +143,40 @@ export function assessTextPaste(input: TextPasteAdmissionInput): TextPasteAdmiss
139143
}
140144
}
141145

146+
const projectsResult =
147+
input.maxResultBytes !== undefined || input.maxResultCharacters !== undefined
148+
if (
149+
!projectsResult &&
150+
input.maxPastedBytes !== undefined &&
151+
isGuaranteedWithinUtf8Limit(pastedText.length, input.maxPastedBytes)
152+
) {
153+
return { accepted: true }
154+
}
155+
156+
const selection = projectsResult ? normalizedSelection(input) : null
157+
const resultCharacters = selection
158+
? selection.currentText.length - (selection.end - selection.start) + pastedText.length
159+
: undefined
160+
if (
161+
input.maxResultCharacters !== undefined &&
162+
resultCharacters !== undefined &&
163+
resultCharacters > input.maxResultCharacters
164+
) {
165+
return {
166+
accepted: false,
167+
reason: 'result-characters',
168+
actual: resultCharacters,
169+
limit: input.maxResultCharacters,
170+
}
171+
}
172+
if (
173+
input.maxResultBytes !== undefined &&
174+
resultCharacters !== undefined &&
175+
isGuaranteedWithinUtf8Limit(resultCharacters, input.maxResultBytes)
176+
) {
177+
return { accepted: true, resultCharacters }
178+
}
179+
142180
const pastedByteLimit = Math.max(input.maxPastedBytes ?? 0, input.maxResultBytes ?? 0)
143181
const pastedBytes = pastedByteLimit > 0 ? utf8ByteLength(pastedText, pastedByteLimit) : undefined
144182

@@ -155,20 +193,11 @@ export function assessTextPaste(input: TextPasteAdmissionInput): TextPasteAdmiss
155193
}
156194
}
157195

158-
if (input.maxResultBytes === undefined && input.maxResultCharacters === undefined) {
196+
if (!projectsResult) {
159197
return { accepted: true, pastedBytes }
160198
}
161199

162-
const { currentText, start, end } = normalizedSelection(input)
163-
const resultCharacters = currentText.length - (end - start) + pastedText.length
164-
if (input.maxResultCharacters !== undefined && resultCharacters > input.maxResultCharacters) {
165-
return {
166-
accepted: false,
167-
reason: 'result-characters',
168-
actual: resultCharacters,
169-
limit: input.maxResultCharacters,
170-
}
171-
}
200+
const { currentText, start, end } = selection as ReturnType<typeof normalizedSelection>
172201

173202
if (input.maxResultBytes === undefined) {
174203
return { accepted: true, pastedBytes, resultCharacters }

0 commit comments

Comments
 (0)