Julius Marmingedeeper in atomf8b6e5b7 months agoHistory
482 lines · 15.2 KB · TypeScript
RawDownload
import { useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react";import { type GitBranch } from "@t3tools/contracts";import { useVirtualizer } from "@tanstack/react-virtual";import * as Option from "effect/Option";import { AsyncResult } from "effect/unstable/reactivity";import { ChevronDownIcon } from "lucide-react";import {  type CSSProperties,  useCallback,  useDeferredValue,  useEffect,  useMemo,  useOptimistic,  useRef,  useState,  useTransition,} from "react"; import { parsePullRequestReference } from "../pullRequestReference";import { REACTIVITY_KEYS } from "../rpc/client";import {  gitBranchesAtom,  gitCheckoutMutationAtom,  gitCreateBranchMutationAtom,  gitStatusAtom,} from "../rpc/gitAtoms";import {  dedupeRemoteBranchesWithLocalMatches,  deriveLocalBranchNameFromRemoteRef,  EnvMode,  resolveBranchSelectionTarget,  resolveBranchToolbarValue,  shouldIncludeBranchPickerItem,} from "./BranchToolbar.logic";import { Button } from "./ui/button";import {  Combobox,  ComboboxEmpty,  ComboboxInput,  ComboboxItem,  ComboboxList,  ComboboxPopup,  ComboboxTrigger,} from "./ui/combobox";import { toastManager } from "./ui/toast"; interface BranchToolbarBranchSelectorProps {  activeProjectCwd: string;  activeThreadBranch: string | null;  activeWorktreePath: string | null;  branchCwd: string | null;  effectiveEnvMode: EnvMode;  envLocked: boolean;  onSetThreadBranch: (branch: string | null, worktreePath: string | null) => void;  onCheckoutPullRequestRequest?: (reference: string) => void;  onComposerFocusRequest?: () => void;} function toBranchActionErrorMessage(error: unknown): string {  return error instanceof Error ? error.message : "An error occurred.";} function getBranchTriggerLabel(input: {  activeWorktreePath: string | null;  effectiveEnvMode: EnvMode;  resolvedActiveBranch: string | null;}): string {  const { activeWorktreePath, effectiveEnvMode, resolvedActiveBranch } = input;  if (!resolvedActiveBranch) {    return "Select branch";  }  if (effectiveEnvMode === "worktree" && !activeWorktreePath) {    return `From ${resolvedActiveBranch}`;  }  return resolvedActiveBranch;} export function BranchToolbarBranchSelector({  activeProjectCwd,  activeThreadBranch,  activeWorktreePath,  branchCwd,  effectiveEnvMode,  envLocked,  onSetThreadBranch,  onCheckoutPullRequestRequest,  onComposerFocusRequest,}: BranchToolbarBranchSelectorProps) {  const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false);  const [branchQuery, setBranchQuery] = useState("");  const deferredBranchQuery = useDeferredValue(branchQuery);  const branchesResult = useAtomValue(gitBranchesAtom(branchCwd));  const branchStatusResult = useAtomValue(gitStatusAtom(branchCwd));  const refreshBranches = useAtomRefresh(gitBranchesAtom(branchCwd));  const refreshBranchStatus = useAtomRefresh(gitStatusAtom(branchCwd));  const checkoutBranch = useAtomSet(gitCheckoutMutationAtom, { mode: "promise" });  const createGitBranch = useAtomSet(gitCreateBranchMutationAtom, { mode: "promise" });  const branchesData = Option.getOrUndefined(AsyncResult.value(branchesResult));  const branchStatus = Option.getOrUndefined(AsyncResult.value(branchStatusResult));  const branches = useMemo(    () => dedupeRemoteBranchesWithLocalMatches(branchesData?.branches ?? []),    [branchesData?.branches],  );  const currentGitBranch =    branchStatus?.branch ?? branches.find((branch) => branch.current)?.name ?? null;  const canonicalActiveBranch = resolveBranchToolbarValue({    envMode: effectiveEnvMode,    activeWorktreePath,    activeThreadBranch,    currentGitBranch,  });  const branchNames = useMemo(() => branches.map((branch) => branch.name), [branches]);  const branchByName = useMemo(    () => new Map(branches.map((branch) => [branch.name, branch] as const)),    [branches],  );  const trimmedBranchQuery = branchQuery.trim();  const deferredTrimmedBranchQuery = deferredBranchQuery.trim();  const normalizedDeferredBranchQuery = deferredTrimmedBranchQuery.toLowerCase();  const prReference = parsePullRequestReference(trimmedBranchQuery);  const isSelectingWorktreeBase =    effectiveEnvMode === "worktree" && !envLocked && !activeWorktreePath;  const checkoutPullRequestItemValue =    prReference && onCheckoutPullRequestRequest ? `__checkout_pull_request__:${prReference}` : null;  const canCreateBranch = !isSelectingWorktreeBase && trimmedBranchQuery.length > 0;  const hasExactBranchMatch = branchByName.has(trimmedBranchQuery);  const createBranchItemValue = canCreateBranch    ? `__create_new_branch__:${trimmedBranchQuery}`    : null;  const branchPickerItems = useMemo(() => {    const items = [...branchNames];    if (createBranchItemValue && !hasExactBranchMatch) {      items.push(createBranchItemValue);    }    if (checkoutPullRequestItemValue) {      items.unshift(checkoutPullRequestItemValue);    }    return items;  }, [branchNames, checkoutPullRequestItemValue, createBranchItemValue, hasExactBranchMatch]);  const filteredBranchPickerItems = useMemo(    () =>      normalizedDeferredBranchQuery.length === 0        ? branchPickerItems        : branchPickerItems.filter((itemValue) =>            shouldIncludeBranchPickerItem({              itemValue,              normalizedQuery: normalizedDeferredBranchQuery,              createBranchItemValue,              checkoutPullRequestItemValue,            }),          ),    [      branchPickerItems,      checkoutPullRequestItemValue,      createBranchItemValue,      normalizedDeferredBranchQuery,    ],  );  const [resolvedActiveBranch, setOptimisticBranch] = useOptimistic(    canonicalActiveBranch,    (_currentBranch: string | null, optimisticBranch: string | null) => optimisticBranch,  );  const [isBranchActionPending, startBranchActionTransition] = useTransition();  const isBranchesLoading =    branches.length === 0 && (AsyncResult.isInitial(branchesResult) || branchesResult.waiting);  const shouldVirtualizeBranchList = filteredBranchPickerItems.length > 40;   const runBranchAction = (action: () => Promise<void>) => {    startBranchActionTransition(async () => {      await action().catch(() => undefined);    });  };   const selectBranch = (branch: GitBranch) => {    if (!branchCwd || isBranchActionPending) return;     // In new-worktree mode, selecting a branch sets the base branch.    if (isSelectingWorktreeBase) {      onSetThreadBranch(branch.name, null);      setIsBranchMenuOpen(false);      onComposerFocusRequest?.();      return;    }     const selectionTarget = resolveBranchSelectionTarget({      activeProjectCwd,      activeWorktreePath,      branch,    });     // If the branch already lives in a worktree, point the thread there.    if (selectionTarget.reuseExistingWorktree) {      onSetThreadBranch(branch.name, selectionTarget.nextWorktreePath);      setIsBranchMenuOpen(false);      onComposerFocusRequest?.();      return;    }     const selectedBranchName = branch.isRemote      ? deriveLocalBranchNameFromRemoteRef(branch.name)      : branch.name;     setIsBranchMenuOpen(false);    onComposerFocusRequest?.();     runBranchAction(async () => {      setOptimisticBranch(selectedBranchName);      try {        await checkoutBranch({          payload: {            cwd: selectionTarget.checkoutCwd,            branch: branch.name,          },          reactivityKeys: [REACTIVITY_KEYS.git(selectionTarget.checkoutCwd)],        });      } catch (error) {        toastManager.add({          type: "error",          title: "Failed to checkout branch.",          description: toBranchActionErrorMessage(error),        });        return;      }       setOptimisticBranch(selectedBranchName);      onSetThreadBranch(selectedBranchName, selectionTarget.nextWorktreePath);    });  };   const createBranch = (rawName: string) => {    const name = rawName.trim();    if (!branchCwd || !name || isBranchActionPending) return;     setIsBranchMenuOpen(false);    onComposerFocusRequest?.();     runBranchAction(async () => {      setOptimisticBranch(name);       try {        await createGitBranch({          payload: { cwd: branchCwd, branch: name },        });        try {          await checkoutBranch({            payload: { cwd: branchCwd, branch: name },            reactivityKeys: [REACTIVITY_KEYS.git(branchCwd)],          });        } catch (error) {          toastManager.add({            type: "error",            title: "Failed to checkout branch.",            description: toBranchActionErrorMessage(error),          });          return;        }      } catch (error) {        toastManager.add({          type: "error",          title: "Failed to create branch.",          description: toBranchActionErrorMessage(error),        });        return;      }       setOptimisticBranch(name);      onSetThreadBranch(name, activeWorktreePath);      setBranchQuery("");    });  };   useEffect(() => {    if (      effectiveEnvMode !== "worktree" ||      activeWorktreePath ||      activeThreadBranch ||      !currentGitBranch    ) {      return;    }    onSetThreadBranch(currentGitBranch, null);  }, [    activeThreadBranch,    activeWorktreePath,    currentGitBranch,    effectiveEnvMode,    onSetThreadBranch,  ]);   const handleOpenChange = useCallback(    (open: boolean) => {      setIsBranchMenuOpen(open);      if (!open) {        setBranchQuery("");        return;      }      if (branchCwd) {        refreshBranches();        refreshBranchStatus();      }    },    [branchCwd, refreshBranchStatus, refreshBranches],  );   const branchListScrollElementRef = useRef<HTMLDivElement | null>(null);  const branchListVirtualizer = useVirtualizer({    count: filteredBranchPickerItems.length,    estimateSize: (index) =>      filteredBranchPickerItems[index] === checkoutPullRequestItemValue ? 44 : 28,    getScrollElement: () => branchListScrollElementRef.current,    overscan: 12,    enabled: isBranchMenuOpen && shouldVirtualizeBranchList,    initialRect: {      height: 224,      width: 0,    },  });  const virtualBranchRows = branchListVirtualizer.getVirtualItems();  const setBranchListRef = useCallback(    (element: HTMLDivElement | null) => {      branchListScrollElementRef.current =        (element?.parentElement as HTMLDivElement | null) ?? null;      if (element) {        branchListVirtualizer.measure();      }    },    [branchListVirtualizer],  );   useEffect(() => {    if (!isBranchMenuOpen || !shouldVirtualizeBranchList) return;    queueMicrotask(() => {      branchListVirtualizer.measure();    });  }, [    branchListVirtualizer,    filteredBranchPickerItems.length,    isBranchMenuOpen,    shouldVirtualizeBranchList,  ]);   const triggerLabel = getBranchTriggerLabel({    activeWorktreePath,    effectiveEnvMode,    resolvedActiveBranch,  });   function renderPickerItem(itemValue: string, index: number, style?: CSSProperties) {    if (checkoutPullRequestItemValue && itemValue === checkoutPullRequestItemValue) {      return (        <ComboboxItem          hideIndicator          key={itemValue}          index={index}          value={itemValue}          style={style}          onClick={() => {            if (!prReference || !onCheckoutPullRequestRequest) {              return;            }            setIsBranchMenuOpen(false);            setBranchQuery("");            onComposerFocusRequest?.();            onCheckoutPullRequestRequest(prReference);          }}        >          <div className="flex min-w-0 flex-col items-start py-1">            <span className="truncate font-medium">Checkout Pull Request</span>            <span className="truncate text-muted-foreground text-xs">{prReference}</span>          </div>        </ComboboxItem>      );    }    if (createBranchItemValue && itemValue === createBranchItemValue) {      return (        <ComboboxItem          hideIndicator          key={itemValue}          index={index}          value={itemValue}          style={style}          onClick={() => createBranch(trimmedBranchQuery)}        >          <span className="truncate">Create new branch "{trimmedBranchQuery}"</span>        </ComboboxItem>      );    }     const branch = branchByName.get(itemValue);    if (!branch) return null;     const hasSecondaryWorktree = branch.worktreePath && branch.worktreePath !== activeProjectCwd;    const badge = branch.current      ? "current"      : hasSecondaryWorktree        ? "worktree"        : branch.isRemote          ? "remote"          : branch.isDefault            ? "default"            : null;    return (      <ComboboxItem        hideIndicator        key={itemValue}        index={index}        value={itemValue}        style={style}        onClick={() => selectBranch(branch)}      >        <div className="flex w-full items-center justify-between gap-2">          <span className="truncate">{itemValue}</span>          {badge && <span className="shrink-0 text-[10px] text-muted-foreground/45">{badge}</span>}        </div>      </ComboboxItem>    );  }   return (    <Combobox      items={branchPickerItems}      filteredItems={filteredBranchPickerItems}      autoHighlight      virtualized={shouldVirtualizeBranchList}      onItemHighlighted={(_value, eventDetails) => {        if (!isBranchMenuOpen || eventDetails.index < 0) return;        branchListVirtualizer.scrollToIndex(eventDetails.index, { align: "auto" });      }}      onOpenChange={handleOpenChange}      open={isBranchMenuOpen}      value={resolvedActiveBranch}    >      <ComboboxTrigger        render={<Button variant="ghost" size="xs" />}        className="text-muted-foreground/70 hover:text-foreground/80"        disabled={isBranchesLoading || isBranchActionPending}      >        <span className="max-w-[240px] truncate">{triggerLabel}</span>        <ChevronDownIcon />      </ComboboxTrigger>      <ComboboxPopup align="end" side="top" className="w-80">        <div className="border-b p-1">          <ComboboxInput            className="[&_input]:font-sans rounded-md"            inputClassName="ring-0"            placeholder="Search branches..."            showTrigger={false}            size="sm"            value={branchQuery}            onChange={(event) => setBranchQuery(event.target.value)}          />        </div>        <ComboboxEmpty>No branches found.</ComboboxEmpty>         <ComboboxList ref={setBranchListRef} className="max-h-56">          {shouldVirtualizeBranchList ? (            <div              className="relative"              style={{                height: `${branchListVirtualizer.getTotalSize()}px`,              }}            >              {virtualBranchRows.map((virtualRow) => {                const itemValue = filteredBranchPickerItems[virtualRow.index];                if (!itemValue) return null;                return renderPickerItem(itemValue, virtualRow.index, {                  position: "absolute",                  top: 0,                  left: 0,                  width: "100%",                  transform: `translateY(${virtualRow.start}px)`,                });              })}            </div>          ) : (            filteredBranchPickerItems.map((itemValue, index) => renderPickerItem(itemValue, index))          )}        </ComboboxList>      </ComboboxPopup>    </Combobox>  );}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t