| 1 | import { |
| 2 | AuthOrchestrationOperateScope, |
| 3 | EDITORS, |
| 4 | EditorId, |
| 5 | EnvironmentAuthorizationError, |
| 6 | EnvironmentId, |
| 7 | } from "@t3tools/contracts"; |
| 8 | import { |
| 9 | mapAtomCommandResult, |
| 10 | type AtomCommandFailure, |
| 11 | type AtomCommandResult, |
| 12 | } from "@t3tools/client-runtime/state/runtime"; |
| 13 | import * as Cause from "effect/Cause"; |
| 14 | import * as Schema from "effect/Schema"; |
| 15 | import { AsyncResult } from "effect/reactivity"; |
| 16 | import { getLocalStorageItem, setLocalStorageItem, useLocalStorage } from "./hooks/useLocalStorage"; |
| 17 | import { useCallback, useMemo } from "react"; |
| 18 | import { shellEnvironment } from "./state/shell"; |
| 19 | import { useAtomCommand } from "./state/use-atom-command"; |
| 20 | import { readEnvironmentScope } from "./state/session"; |
| 21 | |
| 22 | const LAST_EDITOR_KEY = "t3code:last-editor"; |
| 23 | |
| 24 | export class PreferredEditorEnvironmentRequiredError extends Schema.TaggedError<PreferredEditorEnvironmentRequiredError>()( |
| 25 | "PreferredEditorEnvironmentRequiredError", |
| 26 | { |
| 27 | targetPath: Schema.String, |
| 28 | }, |
| 29 | ) { |
| 30 | override get message(): string { |
| 31 | return `Cannot open ${this.targetPath} because no environment is selected.`; |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | export class PreferredEditorUnavailableError extends Schema.TaggedError<PreferredEditorUnavailableError>()( |
| 36 | "PreferredEditorUnavailableError", |
| 37 | { |
| 38 | environmentId: EnvironmentId, |
| 39 | targetPath: Schema.String, |
| 40 | availableEditorIds: Schema.Array(EditorId), |
| 41 | }, |
| 42 | ) { |
| 43 | override get message(): string { |
| 44 | return `No available editor can open ${this.targetPath} in environment ${this.environmentId}.`; |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | export function usePreferredEditor(availableEditors: ReadonlyArray<EditorId>) { |
| 49 | const [lastEditor, setLastEditor] = useLocalStorage(LAST_EDITOR_KEY, null, EditorId); |
| 50 | |
| 51 | const effectiveEditor = useMemo(() => { |
| 52 | if (lastEditor && availableEditors.includes(lastEditor)) return lastEditor; |
| 53 | return EDITORS.find((editor) => availableEditors.includes(editor.id))?.id ?? null; |
| 54 | }, [lastEditor, availableEditors]); |
| 55 | |
| 56 | return [effectiveEditor, setLastEditor] as const; |
| 57 | } |
| 58 | |
| 59 | function resolveAndPersistPreferredEditor(availableEditors: readonly EditorId[]): EditorId | null { |
| 60 | const availableEditorIds = new Set(availableEditors); |
| 61 | const stored = getLocalStorageItem(LAST_EDITOR_KEY, EditorId); |
| 62 | if (stored && availableEditorIds.has(stored)) return stored; |
| 63 | const editor = EDITORS.find((editor) => availableEditorIds.has(editor.id))?.id ?? null; |
| 64 | if (editor) setLocalStorageItem(LAST_EDITOR_KEY, editor, EditorId); |
| 65 | return editor ?? null; |
| 66 | } |
| 67 | |
| 68 | export function useOpenInPreferredEditor( |
| 69 | environmentId: EnvironmentId | null, |
| 70 | availableEditors: readonly EditorId[], |
| 71 | ) { |
| 72 | const openInEditor = useAtomCommand(shellEnvironment.openInEditor, { |
| 73 | reportFailure: false, |
| 74 | }); |
| 75 | type OpenInEditorError = AtomCommandFailure<Awaited<ReturnType<typeof openInEditor>>>; |
| 76 | |
| 77 | return useCallback( |
| 78 | async ( |
| 79 | targetPath: string, |
| 80 | ): Promise< |
| 81 | AtomCommandResult< |
| 82 | EditorId, |
| 83 | | OpenInEditorError |
| 84 | | PreferredEditorEnvironmentRequiredError |
| 85 | | PreferredEditorUnavailableError |
| 86 | | EnvironmentAuthorizationError |
| 87 | > |
| 88 | > => { |
| 89 | if (environmentId === null) { |
| 90 | return AsyncResult.failure( |
| 91 | Cause.fail( |
| 92 | new PreferredEditorEnvironmentRequiredError({ |
| 93 | targetPath, |
| 94 | }), |
| 95 | ), |
| 96 | ); |
| 97 | } |
| 98 | if (!readEnvironmentScope(environmentId, AuthOrchestrationOperateScope)) { |
| 99 | return AsyncResult.failure( |
| 100 | Cause.fail( |
| 101 | new EnvironmentAuthorizationError({ |
| 102 | requiredScope: AuthOrchestrationOperateScope, |
| 103 | message: "This connection cannot open an editor on this environment.", |
| 104 | }), |
| 105 | ), |
| 106 | ); |
| 107 | } |
| 108 | const editor = resolveAndPersistPreferredEditor(availableEditors); |
| 109 | if (!editor) { |
| 110 | return AsyncResult.failure( |
| 111 | Cause.fail( |
| 112 | new PreferredEditorUnavailableError({ |
| 113 | environmentId, |
| 114 | targetPath, |
| 115 | availableEditorIds: availableEditors, |
| 116 | }), |
| 117 | ), |
| 118 | ); |
| 119 | } |
| 120 | const result = await openInEditor({ |
| 121 | environmentId, |
| 122 | input: { |
| 123 | cwd: targetPath, |
| 124 | editor, |
| 125 | }, |
| 126 | }); |
| 127 | return mapAtomCommandResult(result, () => editor); |
| 128 | }, |
| 129 | [availableEditors, environmentId, openInEditor], |
| 130 | ); |
| 131 | } |