| ... | ... | @@ -32,11 +32,8 @@ import { |
| 32 | 32 | type ElementNode, |
| 33 | 33 | type LexicalNode, |
| 34 | 34 | type SerializedLexicalNode, |
| 35 | | TextNode, |
| 36 | | type EditorConfig, |
| 37 | 35 | type EditorState, |
| 38 | 36 | type NodeKey, |
| 39 | | type SerializedTextNode, |
| 40 | 37 | type Spread, |
| 41 | 38 | } from "lexical"; |
| 42 | 39 | import { |
| ... | ... | @@ -103,7 +100,7 @@ type SerializedComposerMentionNode = Spread< |
| 103 | 100 | type: "composer-mention"; |
| 104 | 101 | version: 1; |
| 105 | 102 | }, |
| 106 | | SerializedTextNode |
| 103 | SerializedLexicalNode |
| 107 | 104 | >; |
| 108 | 105 | |
| 109 | 106 | type SerializedComposerSkillNode = Spread< |
| ... | ... | @@ -132,7 +129,40 @@ const ComposerTerminalContextActionsContext = createContext<{ |
| 132 | 129 | onRemoveTerminalContext: () => {}, |
| 133 | 130 | }); |
| 134 | 131 | |
| 135 | | class ComposerMentionNode extends TextNode { |
| 132 | function ComposerMentionDecorator(props: { path: string }) { |
| 133 | const theme = resolvedThemeFromDocument(); |
| 134 | const chip = ( |
| 135 | <span |
| 136 | className={COMPOSER_INLINE_CHIP_CLASS_NAME} |
| 137 | contentEditable={false} |
| 138 | spellCheck={false} |
| 139 | data-composer-mention-chip="true" |
| 140 | > |
| 141 | <img |
| 142 | alt="" |
| 143 | aria-hidden="true" |
| 144 | className={COMPOSER_INLINE_CHIP_ICON_CLASS_NAME} |
| 145 | loading="lazy" |
| 146 | src={getVscodeIconUrlForEntry(props.path, inferEntryKindFromPath(props.path), theme)} |
| 147 | /> |
| 148 | <span className={COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME}>{basenameOfPath(props.path)}</span> |
| 149 | </span> |
| 150 | ); |
| 151 | |
| 152 | return ( |
| 153 | <Tooltip> |
| 154 | <TooltipTrigger render={chip} /> |
| 155 | <TooltipPopup |
| 156 | side="top" |
| 157 | className="max-w-[30rem] whitespace-normal leading-tight wrap-anywhere" |
| 158 | > |
| 159 | {props.path} |
| 160 | </TooltipPopup> |
| 161 | </Tooltip> |
| 162 | ); |
| 163 | } |
| 164 | |
| 165 | class ComposerMentionNode extends DecoratorNode<ReactElement> { |
| 136 | 166 | __path: string; |
| 137 | 167 | |
| 138 | 168 | static override getType(): string { |
| ... | ... | @@ -144,12 +174,12 @@ class ComposerMentionNode extends TextNode { |
| 144 | 174 | } |
| 145 | 175 | |
| 146 | 176 | static override importJSON(serializedNode: SerializedComposerMentionNode): ComposerMentionNode { |
| 147 | | return $createComposerMentionNode(serializedNode.path); |
| 177 | return $createComposerMentionNode(serializedNode.path).updateFromJSON(serializedNode); |
| 148 | 178 | } |
| 149 | 179 | |
| 150 | 180 | constructor(path: string, key?: NodeKey) { |
| 181 | super(key); |
| 151 | 182 | const normalizedPath = path.startsWith("@") ? path.slice(1) : path; |
| 152 | | super(`@${normalizedPath}`, key); |
| 153 | 183 | this.__path = normalizedPath; |
| 154 | 184 | } |
| 155 | 185 | |
| ... | ... | @@ -162,41 +192,26 @@ class ComposerMentionNode extends TextNode { |
| 162 | 192 | }; |
| 163 | 193 | } |
| 164 | 194 | |
| 165 | | override createDOM(_config: EditorConfig): HTMLElement { |
| 195 | override createDOM(): HTMLElement { |
| 166 | 196 | const dom = document.createElement("span"); |
| 167 | | dom.className = COMPOSER_INLINE_CHIP_CLASS_NAME; |
| 168 | | dom.contentEditable = "false"; |
| 169 | | dom.setAttribute("spellcheck", "false"); |
| 170 | | renderMentionChipDom(dom, this.__path); |
| 197 | dom.className = "inline-flex align-middle leading-none"; |
| 171 | 198 | return dom; |
| 172 | 199 | } |
| 173 | 200 | |
| 174 | | override updateDOM( |
| 175 | | prevNode: ComposerMentionNode, |
| 176 | | dom: HTMLElement, |
| 177 | | _config: EditorConfig, |
| 178 | | ): boolean { |
| 179 | | dom.contentEditable = "false"; |
| 180 | | if (prevNode.__text !== this.__text || prevNode.__path !== this.__path) { |
| 181 | | renderMentionChipDom(dom, this.__path); |
| 182 | | } |
| 183 | | return false; |
| 184 | | } |
| 185 | | |
| 186 | | override canInsertTextBefore(): false { |
| 201 | override updateDOM(): false { |
| 187 | 202 | return false; |
| 188 | 203 | } |
| 189 | 204 | |
| 190 | | override canInsertTextAfter(): false { |
| 191 | | return false; |
| 205 | override getTextContent(): string { |
| 206 | return `@${this.__path}`; |
| 192 | 207 | } |
| 193 | 208 | |
| 194 | | override isTextEntity(): true { |
| 209 | override isInline(): true { |
| 195 | 210 | return true; |
| 196 | 211 | } |
| 197 | 212 | |
| 198 | | override isToken(): true { |
| 199 | | return true; |
| 213 | override decorate(): ReactElement { |
| 214 | return <ComposerMentionDecorator path={this.__path} />; |
| 200 | 215 | } |
| 201 | 216 | } |
| 202 | 217 | |
| ... | ... | @@ -434,26 +449,6 @@ function resolvedThemeFromDocument(): "light" | "dark" { |
| 434 | 449 | return document.documentElement.classList.contains("dark") ? "dark" : "light"; |
| 435 | 450 | } |
| 436 | 451 | |
| 437 | | function renderMentionChipDom(container: HTMLElement, pathValue: string): void { |
| 438 | | container.textContent = ""; |
| 439 | | container.style.setProperty("user-select", "none"); |
| 440 | | container.style.setProperty("-webkit-user-select", "none"); |
| 441 | | |
| 442 | | const theme = resolvedThemeFromDocument(); |
| 443 | | const icon = document.createElement("img"); |
| 444 | | icon.alt = ""; |
| 445 | | icon.ariaHidden = "true"; |
| 446 | | icon.className = COMPOSER_INLINE_CHIP_ICON_CLASS_NAME; |
| 447 | | icon.loading = "lazy"; |
| 448 | | icon.src = getVscodeIconUrlForEntry(pathValue, inferEntryKindFromPath(pathValue), theme); |
| 449 | | |
| 450 | | const label = document.createElement("span"); |
| 451 | | label.className = COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME; |
| 452 | | label.textContent = basenameOfPath(pathValue); |
| 453 | | |
| 454 | | container.append(icon, label); |
| 455 | | } |
| 456 | | |
| 457 | 452 | function terminalContextSignature(contexts: ReadonlyArray<TerminalContextDraft>): string { |
| 458 | 453 | return contexts |
| 459 | 454 | .map((context) => |
| ... | ... | @@ -595,12 +590,9 @@ function getAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: number): numb |
| 595 | 590 | } |
| 596 | 591 | |
| 597 | 592 | if ($isTextNode(node)) { |
| 598 | | if (node instanceof ComposerMentionNode) { |
| 599 | | return getAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset); |
| 600 | | } |
| 601 | 593 | return offset + Math.min(pointOffset, node.getTextContentSize()); |
| 602 | 594 | } |
| 603 | | if (node instanceof ComposerSkillNode || node instanceof ComposerTerminalContextNode) { |
| 595 | if (isComposerInlineTokenNode(node)) { |
| 604 | 596 | return getAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset); |
| 605 | 597 | } |
| 606 | 598 | |
| ... | ... | @@ -642,12 +634,9 @@ function getExpandedAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: numbe |
| 642 | 634 | } |
| 643 | 635 | |
| 644 | 636 | if ($isTextNode(node)) { |
| 645 | | if (node instanceof ComposerMentionNode) { |
| 646 | | return getExpandedAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset); |
| 647 | | } |
| 648 | 637 | return offset + Math.min(pointOffset, node.getTextContentSize()); |
| 649 | 638 | } |
| 650 | | if (node instanceof ComposerSkillNode || node instanceof ComposerTerminalContextNode) { |
| 639 | if (isComposerInlineTokenNode(node)) { |
| 651 | 640 | return getExpandedAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset); |
| 652 | 641 | } |
| 653 | 642 | |
| ... | ... | @@ -673,10 +662,7 @@ function findSelectionPointAtOffset( |
| 673 | 662 | node: LexicalNode, |
| 674 | 663 | remainingRef: { value: number }, |
| 675 | 664 | ): { key: string; offset: number; type: "text" | "element" } | null { |
| 676 | | if (node instanceof ComposerMentionNode || node instanceof ComposerSkillNode) { |
| 677 | | return findSelectionPointForInlineToken(node, remainingRef); |
| 678 | | } |
| 679 | | if (node instanceof ComposerTerminalContextNode) { |
| 665 | if (isComposerInlineTokenNode(node)) { |
| 680 | 666 | return findSelectionPointForInlineToken(node, remainingRef); |
| 681 | 667 | } |
| 682 | 668 | |