325 lines · 8.8 KB · TypeScript
RawDownload
import {  buildRemoteOpenUrl,  EditorId,  type EnvironmentId,  type ResolvedKeybindingsConfig,} from "@t3tools/contracts";import { memo, useCallback, useEffect, useMemo } from "react";import { isOpenFavoriteEditorShortcut, shortcutLabelForCommand } from "../../keybindings";import { usePreferredEditor } from "../../editorPreferences";import { editorLabelForPlatform } from "../../editorLabels";import {  openRemoteEditorUrl,  useRemoteCapableEditors,  useRemoteOpenHint,  useRemoteOpenState,} from "../../remoteOpen";import { useEnvironment } from "../../state/environments";import { ChevronDownIcon, FolderClosedIcon } from "lucide-react";import { Button } from "../ui/button";import { Group, GroupSeparator } from "../ui/group";import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "../ui/menu";import {  AntigravityIcon,  CursorIcon,  Icon,  KiroIcon,  TraeIcon,  VisualStudioCode,  VisualStudioCodeInsiders,  VSCodium,  Zed,} from "../Icons";import {  AquaIcon,  CLionIcon,  DataGripIcon,  DataSpellIcon,  GoLandIcon,  IntelliJIdeaIcon,  PhpStormIcon,  PyCharmIcon,  RiderIcon,  RubyMineIcon,  RustRoverIcon,  WebStormIcon,} from "../JetBrainsIcons";import { cn } from "~/lib/utils";import { shellEnvironment } from "~/state/shell";import { useAtomCommand } from "~/state/use-atom-command"; type OpenInOption = {  label: string;  Icon: Icon;  value: EditorId;  kind: "brand" | "generic";}; const resolveOptions = (platform: string, availableEditors: ReadonlyArray<EditorId>) => {  const baseOptions: ReadonlyArray<Omit<OpenInOption, "label">> = [    {      Icon: CursorIcon,      value: "cursor",      kind: "brand",    },    {      Icon: TraeIcon,      value: "trae",      kind: "brand",    },    {      Icon: KiroIcon,      value: "kiro",      kind: "brand",    },    {      Icon: VisualStudioCode,      value: "vscode",      kind: "brand",    },    {      Icon: VisualStudioCodeInsiders,      value: "vscode-insiders",      kind: "brand",    },    {      Icon: VSCodium,      value: "vscodium",      kind: "brand",    },    {      Icon: Zed,      value: "zed",      kind: "brand",    },    {      Icon: AntigravityIcon,      value: "antigravity",      kind: "brand",    },    {      Icon: IntelliJIdeaIcon,      value: "idea",      kind: "brand",    },    {      Icon: AquaIcon,      value: "aqua",      kind: "brand",    },    {      Icon: CLionIcon,      value: "clion",      kind: "brand",    },    {      Icon: DataGripIcon,      value: "datagrip",      kind: "brand",    },    {      Icon: DataSpellIcon,      value: "dataspell",      kind: "brand",    },    {      Icon: GoLandIcon,      value: "goland",      kind: "brand",    },    {      Icon: PhpStormIcon,      value: "phpstorm",      kind: "brand",    },    {      Icon: PyCharmIcon,      value: "pycharm",      kind: "brand",    },    {      Icon: RiderIcon,      value: "rider",      kind: "brand",    },    {      Icon: RubyMineIcon,      value: "rubymine",      kind: "brand",    },    {      Icon: RustRoverIcon,      value: "rustrover",      kind: "brand",    },    {      Icon: WebStormIcon,      value: "webstorm",      kind: "brand",    },    {      Icon: FolderClosedIcon,      value: "file-manager",      kind: "generic",    },  ];  const availableEditorSet = new Set(availableEditors);  return baseOptions    .filter((option) => availableEditorSet.has(option.value))    .map((option) => ({ ...option, label: editorLabelForPlatform(option.value, platform) }));}; function getOpenInIconClass(kind: OpenInOption["kind"]) {  return cn(kind === "brand" ? "text-foreground opacity-100" : "text-muted-foreground");} export const OpenInPicker = memo(function OpenInPicker({  environmentId,  keybindings,  availableEditors,  openInCwd,  compact = false,  enableShortcut = true,}: {  environmentId: EnvironmentId;  keybindings: ResolvedKeybindingsConfig;  availableEditors: ReadonlyArray<EditorId>;  openInCwd: string | null;  compact?: boolean;  enableShortcut?: boolean;}) {  const openInEditorMutation = useAtomCommand(shellEnvironment.openInEditor, "open in editor");  const remote = useRemoteOpenState(environmentId);  const remoteCapableEditors = useRemoteCapableEditors();  const [remoteHintSeen, markRemoteHintSeen] = useRemoteOpenHint();  const environmentLabel = useEnvironment(environmentId)?.label ?? "this machine";  // Remote mode ignores the server's PATH probe: what matters is what runs on  // the viewing machine, which only the desktop app can probe.  const effectiveEditors = remote.mode === "local-exec" ? availableEditors : remoteCapableEditors;  const [preferredEditor, setPreferredEditor] = usePreferredEditor(effectiveEditors);  const options = useMemo(    () => resolveOptions(navigator.platform, effectiveEditors),    [effectiveEditors],  );  const primaryOption = options.find(({ value }) => value === preferredEditor) ?? null;   const openInEditor = useCallback(    (editorId: EditorId | null) => {      if (!openInCwd) return;      const editor = editorId ?? preferredEditor;      if (!editor) return;      if (remote.mode === "remote-unavailable") return;      if (remote.mode === "remote-links") {        const url = buildRemoteOpenUrl({          editor,          host: remote.host.host,          absolutePath: openInCwd,        });        if (url === undefined) return;        // Only record hint-seen/preferred when the shell actually accepted        // the URL (an older desktop build can refuse the editor scheme).        void openRemoteEditorUrl(url).then((opened) => {          if (!opened) return;          markRemoteHintSeen();          setPreferredEditor(editor);        });        return;      }      const result = openInEditorMutation({        environmentId,        input: {          cwd: openInCwd,          editor,        },      });      setPreferredEditor(editor);      return result;    },    [      environmentId,      markRemoteHintSeen,      openInCwd,      openInEditorMutation,      preferredEditor,      remote,      setPreferredEditor,    ],  );   const openFavoriteEditorShortcutLabel = useMemo(    () => shortcutLabelForCommand(keybindings, "editor.openFavorite"),    [keybindings],  );   useEffect(() => {    if (!enableShortcut) return;    const handler = (e: globalThis.KeyboardEvent) => {      if (!isOpenFavoriteEditorShortcut(e, keybindings)) return;      if (!openInCwd) return;      if (!preferredEditor) return;       e.preventDefault();      void openInEditor(preferredEditor);    };    window.addEventListener("keydown", handler);    return () => window.removeEventListener("keydown", handler);  }, [enableShortcut, keybindings, openInCwd, openInEditor, preferredEditor]);   return (    <Group aria-label="Open in editor">      <Button        aria-label={compact ? "Open file in preferred editor" : undefined}        className="ps-[8.5px]"        size="xs"        variant="outline"        disabled={!preferredEditor || !openInCwd || remote.mode === "remote-unavailable"}        onClick={() => openInEditor(preferredEditor)}      >        {primaryOption?.Icon && (          <primaryOption.Icon            aria-hidden="true"            className={cn("size-3.5", getOpenInIconClass(primaryOption.kind))}          />        )}        <span          className={            compact              ? "sr-only"              : "sr-only @3xl/header-actions:not-sr-only @3xl/header-actions:ml-0.5"          }        >          Open        </span>      </Button>      <GroupSeparator {...(!compact ? { className: "hidden @3xl/header-actions:block" } : {})} />      <Menu>        <MenuTrigger          render={<Button aria-label="Choose editor" size="icon-xs" variant="outline" />}        >          <ChevronDownIcon aria-hidden="true" className="size-4" />        </MenuTrigger>        <MenuPopup align="end">          {remote.mode === "remote-unavailable" ? (            <MenuItem disabled>No SSH route to {environmentLabel}</MenuItem>          ) : (            <>              {options.length === 0 && <MenuItem disabled>No installed editors found</MenuItem>}              {options.map(({ label, Icon, value, kind }) => (                <MenuItem key={value} onClick={() => openInEditor(value)}>                  <Icon aria-hidden="true" className={getOpenInIconClass(kind)} />                  {label}                  {value === preferredEditor && openFavoriteEditorShortcutLabel && (                    <MenuShortcut>{openFavoriteEditorShortcutLabel}</MenuShortcut>                  )}                </MenuItem>              ))}              {remote.mode === "remote-links" && !remoteHintSeen && (                <MenuItem disabled>Opens over SSH. Needs your key on {environmentLabel}</MenuItem>              )}            </>          )}        </MenuPopup>      </Menu>    </Group>  );});