Unified · Split
| @@ -32,11 +32,8 @@ import { · 31 unmodified lines | |||
| 32 | type ElementNode, | 32 | type ElementNode, |
| 33 | type LexicalNode, | 33 | type LexicalNode, |
| 34 | type SerializedLexicalNode, | 34 | type SerializedLexicalNode, |
| 35 | TextNode, | ||
| 36 | type EditorConfig, | ||
| 37 | type EditorState, | 35 | type EditorState, |
| 38 | type NodeKey, | 36 | type NodeKey, |
| 39 | type SerializedTextNode, | ||
| 40 | type Spread, | 37 | type Spread, |
| 41 | } from "lexical"; | 38 | } from "lexical"; |
| 42 | import { | 39 | import { |
| @@ -103,7 +100,7 @@ type SerializedComposerMentionNode = Spread< · 60 unmodified lines | |||
| 103 | type: "composer-mention"; | 100 | type: "composer-mention"; |
| 104 | version: 1; | 101 | version: 1; |
| 105 | }, | 102 | }, |
| 106 | SerializedTextNode | 103 | SerializedLexicalNode |
| 107 | >; | 104 | >; |
| 108 | 105 | ||
| 109 | type SerializedComposerSkillNode = Spread< | 106 | type SerializedComposerSkillNode = Spread< |
| @@ -132,7 +129,40 @@ const ComposerTerminalContextActionsContext = createContext<{ · 22 unmodified lines | |||
| 132 | onRemoveTerminalContext: () => {}, | 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 | __path: string; | 166 | __path: string; |
| 137 | 167 | ||
| 138 | static override getType(): string { | 168 | static override getType(): string { |
| @@ -144,12 +174,12 @@ class ComposerMentionNode extends TextNode { · 5 unmodified lines | |||
| 144 | } | 174 | } |
| 145 | 175 | ||
| 146 | static override importJSON(serializedNode: SerializedComposerMentionNode): ComposerMentionNode { | 176 | static override importJSON(serializedNode: SerializedComposerMentionNode): ComposerMentionNode { |
| 147 | return $createComposerMentionNode(serializedNode.path); | 177 | return $createComposerMentionNode(serializedNode.path).updateFromJSON(serializedNode); |
| 148 | } | 178 | } |
| 149 | 179 | ||
| 150 | constructor(path: string, key?: NodeKey) { | 180 | constructor(path: string, key?: NodeKey) { |
| 181 | super(key); | ||
| 151 | const normalizedPath = path.startsWith("@") ? path.slice(1) : path; | 182 | const normalizedPath = path.startsWith("@") ? path.slice(1) : path; |
| 152 | super(`@${normalizedPath}`, key); | ||
| 153 | this.__path = normalizedPath; | 183 | this.__path = normalizedPath; |
| 154 | } | 184 | } |
| 155 | 185 | ||
| @@ -162,41 +192,26 @@ class ComposerMentionNode extends TextNode { · 6 unmodified lines | |||
| 162 | }; | 192 | }; |
| 163 | } | 193 | } |
| 164 | 194 | ||
| 165 | override createDOM(_config: EditorConfig): HTMLElement { | 195 | override createDOM(): HTMLElement { |
| 166 | const dom = document.createElement("span"); | 196 | const dom = document.createElement("span"); |
| 167 | dom.className = COMPOSER_INLINE_CHIP_CLASS_NAME; | 197 | dom.className = "inline-flex align-middle leading-none"; |
| 168 | dom.contentEditable = "false"; | ||
| 169 | dom.setAttribute("spellcheck", "false"); | ||
| 170 | renderMentionChipDom(dom, this.__path); | ||
| 171 | return dom; | 198 | return dom; |
| 172 | } | 199 | } |
| 173 | 200 | ||
| 174 | override updateDOM( | 201 | override updateDOM(): false { |
| 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 { | ||
| 187 | return false; | 202 | return false; |
| 188 | } | 203 | } |
| 189 | 204 | ||
| 190 | override canInsertTextAfter(): false { | 205 | override getTextContent(): string { |
| 191 | return false; | 206 | return `@${this.__path}`; |
| 192 | } | 207 | } |
| 193 | 208 | ||
| 194 | override isTextEntity(): true { | 209 | override isInline(): true { |
| 195 | return true; | 210 | return true; |
| 196 | } | 211 | } |
| 197 | 212 | ||
| 198 | override isToken(): true { | 213 | override decorate(): ReactElement { |
| 199 | return true; | 214 | return <ComposerMentionDecorator path={this.__path} />; |
| 200 | } | 215 | } |
| 201 | } | 216 | } |
| 202 | 217 | ||
| @@ -434,26 +449,6 @@ function resolvedThemeFromDocument(): "light" | "dark" { · 231 unmodified lines | |||
| 434 | return document.documentElement.classList.contains("dark") ? "dark" : "light"; | 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 | function terminalContextSignature(contexts: ReadonlyArray<TerminalContextDraft>): string { | 452 | function terminalContextSignature(contexts: ReadonlyArray<TerminalContextDraft>): string { |
| 458 | return contexts | 453 | return contexts |
| 459 | .map((context) => | 454 | .map((context) => |
| @@ -595,12 +590,9 @@ function getAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: number): numb · 135 unmodified lines | |||
| 595 | } | 590 | } |
| 596 | 591 | ||
| 597 | if ($isTextNode(node)) { | 592 | if ($isTextNode(node)) { |
| 598 | if (node instanceof ComposerMentionNode) { | ||
| 599 | return getAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset); | ||
| 600 | } | ||
| 601 | return offset + Math.min(pointOffset, node.getTextContentSize()); | 593 | return offset + Math.min(pointOffset, node.getTextContentSize()); |
| 602 | } | 594 | } |
| 603 | if (node instanceof ComposerSkillNode || node instanceof ComposerTerminalContextNode) { | 595 | if (isComposerInlineTokenNode(node)) { |
| 604 | return getAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset); | 596 | return getAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset); |
| 605 | } | 597 | } |
| 606 | 598 | ||
| @@ -642,12 +634,9 @@ function getExpandedAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: numbe · 35 unmodified lines | |||
| 642 | } | 634 | } |
| 643 | 635 | ||
| 644 | if ($isTextNode(node)) { | 636 | if ($isTextNode(node)) { |
| 645 | if (node instanceof ComposerMentionNode) { | ||
| 646 | return getExpandedAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset); | ||
| 647 | } | ||
| 648 | return offset + Math.min(pointOffset, node.getTextContentSize()); | 637 | return offset + Math.min(pointOffset, node.getTextContentSize()); |
| 649 | } | 638 | } |
| 650 | if (node instanceof ComposerSkillNode || node instanceof ComposerTerminalContextNode) { | 639 | if (isComposerInlineTokenNode(node)) { |
| 651 | return getExpandedAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset); | 640 | return getExpandedAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset); |
| 652 | } | 641 | } |
| 653 | 642 | ||
| @@ -673,10 +662,7 @@ function findSelectionPointAtOffset( · 19 unmodified lines | |||
| 673 | node: LexicalNode, | 662 | node: LexicalNode, |
| 674 | remainingRef: { value: number }, | 663 | remainingRef: { value: number }, |
| 675 | ): { key: string; offset: number; type: "text" | "element" } | null { | 664 | ): { key: string; offset: number; type: "text" | "element" } | null { |
| 676 | if (node instanceof ComposerMentionNode || node instanceof ComposerSkillNode) { | 665 | if (isComposerInlineTokenNode(node)) { |
| 677 | return findSelectionPointForInlineToken(node, remainingRef); | ||
| 678 | } | ||
| 679 | if (node instanceof ComposerTerminalContextNode) { | ||
| 680 | return findSelectionPointForInlineToken(node, remainingRef); | 666 | return findSelectionPointForInlineToken(node, remainingRef); |
| 681 | } | 667 | } |
| 682 | 668 | ||