312 lines · 7.7 KB · TypeScript
RawDownload
import { 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 { 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, isMacPlatform, isWindowsPlatform } 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<OpenInOption> = [    {      label: "Cursor",      Icon: CursorIcon,      value: "cursor",      kind: "brand",    },    {      label: "Trae",      Icon: TraeIcon,      value: "trae",      kind: "brand",    },    {      label: "Kiro",      Icon: KiroIcon,      value: "kiro",      kind: "brand",    },    {      label: "VS Code",      Icon: VisualStudioCode,      value: "vscode",      kind: "brand",    },    {      label: "VS Code Insiders",      Icon: VisualStudioCodeInsiders,      value: "vscode-insiders",      kind: "brand",    },    {      label: "VSCodium",      Icon: VSCodium,      value: "vscodium",      kind: "brand",    },    {      label: "Zed",      Icon: Zed,      value: "zed",      kind: "brand",    },    {      label: "Antigravity",      Icon: AntigravityIcon,      value: "antigravity",      kind: "brand",    },    {      label: "IntelliJ IDEA",      Icon: IntelliJIdeaIcon,      value: "idea",      kind: "brand",    },    {      label: "Aqua",      Icon: AquaIcon,      value: "aqua",      kind: "brand",    },    {      label: "CLion",      Icon: CLionIcon,      value: "clion",      kind: "brand",    },    {      label: "DataGrip",      Icon: DataGripIcon,      value: "datagrip",      kind: "brand",    },    {      label: "DataSpell",      Icon: DataSpellIcon,      value: "dataspell",      kind: "brand",    },    {      label: "GoLand",      Icon: GoLandIcon,      value: "goland",      kind: "brand",    },    {      label: "PhpStorm",      Icon: PhpStormIcon,      value: "phpstorm",      kind: "brand",    },    {      label: "PyCharm",      Icon: PyCharmIcon,      value: "pycharm",      kind: "brand",    },    {      label: "Rider",      Icon: RiderIcon,      value: "rider",      kind: "brand",    },    {      label: "RubyMine",      Icon: RubyMineIcon,      value: "rubymine",      kind: "brand",    },    {      label: "RustRover",      Icon: RustRoverIcon,      value: "rustrover",      kind: "brand",    },    {      label: "WebStorm",      Icon: WebStormIcon,      value: "webstorm",      kind: "brand",    },    {      label: isMacPlatform(platform)        ? "Finder"        : isWindowsPlatform(platform)          ? "Explorer"          : "Files",      Icon: FolderClosedIcon,      value: "file-manager",      kind: "generic",    },  ];  const availableEditorSet = new Set(availableEditors);  return baseOptions.filter((option) => availableEditorSet.has(option.value));}; 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 [preferredEditor, setPreferredEditor] = usePreferredEditor(availableEditors);  const options = useMemo(    () => resolveOptions(navigator.platform, availableEditors),    [availableEditors],  );  const primaryOption = options.find(({ value }) => value === preferredEditor) ?? null;   const openInEditor = useCallback(    (editorId: EditorId | null) => {      if (!openInCwd) return;      const editor = editorId ?? preferredEditor;      if (!editor) return;      const result = openInEditorMutation({        environmentId,        input: {          cwd: openInCwd,          editor,        },      });      setPreferredEditor(editor);      return result;    },    [environmentId, openInCwd, openInEditorMutation, preferredEditor, 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 openInEditorMutation({        environmentId,        input: {          cwd: openInCwd,          editor: preferredEditor,        },      });    };    window.addEventListener("keydown", handler);    return () => window.removeEventListener("keydown", handler);  }, [    enableShortcut,    environmentId,    keybindings,    openInCwd,    openInEditorMutation,    preferredEditor,  ]);   return (    <Group aria-label="Open in editor">      <Button        aria-label={compact ? "Open file in preferred editor" : undefined}        size="xs"        variant="outline"        disabled={!preferredEditor || !openInCwd}        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={compact ? "Choose editor" : "Copy options"}              size="icon-xs"              variant="outline"            />          }        >          <ChevronDownIcon aria-hidden="true" className="size-4" />        </MenuTrigger>        <MenuPopup align="end">          {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>          ))}        </MenuPopup>      </Menu>    </Group>  );});