MCPcopy
hub / github.com/codeaashu/claude-code / openFileInExternalEditor

Function openFileInExternalEditor

src/utils/editor.ts:81–162  ·  view source on GitHub ↗
(
  filePath: string,
  line?: number,
)

Source from the content-addressed store, hash-verified

79 * Returns true if the editor was launched, false if no editor is available.
80 */
81export function openFileInExternalEditor(
82 filePath: string,
83 line?: number,
84): boolean {
85 const editor = getExternalEditor()
86 if (!editor) return false
87
88 // Spawn the user's actual binary (preserves code-insiders, abs paths, etc.).
89 // Split into binary + extra args so multi-word values like 'start /wait
90 // notepad' or 'code --wait' propagate all tokens to spawn.
91 const parts = editor.split(' ')
92 const base = parts[0] ?? editor
93 const editorArgs = parts.slice(1)
94 const guiFamily = classifyGuiEditor(editor)
95
96 if (guiFamily) {
97 const gotoArgv = guiGotoArgv(guiFamily, filePath, line)
98 const detachedOpts: SpawnOptions = { detached: true, stdio: 'ignore' }
99 let child
100 if (process.platform === 'win32') {
101 // shell: true on win32 so code.cmd / cursor.cmd / windsurf.cmd resolve —
102 // CreateProcess can't execute .cmd/.bat directly. Assemble quoted command
103 // string; cmd.exe doesn't expand $() or backticks inside double quotes.
104 // Quote each arg so paths with spaces survive the shell join.
105 const gotoStr = gotoArgv.map(a => `"${a}"`).join(' ')
106 child = spawn(`${editor} ${gotoStr}`, { ...detachedOpts, shell: true })
107 } else {
108 // POSIX: argv array with no shell — injection-safe. shell: true would
109 // expand $() / backticks inside double quotes, and filePath is
110 // filesystem-sourced (possible RCE from a malicious repo filename).
111 child = spawn(base, [...editorArgs, ...gotoArgv], detachedOpts)
112 }
113 // spawn() emits ENOENT asynchronously. ENOENT on $VISUAL/$EDITOR is a
114 // user-config error, not an internal bug — don't pollute error telemetry.
115 child.on('error', e =>
116 logForDebugging(`editor spawn failed: ${e}`, { level: 'error' }),
117 )
118 child.unref()
119 return true
120 }
121
122 // Terminal editor — needs alt-screen handoff since it takes over the
123 // terminal. Blocks until the editor exits.
124 const inkInstance = instances.get(process.stdout)
125 if (!inkInstance) return false
126 // Only prepend +N for editors known to support it — notepad treats +42 as a
127 // filename to open. Test basename so /home/vim/bin/kak doesn't match 'vim'
128 // via the directory segment.
129 const useGotoLine = line && PLUS_N_EDITORS.test(basename(base))
130 inkInstance.enterAlternateScreen()
131 try {
132 const syncOpts: SpawnSyncOptions = { stdio: 'inherit' }
133 let result
134 if (process.platform === 'win32') {
135 // On Windows use shell: true so cmd.exe builtins like `start` resolve.
136 // shell: true joins args unquoted, so assemble the command string with
137 // explicit quoting ourselves (matching promptEditor.ts:74). spawnSync
138 // returns errors in .error rather than throwing.

Callers 3

GlobalSearchDialogFunction · 0.85
QuickOpenDialogFunction · 0.85
REPLFunction · 0.85

Calls 8

classifyGuiEditorFunction · 0.85
guiGotoArgvFunction · 0.85
spawnFunction · 0.85
logForDebuggingFunction · 0.85
onMethod · 0.80
enterAlternateScreenMethod · 0.80
exitAlternateScreenMethod · 0.80
getMethod · 0.65

Tested by

no test coverage detected