Skip to content

Commit 71447c4

Browse files
committed
fix(ui): match distributed multi-cursor paste
1 parent 0dd5850 commit 71447c4

3 files changed

Lines changed: 83 additions & 3 deletions

File tree

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor-paste.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,37 @@ describe('assessTextEditorPaste', () => {
4343
)
4444
).toMatchObject({ accepted: false, reason: 'result-bytes', limit: 10 })
4545
})
46+
47+
it('projects one matching clipboard line per cursor in Monaco spread mode', () => {
48+
expect(
49+
assessTextEditorPaste(
50+
{
51+
pastedText: 'x\ny\n',
52+
currentText: '12345678',
53+
selections: [
54+
{ start: 2, end: 2 },
55+
{ start: 6, end: 6 },
56+
],
57+
},
58+
10
59+
)
60+
).toMatchObject({ accepted: true, resultBytes: 10 })
61+
})
62+
63+
it('projects the full clipboard at every cursor when Monaco spread mode is disabled', () => {
64+
expect(
65+
assessTextEditorPaste(
66+
{
67+
pastedText: 'x\ny',
68+
currentText: '123456',
69+
selections: [
70+
{ start: 2, end: 2 },
71+
{ start: 4, end: 4 },
72+
],
73+
multiCursorPaste: 'full',
74+
},
75+
10
76+
)
77+
).toMatchObject({ accepted: false, reason: 'result-bytes', limit: 10 })
78+
})
4679
})

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

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface TextEditorPasteInput {
1414
pastedText: string
1515
currentText: string
1616
selections: readonly TextEditorPasteSelection[]
17+
multiCursorPaste?: 'spread' | 'full'
1718
}
1819

1920
function normalizedSelections(
@@ -45,19 +46,58 @@ function mergedReplacementRanges(
4546
return ranges
4647
}
4748

49+
function distributedPasteRanges(
50+
text: string,
51+
selectionCount: number
52+
): TextEditorPasteSelection[] | null {
53+
if (selectionCount <= 1) return null
54+
55+
let textEnd = text.length
56+
if (text.charCodeAt(textEnd - 1) === 10) textEnd -= 1
57+
if (text.charCodeAt(textEnd - 1) === 13) textEnd -= 1
58+
59+
const ranges: TextEditorPasteSelection[] = []
60+
let lineStart = 0
61+
for (let index = 0; index < textEnd; index++) {
62+
const code = text.charCodeAt(index)
63+
if (code !== 10 && code !== 13) continue
64+
ranges.push({ start: lineStart, end: index })
65+
if (ranges.length >= selectionCount) return null
66+
if (code === 13 && text.charCodeAt(index + 1) === 10) index += 1
67+
lineStart = index + 1
68+
}
69+
ranges.push({ start: lineStart, end: textEnd })
70+
return ranges.length === selectionCount ? ranges : null
71+
}
72+
4873
/** Applies the workspace-file content contract to every selection in a projected Monaco paste. */
4974
export function assessTextEditorPaste(
5075
input: TextEditorPasteInput,
5176
maxBytes = PASTE_LIMITS.TEXT_EDITOR_BYTES
5277
): TextPasteAdmission {
5378
const selections = normalizedSelections(input.selections, input.currentText.length)
5479
const replacementRanges = mergedReplacementRanges(selections)
80+
const distributedRanges =
81+
(input.multiCursorPaste ?? 'spread') === 'spread'
82+
? distributedPasteRanges(input.pastedText, selections.length)
83+
: null
5584
const replacedCharacters = replacementRanges.reduce(
5685
(total, selection) => total + selection.end - selection.start,
5786
0
5887
)
59-
const resultCharacters =
60-
input.currentText.length - replacedCharacters + input.pastedText.length * selections.length
88+
const insertedCharacters = distributedRanges
89+
? distributedRanges.reduce((total, range) => total + range.end - range.start, 0)
90+
: input.pastedText.length * selections.length
91+
const resultCharacters = input.currentText.length - replacedCharacters + insertedCharacters
92+
93+
if (input.pastedText.length > maxBytes) {
94+
return {
95+
accepted: false,
96+
reason: 'pasted-bytes',
97+
actual: input.pastedText.length,
98+
limit: maxBytes,
99+
}
100+
}
61101

62102
if (resultCharacters <= Math.floor(maxBytes / 3)) {
63103
return { accepted: true, resultCharacters }
@@ -68,7 +108,13 @@ export function assessTextEditorPaste(
68108
return { accepted: false, reason: 'pasted-bytes', actual: pastedBytes, limit: maxBytes }
69109
}
70110

71-
const insertedBytes = pastedBytes * selections.length
111+
const insertedBytes = distributedRanges
112+
? distributedRanges.reduce(
113+
(total, range) =>
114+
total + utf8ByteLengthRange(input.pastedText, range.start, range.end, maxBytes - total),
115+
0
116+
)
117+
: pastedBytes * selections.length
72118
if (insertedBytes > maxBytes) {
73119
return { accepted: false, reason: 'result-bytes', actual: insertedBytes, limit: maxBytes }
74120
}

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,7 @@ export const TextEditor = memo(function TextEditor({
620620
pastedText,
621621
currentText,
622622
selections: selectionOffsets,
623+
multiCursorPaste: editor?.getRawOptions().multiCursorPaste ?? 'spread',
623624
})
624625
if (admission.accepted) return
625626

0 commit comments

Comments
 (0)