import { EDITORS, EditorId, EnvironmentId } from "@t3tools/contracts"; import { mapAtomCommandResult, type AtomCommandFailure, type AtomCommandResult, } from "@t3tools/client-runtime/state/runtime"; import * as Cause from "effect/Cause"; import * as Schema from "effect/Schema"; import { AsyncResult } from "effect/unstable/reactivity"; import { getLocalStorageItem, setLocalStorageItem, useLocalStorage } from "./hooks/useLocalStorage"; import { useCallback, useMemo } from "react"; import { shellEnvironment } from "./state/shell"; import { useAtomCommand } from "./state/use-atom-command"; const LAST_EDITOR_KEY = "t3code:last-editor"; export class PreferredEditorEnvironmentRequiredError extends Schema.TaggedError()( "PreferredEditorEnvironmentRequiredError", { targetPath: Schema.String, }, ) { override get message(): string { return `Cannot open ${this.targetPath} because no environment is selected.`; } } export class PreferredEditorUnavailableError extends Schema.TaggedError()( "PreferredEditorUnavailableError", { environmentId: EnvironmentId, targetPath: Schema.String, availableEditorIds: Schema.Array(EditorId), }, ) { override get message(): string { return `No available editor can open ${this.targetPath} in environment ${this.environmentId}.`; } } export function usePreferredEditor(availableEditors: ReadonlyArray) { const [lastEditor, setLastEditor] = useLocalStorage(LAST_EDITOR_KEY, null, EditorId); const effectiveEditor = useMemo(() => { if (lastEditor && availableEditors.includes(lastEditor)) return lastEditor; return EDITORS.find((editor) => availableEditors.includes(editor.id))?.id ?? null; }, [lastEditor, availableEditors]); return [effectiveEditor, setLastEditor] as const; } export function resolveAndPersistPreferredEditor( availableEditors: readonly EditorId[], ): EditorId | null { const availableEditorIds = new Set(availableEditors); const stored = getLocalStorageItem(LAST_EDITOR_KEY, EditorId); if (stored && availableEditorIds.has(stored)) return stored; const editor = EDITORS.find((editor) => availableEditorIds.has(editor.id))?.id ?? null; if (editor) setLocalStorageItem(LAST_EDITOR_KEY, editor, EditorId); return editor ?? null; } export function useOpenInPreferredEditor( environmentId: EnvironmentId | null, availableEditors: readonly EditorId[], ) { const openInEditor = useAtomCommand(shellEnvironment.openInEditor, { reportFailure: false, }); type OpenInEditorError = AtomCommandFailure>>; return useCallback( async ( targetPath: string, ): Promise< AtomCommandResult< EditorId, | OpenInEditorError | PreferredEditorEnvironmentRequiredError | PreferredEditorUnavailableError > > => { if (environmentId === null) { return AsyncResult.failure( Cause.fail( new PreferredEditorEnvironmentRequiredError({ targetPath, }), ), ); } const editor = resolveAndPersistPreferredEditor(availableEditors); if (!editor) { return AsyncResult.failure( Cause.fail( new PreferredEditorUnavailableError({ environmentId, targetPath, availableEditorIds: availableEditors, }), ), ); } const result = await openInEditor({ environmentId, input: { cwd: targetPath, editor, }, }); return mapAtomCommandResult(result, () => editor); }, [availableEditors, environmentId, openInEditor], ); }