fix: prefer vim over nano for $EDITOR fallback (CLI + TUI)

prompt_toolkit's default editor list is: $VISUAL, $EDITOR, /usr/bin/editor,
/usr/bin/nano, /usr/bin/pico, /usr/bin/vi, /usr/bin/emacs — so when
neither env var is set, the base CLI launched nano. The TUI fell back
to a literal 'vi'. Same Ctrl+G keystroke, two different editors.

Pick the same chain on both surfaces:
  $VISUAL → $EDITOR → vim → vi → nano

CLI: override input_area.buffer._open_file_in_editor on the TextArea
once at app build time. Local to that buffer; doesn't touch
os.environ or affect other subprocesses.

TUI: extract resolveEditor() into ui-tui/src/lib/editor.ts. PATH walk
with accessSync(X_OK), no shelling out. Six-line unit test verifies
the priority order and the multi-entry PATH walk.
This commit is contained in:
Brooklyn Nicholson 2026-04-25 20:11:25 -05:00
parent 5fac6c3440
commit db7c5735f0
4 changed files with 128 additions and 1 deletions

View file

@ -0,0 +1,66 @@
import { chmodSync, mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { delimiter, join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { resolveEditor } from './editor.js'
describe('resolveEditor', () => {
let dir: string
const exe = (name: string) => {
const path = join(dir, name)
writeFileSync(path, '#!/bin/sh\nexit 0\n')
chmodSync(path, 0o755)
return path
}
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'editor-test-'))
})
afterEach(() => {
// tmp dir is small; let the OS reap it
})
it('honors $VISUAL above all else', () => {
expect(resolveEditor({ EDITOR: 'vim', PATH: dir, VISUAL: 'helix' })).toBe('helix')
})
it('falls back to $EDITOR when $VISUAL is unset', () => {
expect(resolveEditor({ EDITOR: 'nvim', PATH: dir })).toBe('nvim')
})
it('prefers vim over vi over nano on $PATH', () => {
exe('nano')
exe('vi')
const vim = exe('vim')
expect(resolveEditor({ PATH: dir })).toBe(vim)
})
it('falls back to vi when only vi and nano exist', () => {
exe('nano')
const vi = exe('vi')
expect(resolveEditor({ PATH: dir })).toBe(vi)
})
it('returns literal "vi" when nothing on PATH and no env', () => {
mkdirSync(join(dir, 'empty'), { recursive: true })
expect(resolveEditor({ PATH: join(dir, 'empty') })).toBe('vi')
})
it('walks multi-entry PATH', () => {
const a = mkdtempSync(join(tmpdir(), 'editor-a-'))
const b = mkdtempSync(join(tmpdir(), 'editor-b-'))
writeFileSync(join(b, 'vim'), '#!/bin/sh\n')
chmodSync(join(b, 'vim'), 0o755)
expect(resolveEditor({ PATH: [a, b].join(delimiter) })).toBe(join(b, 'vim'))
})
})