Skip to content
Source
Sign inSign up

editorPreferences.ts

129 lines4.0 KB

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