| ... | @@ -32,11 +32,8 @@ import { | ... | @@ -32,11 +32,8 @@ import { |
| 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< | ... | @@ -103,7 +100,7 @@ type SerializedComposerMentionNode = Spread< |
| 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<{ | ... | @@ -132,7 +129,40 @@ const ComposerTerminalContextActionsContext = createContext<{ |
| 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 { | ... | @@ -144,12 +174,12 @@ class ComposerMentionNode extends TextNode { |
| 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 { | ... | @@ -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 | 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" { | ... | @@ -434,26 +449,6 @@ function resolvedThemeFromDocument(): "light" | "dark" { |
| 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 | ... | @@ -595,12 +590,9 @@ function getAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: number): numb |
| 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 | ... | @@ -642,12 +634,9 @@ function getExpandedAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: numbe |
| 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( | ... | @@ -673,10 +662,7 @@ function findSelectionPointAtOffset( |
| 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 | |