1import {
2 AuthOrchestrationOperateScope,
3 EDITORS,
4 EditorId,
5 EnvironmentAuthorizationError,
6 EnvironmentId,
7} from "@t3tools/contracts";
8import {
9 mapAtomCommandResult,
10 type AtomCommandFailure,
11 type AtomCommandResult,
12} from "@t3tools/client-runtime/state/runtime";
13import * as Cause from "effect/Cause";
14import * as Schema from "effect/Schema";
15import { AsyncResult } from "effect/reactivity";
16import { getLocalStorageItem, setLocalStorageItem, useLocalStorage } from "./hooks/useLocalStorage";
17import { useCallback, useMemo } from "react";
18import { shellEnvironment } from "./state/shell";
19import { useAtomCommand } from "./state/use-atom-command";
20import { readEnvironmentScope } from "./state/session";
21
22const LAST_EDITOR_KEY = "t3code:last-editor";
23
24export 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
35export 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
48export 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
59function 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
68export 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}