source dump of claude code
at main 188 lines 5.7 kB view raw
1import { 2 expandPastedTextRefs, 3 formatPastedTextRef, 4 getPastedTextRefNumLines, 5} from '../history.js' 6import instances from '../ink/instances.js' 7import type { PastedContent } from './config.js' 8import { classifyGuiEditor, getExternalEditor } from './editor.js' 9import { execSync_DEPRECATED } from './execSyncWrapper.js' 10import { getFsImplementation } from './fsOperations.js' 11import { toIDEDisplayName } from './ide.js' 12import { writeFileSync_DEPRECATED } from './slowOperations.js' 13import { generateTempFilePath } from './tempfile.js' 14 15// Map of editor command overrides (e.g., to add wait flags) 16const EDITOR_OVERRIDES: Record<string, string> = { 17 code: 'code -w', // VS Code: wait for file to be closed 18 subl: 'subl --wait', // Sublime Text: wait for file to be closed 19} 20 21function isGuiEditor(editor: string): boolean { 22 return classifyGuiEditor(editor) !== undefined 23} 24 25export type EditorResult = { 26 content: string | null 27 error?: string 28} 29 30// sync IO: called from sync context (React components, sync command handlers) 31export function editFileInEditor(filePath: string): EditorResult { 32 const fs = getFsImplementation() 33 const inkInstance = instances.get(process.stdout) 34 if (!inkInstance) { 35 throw new Error('Ink instance not found - cannot pause rendering') 36 } 37 38 const editor = getExternalEditor() 39 if (!editor) { 40 return { content: null } 41 } 42 43 try { 44 fs.statSync(filePath) 45 } catch { 46 return { content: null } 47 } 48 49 const useAlternateScreen = !isGuiEditor(editor) 50 51 if (useAlternateScreen) { 52 // Terminal editors (vi, nano, etc.) take over the terminal. Delegate to 53 // Ink's alt-screen-aware handoff so fullscreen mode (where <AlternateScreen> 54 // already entered alt screen) doesn't get knocked back to the main buffer 55 // by a hardcoded ?1049l. enterAlternateScreen() internally calls pause() 56 // and suspendStdin(); exitAlternateScreen() undoes both and resets frame 57 // state so the next render writes from scratch. 58 inkInstance.enterAlternateScreen() 59 } else { 60 // GUI editors (code, subl, etc.) open in a separate window — just pause 61 // Ink and release stdin while they're open. 62 inkInstance.pause() 63 inkInstance.suspendStdin() 64 } 65 66 try { 67 // Use override command if available, otherwise use the editor as-is 68 const editorCommand = EDITOR_OVERRIDES[editor] ?? editor 69 execSync_DEPRECATED(`${editorCommand} "${filePath}"`, { 70 stdio: 'inherit', 71 }) 72 73 // Read the edited content 74 const editedContent = fs.readFileSync(filePath, { encoding: 'utf-8' }) 75 return { content: editedContent } 76 } catch (err) { 77 if ( 78 typeof err === 'object' && 79 err !== null && 80 'status' in err && 81 typeof (err as { status: unknown }).status === 'number' 82 ) { 83 const status = (err as { status: number }).status 84 if (status !== 0) { 85 const editorName = toIDEDisplayName(editor) 86 return { 87 content: null, 88 error: `${editorName} exited with code ${status}`, 89 } 90 } 91 } 92 return { content: null } 93 } finally { 94 if (useAlternateScreen) { 95 inkInstance.exitAlternateScreen() 96 } else { 97 inkInstance.resumeStdin() 98 inkInstance.resume() 99 } 100 } 101} 102 103/** 104 * Re-collapse expanded pasted text by finding content that matches 105 * pastedContents and replacing it with references. 106 */ 107function recollapsePastedContent( 108 editedPrompt: string, 109 originalPrompt: string, 110 pastedContents: Record<number, PastedContent>, 111): string { 112 let collapsed = editedPrompt 113 114 // Find pasted content in the edited text and re-collapse it 115 for (const [id, content] of Object.entries(pastedContents)) { 116 if (content.type === 'text') { 117 const pasteId = parseInt(id) 118 const contentStr = content.content 119 120 // Check if this exact content exists in the edited prompt 121 const contentIndex = collapsed.indexOf(contentStr) 122 if (contentIndex !== -1) { 123 // Replace with reference 124 const numLines = getPastedTextRefNumLines(contentStr) 125 const ref = formatPastedTextRef(pasteId, numLines) 126 collapsed = 127 collapsed.slice(0, contentIndex) + 128 ref + 129 collapsed.slice(contentIndex + contentStr.length) 130 } 131 } 132 } 133 134 return collapsed 135} 136 137// sync IO: called from sync context (React components, sync command handlers) 138export function editPromptInEditor( 139 currentPrompt: string, 140 pastedContents?: Record<number, PastedContent>, 141): EditorResult { 142 const fs = getFsImplementation() 143 const tempFile = generateTempFilePath() 144 145 try { 146 // Expand any pasted text references before editing 147 const expandedPrompt = pastedContents 148 ? expandPastedTextRefs(currentPrompt, pastedContents) 149 : currentPrompt 150 151 // Write expanded prompt to temp file 152 writeFileSync_DEPRECATED(tempFile, expandedPrompt, { 153 encoding: 'utf-8', 154 flush: true, 155 }) 156 157 // Delegate to editFileInEditor 158 const result = editFileInEditor(tempFile) 159 160 if (result.content === null) { 161 return result 162 } 163 164 // Trim a single trailing newline if present (common editor behavior) 165 let finalContent = result.content 166 if (finalContent.endsWith('\n') && !finalContent.endsWith('\n\n')) { 167 finalContent = finalContent.slice(0, -1) 168 } 169 170 // Re-collapse pasted content if it wasn't edited 171 if (pastedContents) { 172 finalContent = recollapsePastedContent( 173 finalContent, 174 currentPrompt, 175 pastedContents, 176 ) 177 } 178 179 return { content: finalContent } 180 } finally { 181 // Clean up temp file 182 try { 183 fs.unlinkSync(tempFile) 184 } catch { 185 // Ignore cleanup errors 186 } 187 } 188}