1507 lines · 46.0 KB · TypeScript
RawDownload
import { LexicalComposer, type InitialConfigType } from "@lexical/react/LexicalComposer";import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";import { ContentEditable } from "@lexical/react/LexicalContentEditable";import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";import {  $applyNodeReplacement,  $createRangeSelection,  $getSelection,  $setSelection,  $isElementNode,  $isLineBreakNode,  $isRangeSelection,  $isTextNode,  $createLineBreakNode,  $createParagraphNode,  $createTextNode,  KEY_ARROW_DOWN_COMMAND,  KEY_ARROW_LEFT_COMMAND,  KEY_ARROW_RIGHT_COMMAND,  KEY_ARROW_UP_COMMAND,  KEY_ENTER_COMMAND,  KEY_TAB_COMMAND,  COMMAND_PRIORITY_HIGH,  KEY_BACKSPACE_COMMAND,  $getRoot,  HISTORY_MERGE_TAG,  DecoratorNode,  type ElementNode,  type LexicalNode,  type SerializedLexicalNode,  TextNode,  type EditorConfig,  type EditorState,  type NodeKey,  type SerializedTextNode,  type Spread,} from "lexical";import {  createContext,  forwardRef,  useCallback,  useContext,  useEffect,  useImperativeHandle,  useLayoutEffect,  useMemo,  useRef,  type ClipboardEventHandler,  type ReactElement,  type Ref,} from "react"; import {  clampCollapsedComposerCursor,  collapseExpandedComposerCursor,  expandCollapsedComposerCursor,  isCollapsedCursorAdjacentToInlineToken,} from "~/composer-logic";import {  selectionTouchesMentionBoundary,  splitPromptIntoComposerSegments,} from "~/composer-editor-mentions";import {  INLINE_TERMINAL_CONTEXT_PLACEHOLDER,  type TerminalContextDraft,} from "~/lib/terminalContext";import { cn } from "~/lib/utils";import { basenameOfPath, getVscodeIconUrlForEntry, inferEntryKindFromPath } from "~/vscode-icons";import {  COMPOSER_INLINE_CHIP_CLASS_NAME,  COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,  COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME,} from "./composerInlineChip";import { ComposerPendingTerminalContextChip } from "./chat/ComposerPendingTerminalContexts"; const COMPOSER_EDITOR_HMR_KEY = `composer-editor-${Math.random().toString(36).slice(2)}`;const SURROUND_SYMBOLS: [string, string][] = [  ["(", ")"],  ["[", "]"],  ["{", "}"],  ["'", "'"],  ['"', '"'],  ["“", "”"],  ["`", "`"],  ["<", ">"],  ["«", "»"],  ["*", "*"],  ["_", "_"],];const SURROUND_SYMBOLS_MAP = new Map<string, string>(SURROUND_SYMBOLS);const BACKTICK_SURROUND_CLOSE_SYMBOL = SURROUND_SYMBOLS_MAP.get("`") ?? null; type SerializedComposerMentionNode = Spread<  {    path: string;    type: "composer-mention";    version: 1;  },  SerializedTextNode>; type SerializedComposerTerminalContextNode = Spread<  {    context: TerminalContextDraft;    type: "composer-terminal-context";    version: 1;  },  SerializedLexicalNode>; const ComposerTerminalContextActionsContext = createContext<{  onRemoveTerminalContext: (contextId: string) => void;}>({  onRemoveTerminalContext: () => {},}); class ComposerMentionNode extends TextNode {  __path: string;   static override getType(): string {    return "composer-mention";  }   static override clone(node: ComposerMentionNode): ComposerMentionNode {    return new ComposerMentionNode(node.__path, node.__key);  }   static override importJSON(serializedNode: SerializedComposerMentionNode): ComposerMentionNode {    return $createComposerMentionNode(serializedNode.path);  }   constructor(path: string, key?: NodeKey) {    const normalizedPath = path.startsWith("@") ? path.slice(1) : path;    super(`@${normalizedPath}`, key);    this.__path = normalizedPath;  }   override exportJSON(): SerializedComposerMentionNode {    return {      ...super.exportJSON(),      path: this.__path,      type: "composer-mention",      version: 1,    };  }   override createDOM(_config: EditorConfig): HTMLElement {    const dom = document.createElement("span");    dom.className = COMPOSER_INLINE_CHIP_CLASS_NAME;    dom.contentEditable = "false";    dom.setAttribute("spellcheck", "false");    renderMentionChipDom(dom, this.__path);    return dom;  }   override updateDOM(    prevNode: ComposerMentionNode,    dom: HTMLElement,    _config: EditorConfig,  ): boolean {    dom.contentEditable = "false";    if (prevNode.__text !== this.__text || prevNode.__path !== this.__path) {      renderMentionChipDom(dom, this.__path);    }    return false;  }   override canInsertTextBefore(): false {    return false;  }   override canInsertTextAfter(): false {    return false;  }   override isTextEntity(): true {    return true;  }   override isToken(): true {    return true;  }} function $createComposerMentionNode(path: string): ComposerMentionNode {  return $applyNodeReplacement(new ComposerMentionNode(path));} function ComposerTerminalContextDecorator(props: { context: TerminalContextDraft }) {  return <ComposerPendingTerminalContextChip context={props.context} />;} class ComposerTerminalContextNode extends DecoratorNode<ReactElement> {  __context: TerminalContextDraft;   static override getType(): string {    return "composer-terminal-context";  }   static override clone(node: ComposerTerminalContextNode): ComposerTerminalContextNode {    return new ComposerTerminalContextNode(node.__context, node.__key);  }   static override importJSON(    serializedNode: SerializedComposerTerminalContextNode,  ): ComposerTerminalContextNode {    return $createComposerTerminalContextNode(serializedNode.context);  }   constructor(context: TerminalContextDraft, key?: NodeKey) {    super(key);    this.__context = context;  }   override exportJSON(): SerializedComposerTerminalContextNode {    return {      ...super.exportJSON(),      context: this.__context,      type: "composer-terminal-context",      version: 1,    };  }   override createDOM(): HTMLElement {    const dom = document.createElement("span");    dom.className = "inline-flex align-middle leading-none";    return dom;  }   override updateDOM(): false {    return false;  }   override getTextContent(): string {    return INLINE_TERMINAL_CONTEXT_PLACEHOLDER;  }   override isInline(): true {    return true;  }   override decorate(): ReactElement {    return <ComposerTerminalContextDecorator context={this.__context} />;  }} function $createComposerTerminalContextNode(  context: TerminalContextDraft,): ComposerTerminalContextNode {  return $applyNodeReplacement(new ComposerTerminalContextNode(context));} type ComposerInlineTokenNode = ComposerMentionNode | ComposerTerminalContextNode; function isComposerInlineTokenNode(candidate: unknown): candidate is ComposerInlineTokenNode {  return (    candidate instanceof ComposerMentionNode || candidate instanceof ComposerTerminalContextNode  );} function resolvedThemeFromDocument(): "light" | "dark" {  return document.documentElement.classList.contains("dark") ? "dark" : "light";} function renderMentionChipDom(container: HTMLElement, pathValue: string): void {  container.textContent = "";  container.style.setProperty("user-select", "none");  container.style.setProperty("-webkit-user-select", "none");   const theme = resolvedThemeFromDocument();  const icon = document.createElement("img");  icon.alt = "";  icon.ariaHidden = "true";  icon.className = COMPOSER_INLINE_CHIP_ICON_CLASS_NAME;  icon.loading = "lazy";  icon.src = getVscodeIconUrlForEntry(pathValue, inferEntryKindFromPath(pathValue), theme);   const label = document.createElement("span");  label.className = COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME;  label.textContent = basenameOfPath(pathValue);   container.append(icon, label);} function terminalContextSignature(contexts: ReadonlyArray<TerminalContextDraft>): string {  return contexts    .map((context) =>      [        context.id,        context.threadId,        context.terminalId,        context.terminalLabel,        context.lineStart,        context.lineEnd,        context.createdAt,        context.text,      ].join("\u001f"),    )    .join("\u001e");} function clampExpandedCursor(value: string, cursor: number): number {  if (!Number.isFinite(cursor)) return value.length;  return Math.max(0, Math.min(value.length, Math.floor(cursor)));} function getComposerInlineTokenTextLength(_node: ComposerInlineTokenNode): 1 {  return 1;} function getComposerInlineTokenExpandedTextLength(node: ComposerInlineTokenNode): number {  return node.getTextContentSize();} function getAbsoluteOffsetForInlineTokenPoint(  node: ComposerInlineTokenNode,  absoluteOffset: number,  pointOffset: number,): number {  return absoluteOffset + (pointOffset > 0 ? getComposerInlineTokenTextLength(node) : 0);} function getExpandedAbsoluteOffsetForInlineTokenPoint(  node: ComposerInlineTokenNode,  absoluteOffset: number,  pointOffset: number,): number {  return absoluteOffset + (pointOffset > 0 ? getComposerInlineTokenExpandedTextLength(node) : 0);} function findSelectionPointForInlineToken(  node: ComposerInlineTokenNode,  remainingRef: { value: number },): { key: string; offset: number; type: "element" } | null {  const parent = node.getParent();  if (!parent || !$isElementNode(parent)) return null;  const index = node.getIndexWithinParent();  if (remainingRef.value === 0) {    return {      key: parent.getKey(),      offset: index,      type: "element",    };  }  if (remainingRef.value === getComposerInlineTokenTextLength(node)) {    return {      key: parent.getKey(),      offset: index + 1,      type: "element",    };  }  remainingRef.value -= getComposerInlineTokenTextLength(node);  return null;} function getComposerNodeTextLength(node: LexicalNode): number {  if (isComposerInlineTokenNode(node)) {    return getComposerInlineTokenTextLength(node);  }  if ($isTextNode(node)) {    return node.getTextContentSize();  }  if ($isLineBreakNode(node)) {    return 1;  }  if ($isElementNode(node)) {    return node.getChildren().reduce((total, child) => total + getComposerNodeTextLength(child), 0);  }  return 0;} function getComposerNodeExpandedTextLength(node: LexicalNode): number {  if (isComposerInlineTokenNode(node)) {    return getComposerInlineTokenExpandedTextLength(node);  }  if ($isTextNode(node)) {    return node.getTextContentSize();  }  if ($isLineBreakNode(node)) {    return 1;  }  if ($isElementNode(node)) {    return node      .getChildren()      .reduce((total, child) => total + getComposerNodeExpandedTextLength(child), 0);  }  return 0;} function getAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: number): number {  let offset = 0;  let current: LexicalNode | null = node;   while (current) {    const nextParent = current.getParent() as LexicalNode | null;    if (!nextParent || !$isElementNode(nextParent)) {      break;    }    const siblings = nextParent.getChildren();    const index = current.getIndexWithinParent();    for (let i = 0; i < index; i += 1) {      const sibling = siblings[i];      if (!sibling) continue;      offset += getComposerNodeTextLength(sibling);    }    current = nextParent;  }   if ($isTextNode(node)) {    if (node instanceof ComposerMentionNode) {      return getAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset);    }    return offset + Math.min(pointOffset, node.getTextContentSize());  }  if (node instanceof ComposerTerminalContextNode) {    return getAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset);  }   if ($isLineBreakNode(node)) {    return offset + Math.min(pointOffset, 1);  }   if ($isElementNode(node)) {    const children = node.getChildren();    const clampedOffset = Math.max(0, Math.min(pointOffset, children.length));    for (let i = 0; i < clampedOffset; i += 1) {      const child = children[i];      if (!child) continue;      offset += getComposerNodeTextLength(child);    }    return offset;  }   return offset;} function getExpandedAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: number): number {  let offset = 0;  let current: LexicalNode | null = node;   while (current) {    const nextParent = current.getParent() as LexicalNode | null;    if (!nextParent || !$isElementNode(nextParent)) {      break;    }    const siblings = nextParent.getChildren();    const index = current.getIndexWithinParent();    for (let i = 0; i < index; i += 1) {      const sibling = siblings[i];      if (!sibling) continue;      offset += getComposerNodeExpandedTextLength(sibling);    }    current = nextParent;  }   if ($isTextNode(node)) {    if (node instanceof ComposerMentionNode) {      return getExpandedAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset);    }    return offset + Math.min(pointOffset, node.getTextContentSize());  }  if (node instanceof ComposerTerminalContextNode) {    return getExpandedAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset);  }   if ($isLineBreakNode(node)) {    return offset + Math.min(pointOffset, 1);  }   if ($isElementNode(node)) {    const children = node.getChildren();    const clampedOffset = Math.max(0, Math.min(pointOffset, children.length));    for (let i = 0; i < clampedOffset; i += 1) {      const child = children[i];      if (!child) continue;      offset += getComposerNodeExpandedTextLength(child);    }    return offset;  }   return offset;} function findSelectionPointAtOffset(  node: LexicalNode,  remainingRef: { value: number },): { key: string; offset: number; type: "text" | "element" } | null {  if (node instanceof ComposerMentionNode) {    return findSelectionPointForInlineToken(node, remainingRef);  }  if (node instanceof ComposerTerminalContextNode) {    return findSelectionPointForInlineToken(node, remainingRef);  }   if ($isTextNode(node)) {    const size = node.getTextContentSize();    if (remainingRef.value <= size) {

Lines 1–500 of 1507.