History

feat(web): add tooltip to composer file mention pill (#1944)

Guilherme Vieira committed 7 months ago

Commit 28cb9db1 parent 7968f271 file · +48 −62Browse files

Unified · Split

apps/web/src/components/ComposerPromptEditor.tsx+48−62
@@ -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";
42import {39import {
@@ -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 SerializedTextNode103 SerializedLexicalNode
107>;104>;
108 105
109type SerializedComposerSkillNode = Spread<106type SerializedComposerSkillNode = Spread<
@@ -132,7 +129,40 @@ const ComposerTerminalContextActionsContext = createContext<{ · 22 unmodified lines
132 onRemoveTerminalContext: () => {},129 onRemoveTerminalContext: () => {},
133});130});
134 131
135class ComposerMentionNode extends TextNode {132function 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
165class 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
437function 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
457function terminalContextSignature(contexts: ReadonlyArray<TerminalContextDraft>): string {452function terminalContextSignature(contexts: ReadonlyArray<TerminalContextDraft>): string {
458 return contexts453 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