promptEditor.ts
utils/promptEditor.ts
No strong subsystem tag
189
Lines
5664
Bytes
3
Exports
9
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with general runtime concerns. It contains 189 lines, 9 detected imports, and 3 detected exports.
Important relationships
Detected exports
EditorResulteditFileInEditoreditPromptInEditor
Keywords
contenteditorinkinstancestatuspastedcontentsfinalcontentsynccollapsedtempfilecode
Detected imports
../history.js../ink/instances.js./config.js./editor.js./execSyncWrapper.js./fsOperations.js./ide.js./slowOperations.js./tempfile.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
import {
expandPastedTextRefs,
formatPastedTextRef,
getPastedTextRefNumLines,
} from '../history.js'
import instances from '../ink/instances.js'
import type { PastedContent } from './config.js'
import { classifyGuiEditor, getExternalEditor } from './editor.js'
import { execSync_DEPRECATED } from './execSyncWrapper.js'
import { getFsImplementation } from './fsOperations.js'
import { toIDEDisplayName } from './ide.js'
import { writeFileSync_DEPRECATED } from './slowOperations.js'
import { generateTempFilePath } from './tempfile.js'
// Map of editor command overrides (e.g., to add wait flags)
const EDITOR_OVERRIDES: Record<string, string> = {
code: 'code -w', // VS Code: wait for file to be closed
subl: 'subl --wait', // Sublime Text: wait for file to be closed
}
function isGuiEditor(editor: string): boolean {
return classifyGuiEditor(editor) !== undefined
}
export type EditorResult = {
content: string | null
error?: string
}
// sync IO: called from sync context (React components, sync command handlers)
export function editFileInEditor(filePath: string): EditorResult {
const fs = getFsImplementation()
const inkInstance = instances.get(process.stdout)
if (!inkInstance) {
throw new Error('Ink instance not found - cannot pause rendering')
}
const editor = getExternalEditor()
if (!editor) {
return { content: null }
}
try {
fs.statSync(filePath)
} catch {
return { content: null }
}
const useAlternateScreen = !isGuiEditor(editor)
if (useAlternateScreen) {
// Terminal editors (vi, nano, etc.) take over the terminal. Delegate to
// Ink's alt-screen-aware handoff so fullscreen mode (where <AlternateScreen>
// already entered alt screen) doesn't get knocked back to the main buffer
// by a hardcoded ?1049l. enterAlternateScreen() internally calls pause()
// and suspendStdin(); exitAlternateScreen() undoes both and resets frame
// state so the next render writes from scratch.
inkInstance.enterAlternateScreen()
} else {
// GUI editors (code, subl, etc.) open in a separate window — just pause
// Ink and release stdin while they're open.
inkInstance.pause()
inkInstance.suspendStdin()
}
try {
// Use override command if available, otherwise use the editor as-is
const editorCommand = EDITOR_OVERRIDES[editor] ?? editor
execSync_DEPRECATED(`${editorCommand} "${filePath}"`, {
stdio: 'inherit',
})
// Read the edited content
const editedContent = fs.readFileSync(filePath, { encoding: 'utf-8' })
return { content: editedContent }
} catch (err) {
if (
typeof err === 'object' &&
err !== null &&
'status' in err &&
typeof (err as { status: unknown }).status === 'number'
) {
const status = (err as { status: number }).status
if (status !== 0) {
const editorName = toIDEDisplayName(editor)
return {
content: null,
error: `${editorName} exited with code ${status}`,
}
}
}
return { content: null }
} finally {
if (useAlternateScreen) {
inkInstance.exitAlternateScreen()
} else {
inkInstance.resumeStdin()
inkInstance.resume()
}
}
}
/**
* Re-collapse expanded pasted text by finding content that matches
* pastedContents and replacing it with references.
*/
function recollapsePastedContent(
editedPrompt: string,
originalPrompt: string,
pastedContents: Record<number, PastedContent>,
): string {
let collapsed = editedPrompt
// Find pasted content in the edited text and re-collapse it
for (const [id, content] of Object.entries(pastedContents)) {
if (content.type === 'text') {
const pasteId = parseInt(id)
const contentStr = content.content
// Check if this exact content exists in the edited prompt
const contentIndex = collapsed.indexOf(contentStr)
if (contentIndex !== -1) {
// Replace with reference
const numLines = getPastedTextRefNumLines(contentStr)
const ref = formatPastedTextRef(pasteId, numLines)
collapsed =
collapsed.slice(0, contentIndex) +
ref +
collapsed.slice(contentIndex + contentStr.length)
}
}
}
return collapsed
}
// sync IO: called from sync context (React components, sync command handlers)
export function editPromptInEditor(
currentPrompt: string,
pastedContents?: Record<number, PastedContent>,
): EditorResult {
const fs = getFsImplementation()
const tempFile = generateTempFilePath()
try {
// Expand any pasted text references before editing
const expandedPrompt = pastedContents
? expandPastedTextRefs(currentPrompt, pastedContents)
: currentPrompt
// Write expanded prompt to temp file
writeFileSync_DEPRECATED(tempFile, expandedPrompt, {
encoding: 'utf-8',
flush: true,
})
// Delegate to editFileInEditor
const result = editFileInEditor(tempFile)
if (result.content === null) {
return result
}
// Trim a single trailing newline if present (common editor behavior)
let finalContent = result.content
if (finalContent.endsWith('\n') && !finalContent.endsWith('\n\n')) {
finalContent = finalContent.slice(0, -1)
}
// Re-collapse pasted content if it wasn't edited
if (pastedContents) {
finalContent = recollapsePastedContent(
finalContent,
currentPrompt,
pastedContents,
)
}
return { content: finalContent }
} finally {
// Clean up temp file
try {
fs.unlinkSync(tempFile)
} catch {
// Ignore cleanup errors
}
}
}