6bb10ca40e
- Added external editor callback support to extension UI dialogs and hook selector for opening plans in external editor. - Added `matchesAppExternalEditor()` keybinding matcher to detect external editor shortcuts (ctrl+g) across components. - Added `trimTrailingNewline` option to `openInEditor()` function to preserve trailing newlines when editing externally. - Modified plan review flow to re-read plan content from disk before approval to reflect external editor changes. - Refactored plan preview rendering to cache and reuse UI container instead of recreating children on each render. - Refactored Promise creation in external editor utility to use `Promise.withResolvers()` for cleaner async handling.
66 lines
2.1 KiB
TypeScript
66 lines
2.1 KiB
TypeScript
/**
|
|
* Utilities for launching an external text editor ($VISUAL / $EDITOR).
|
|
*/
|
|
import { spawn } from "node:child_process";
|
|
import * as fs from "node:fs/promises";
|
|
import * as os from "node:os";
|
|
import * as path from "node:path";
|
|
import { $env, Snowflake } from "@oh-my-pi/pi-utils";
|
|
|
|
/** Returns the user's preferred editor command, or undefined if not configured. */
|
|
export function getEditorCommand(): string | undefined {
|
|
return $env.VISUAL || $env.EDITOR || undefined;
|
|
}
|
|
|
|
export interface OpenInEditorOptions {
|
|
/** File extension for the temp file (default: ".md"). */
|
|
extension?: string;
|
|
/** Custom stdio configuration (default: all "inherit"). */
|
|
stdio?: [number | "inherit", number | "inherit", number | "inherit"];
|
|
/** Keep the file's trailing newline instead of trimming it from the returned text. */
|
|
trimTrailingNewline?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Opens `content` in the user's external editor and returns the edited text.
|
|
* Returns `null` if the editor exits with a non-zero code.
|
|
*
|
|
* The caller is responsible for stopping/starting the TUI around this call.
|
|
*/
|
|
export async function openInEditor(
|
|
editorCmd: string,
|
|
content: string,
|
|
options?: OpenInEditorOptions,
|
|
): Promise<string | null> {
|
|
const ext = options?.extension ?? ".md";
|
|
const tmpFile = path.join(os.tmpdir(), `omp-editor-${Snowflake.next()}${ext}`);
|
|
|
|
try {
|
|
await Bun.write(tmpFile, content);
|
|
|
|
const [editor, ...editorArgs] = editorCmd.split(" ");
|
|
const stdio = options?.stdio ?? ["inherit", "inherit", "inherit"];
|
|
|
|
const child = spawn(editor, [...editorArgs, tmpFile], { stdio, shell: process.platform === "win32" });
|
|
const { promise, reject, resolve } = Promise.withResolvers<number>();
|
|
child.once("exit", (code, signal) => resolve(code ?? (signal ? -1 : 0)));
|
|
child.once("error", error => reject(error));
|
|
const exitCode = await promise;
|
|
|
|
if (exitCode === 0) {
|
|
const text = await Bun.file(tmpFile).text();
|
|
if (options?.trimTrailingNewline === false) {
|
|
return text;
|
|
}
|
|
return text.replace(/\n$/, "");
|
|
}
|
|
return null;
|
|
} finally {
|
|
try {
|
|
await fs.rm(tmpFile, { force: true });
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
}
|
|
}
|