(pastedText: string)
| 1248 | } |
| 1249 | |
| 1250 | private handlePaste(pastedText: string): void { |
| 1251 | this.cancelAutocomplete(); |
| 1252 | this.exitHistoryBrowsing(); |
| 1253 | this.lastAction = null; |
| 1254 | |
| 1255 | this.pushUndoSnapshot(); |
| 1256 | |
| 1257 | // Some terminals (e.g. tmux popups with extended-keys-format=csi-u) re-encode |
| 1258 | // control bytes inside bracketed paste as CSI-u Ctrl+<letter> sequences |
| 1259 | // (ESC [ <codepoint> ; 5 u). Decode those back to their literal byte so the |
| 1260 | // per-char filter below preserves newlines instead of stripping ESC and |
| 1261 | // leaking the printable tail (e.g. "[106;5u") into the editor. |
| 1262 | const decodedText = pastedText.replace(/\x1b\[(\d+);5u/g, (match, code) => { |
| 1263 | const cp = Number(code); |
| 1264 | if (cp >= 97 && cp <= 122) return String.fromCharCode(cp - 96); |
| 1265 | if (cp >= 65 && cp <= 90) return String.fromCharCode(cp - 64); |
| 1266 | return match; |
| 1267 | }); |
| 1268 | |
| 1269 | // Clean the pasted text: normalize line endings, expand tabs |
| 1270 | const cleanText = this.normalizeText(decodedText); |
| 1271 | |
| 1272 | // Filter out non-printable characters except newlines |
| 1273 | let filteredText = cleanText |
| 1274 | .split("") |
| 1275 | .filter((char) => char === "\n" || char.charCodeAt(0) >= 32) |
| 1276 | .join(""); |
| 1277 | |
| 1278 | // If pasting a file path (starts with /, ~, or .) and the character before |
| 1279 | // the cursor is a word character, prepend a space for better readability |
| 1280 | if (/^[/~.]/.test(filteredText)) { |
| 1281 | const currentLine = this.state.lines[this.state.cursorLine] || ""; |
| 1282 | const charBeforeCursor = this.state.cursorCol > 0 ? currentLine[this.state.cursorCol - 1] : ""; |
| 1283 | if (charBeforeCursor && /\w/.test(charBeforeCursor)) { |
| 1284 | filteredText = ` ${filteredText}`; |
| 1285 | } |
| 1286 | } |
| 1287 | |
| 1288 | // Split into lines to check for large paste |
| 1289 | const pastedLines = filteredText.split("\n"); |
| 1290 | |
| 1291 | // Check if this is a large paste (> 10 lines or > 1000 characters) |
| 1292 | const totalChars = filteredText.length; |
| 1293 | if (pastedLines.length > 10 || totalChars > 1000) { |
| 1294 | // Store the paste and insert a marker |
| 1295 | this.pasteCounter++; |
| 1296 | const pasteId = this.pasteCounter; |
| 1297 | this.pastes.set(pasteId, filteredText); |
| 1298 | |
| 1299 | // Insert marker like "[paste #1 +123 lines]" or "[paste #1 1234 chars]" |
| 1300 | const marker = |
| 1301 | pastedLines.length > 10 |
| 1302 | ? `[paste #${pasteId} +${pastedLines.length} lines]` |
| 1303 | : `[paste #${pasteId} ${totalChars} chars]`; |
| 1304 | this.insertTextAtCursorInternal(marker); |
| 1305 | return; |
| 1306 | } |
| 1307 |
no test coverage detected