|
1 | | -import { assessTextPaste, PASTE_LIMITS, type TextPasteAdmission } from '@sim/utils/paste' |
| 1 | +import { |
| 2 | + PASTE_LIMITS, |
| 3 | + type TextPasteAdmission, |
| 4 | + utf8ByteLength, |
| 5 | + utf8ByteLengthRange, |
| 6 | +} from '@sim/utils/paste' |
| 7 | + |
| 8 | +interface TextEditorPasteSelection { |
| 9 | + start: number |
| 10 | + end: number |
| 11 | +} |
2 | 12 |
|
3 | 13 | interface TextEditorPasteInput { |
4 | 14 | pastedText: string |
5 | 15 | currentText: string |
6 | | - selectionStart: number |
7 | | - selectionEnd: number |
| 16 | + selections: readonly TextEditorPasteSelection[] |
| 17 | +} |
| 18 | + |
| 19 | +function normalizedSelections( |
| 20 | + selections: readonly TextEditorPasteSelection[], |
| 21 | + textLength: number |
| 22 | +): TextEditorPasteSelection[] { |
| 23 | + const source = selections.length > 0 ? selections : [{ start: textLength, end: textLength }] |
| 24 | + return source |
| 25 | + .map(({ start, end }) => ({ |
| 26 | + start: Math.min(Math.max(Math.min(start, end), 0), textLength), |
| 27 | + end: Math.min(Math.max(Math.max(start, end), 0), textLength), |
| 28 | + })) |
| 29 | + .sort((left, right) => left.start - right.start || left.end - right.end) |
| 30 | +} |
| 31 | + |
| 32 | +function mergedReplacementRanges( |
| 33 | + selections: readonly TextEditorPasteSelection[] |
| 34 | +): TextEditorPasteSelection[] { |
| 35 | + const ranges: TextEditorPasteSelection[] = [] |
| 36 | + for (const selection of selections) { |
| 37 | + if (selection.start === selection.end) continue |
| 38 | + const previous = ranges.at(-1) |
| 39 | + if (previous && selection.start <= previous.end) { |
| 40 | + previous.end = Math.max(previous.end, selection.end) |
| 41 | + } else { |
| 42 | + ranges.push({ ...selection }) |
| 43 | + } |
| 44 | + } |
| 45 | + return ranges |
8 | 46 | } |
9 | 47 |
|
10 | | -/** Applies the workspace-file content contract to a projected Monaco paste result. */ |
| 48 | +/** Applies the workspace-file content contract to every selection in a projected Monaco paste. */ |
11 | 49 | export function assessTextEditorPaste( |
12 | 50 | input: TextEditorPasteInput, |
13 | 51 | maxBytes = PASTE_LIMITS.TEXT_EDITOR_BYTES |
14 | 52 | ): TextPasteAdmission { |
15 | | - return assessTextPaste({ |
16 | | - ...input, |
17 | | - maxPastedBytes: maxBytes, |
18 | | - maxResultBytes: maxBytes, |
19 | | - }) |
| 53 | + const selections = normalizedSelections(input.selections, input.currentText.length) |
| 54 | + const replacementRanges = mergedReplacementRanges(selections) |
| 55 | + const replacedCharacters = replacementRanges.reduce( |
| 56 | + (total, selection) => total + selection.end - selection.start, |
| 57 | + 0 |
| 58 | + ) |
| 59 | + const resultCharacters = |
| 60 | + input.currentText.length - replacedCharacters + input.pastedText.length * selections.length |
| 61 | + |
| 62 | + if (resultCharacters <= Math.floor(maxBytes / 3)) { |
| 63 | + return { accepted: true, resultCharacters } |
| 64 | + } |
| 65 | + |
| 66 | + const pastedBytes = utf8ByteLength(input.pastedText, maxBytes) |
| 67 | + if (pastedBytes > maxBytes) { |
| 68 | + return { accepted: false, reason: 'pasted-bytes', actual: pastedBytes, limit: maxBytes } |
| 69 | + } |
| 70 | + |
| 71 | + const insertedBytes = pastedBytes * selections.length |
| 72 | + if (insertedBytes > maxBytes) { |
| 73 | + return { accepted: false, reason: 'result-bytes', actual: insertedBytes, limit: maxBytes } |
| 74 | + } |
| 75 | + |
| 76 | + let retainedBytes = 0 |
| 77 | + let retainedStart = 0 |
| 78 | + for (const selection of replacementRanges) { |
| 79 | + retainedBytes += utf8ByteLengthRange( |
| 80 | + input.currentText, |
| 81 | + retainedStart, |
| 82 | + selection.start, |
| 83 | + maxBytes - insertedBytes - retainedBytes |
| 84 | + ) |
| 85 | + if (retainedBytes + insertedBytes > maxBytes) { |
| 86 | + return { |
| 87 | + accepted: false, |
| 88 | + reason: 'result-bytes', |
| 89 | + actual: retainedBytes + insertedBytes, |
| 90 | + limit: maxBytes, |
| 91 | + } |
| 92 | + } |
| 93 | + retainedStart = selection.end |
| 94 | + } |
| 95 | + retainedBytes += utf8ByteLengthRange( |
| 96 | + input.currentText, |
| 97 | + retainedStart, |
| 98 | + input.currentText.length, |
| 99 | + maxBytes - insertedBytes - retainedBytes |
| 100 | + ) |
| 101 | + |
| 102 | + const resultBytes = retainedBytes + insertedBytes |
| 103 | + if (resultBytes > maxBytes) { |
| 104 | + return { accepted: false, reason: 'result-bytes', actual: resultBytes, limit: maxBytes } |
| 105 | + } |
| 106 | + |
| 107 | + return { accepted: true, pastedBytes, resultBytes, resultCharacters } |
20 | 108 | } |
0 commit comments