import { AuthOrchestrationOperateScope, EDITORS, EditorId, EnvironmentAuthorizationError, 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/reactivity"; import { getLocalStorageItem, setLocalStorageItem, useLocalStorage } from "./hooks/useLocalStorage"; import { shellEnvironment } from "./state/shell"; import { useAtomCommand } from "./state/use-atom-command"; import { readEnvironmentScope } from "./state/session"; import { openDeviceLocalEditor, useLocalEditors, useRemoteOpenState } from "./remoteOpen"; 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 = lastEditor && availableEditors.includes(lastEditor) ? lastEditor : (EDITORS.find((editor) => availableEditors.includes(editor.id))?.id ?? null); return [effectiveEditor, setLastEditor] as const; } 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 remote = useRemoteOpenState(environmentId); const clientEditors = useLocalEditors(); const openInEditor = useAtomCommand(shellEnvironment.openInEditor, { reportFailure: false, }); type OpenInEditorError = AtomCommandFailure>>; return async ( targetPath: string, ): Promise< AtomCommandResult< EditorId, | OpenInEditorError | PreferredEditorEnvironmentRequiredError | PreferredEditorUnavailableError | EnvironmentAuthorizationError > > => { if (environmentId === null) { return AsyncResult.failure( Cause.fail( new PreferredEditorEnvironmentRequiredError({ targetPath, }), ), ); } if (remote.mode === "device-local") { const editor = resolveAndPersistPreferredEditor(clientEditors); if (editor && (await openDeviceLocalEditor(editor, targetPath))) { return AsyncResult.success(editor); } return AsyncResult.failure( Cause.fail( new PreferredEditorUnavailableError({ environmentId, targetPath, availableEditorIds: clientEditors, }), ), ); } if (!readEnvironmentScope(environmentId, AuthOrchestrationOperateScope)) { return AsyncResult.failure( Cause.fail( new EnvironmentAuthorizationError({ requiredScope: AuthOrchestrationOperateScope, message: "This connection cannot open an editor on this environment.", }), ), ); } 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); }; }