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
3232 type ElementNode,
3333 type LexicalNode,
3434 type SerializedLexicalNode,
35− TextNode,
36− type EditorConfig,
3735 type EditorState,
3836 type NodeKey,
39− type SerializedTextNode,
4037 type Spread,
4138 } from "lexical";
4239 import {
@@ -103,7 +100,7 @@ type SerializedComposerMentionNode = Spread< · 60 unmodified lines
103100 type: "composer-mention";
104101 version: 1;
105102 },
106− SerializedTextNode
103+ SerializedLexicalNode
107104 >;
108105
109106 type SerializedComposerSkillNode = Spread<
@@ -132,7 +129,40 @@ const ComposerTerminalContextActionsContext = createContext<{ · 22 unmodified lines
132129 onRemoveTerminalContext: () => {},
133130 });
134131
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> {
136166 __path: string;
137167
138168 static override getType(): string {
@@ -144,12 +174,12 @@ class ComposerMentionNode extends TextNode { · 5 unmodified lines
144174 }
145175
146176 static override importJSON(serializedNode: SerializedComposerMentionNode): ComposerMentionNode {
147− return $createComposerMentionNode(serializedNode.path);
177+ return $createComposerMentionNode(serializedNode.path).updateFromJSON(serializedNode);
148178 }
149179
150180 constructor(path: string, key?: NodeKey) {
181+ super(key);
151182 const normalizedPath = path.startsWith("@") ? path.slice(1) : path;
152− super(`@${normalizedPath}`, key);
153183 this.__path = normalizedPath;
154184 }
155185
@@ -162,41 +192,26 @@ class ComposerMentionNode extends TextNode { · 6 unmodified lines
162192 };
163193 }
164194
165− override createDOM(_config: EditorConfig): HTMLElement {
195+ override createDOM(): HTMLElement {
166196 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";
171198 return dom;
172199 }
173200
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 {
187202 return false;
188203 }
189204
190− override canInsertTextAfter(): false {
191− return false;
205+ override getTextContent(): string {
206+ return `@${this.__path}`;
192207 }
193208
194− override isTextEntity(): true {
209+ override isInline(): true {
195210 return true;
196211 }
197212
198− override isToken(): true {
199− return true;
213+ override decorate(): ReactElement {
214+ return <ComposerMentionDecorator path={this.__path} />;
200215 }
201216 }
202217
@@ -434,26 +449,6 @@ function resolvedThemeFromDocument(): "light" | "dark" { · 231 unmodified lines
434449 return document.documentElement.classList.contains("dark") ? "dark" : "light";
435450 }
436451
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−
457452 function terminalContextSignature(contexts: ReadonlyArray<TerminalContextDraft>): string {
458453 return contexts
459454 .map((context) =>
@@ -595,12 +590,9 @@ function getAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: number): numb · 135 unmodified lines
595590 }
596591
597592 if ($isTextNode(node)) {
598− if (node instanceof ComposerMentionNode) {
599− return getAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset);
600− }
601593 return offset + Math.min(pointOffset, node.getTextContentSize());
602594 }
603− if (node instanceof ComposerSkillNode || node instanceof ComposerTerminalContextNode) {
595+ if (isComposerInlineTokenNode(node)) {
604596 return getAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset);
605597 }
606598
@@ -642,12 +634,9 @@ function getExpandedAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: numbe · 35 unmodified lines
642634 }
643635
644636 if ($isTextNode(node)) {
645− if (node instanceof ComposerMentionNode) {
646− return getExpandedAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset);
647− }
648637 return offset + Math.min(pointOffset, node.getTextContentSize());
649638 }
650− if (node instanceof ComposerSkillNode || node instanceof ComposerTerminalContextNode) {
639+ if (isComposerInlineTokenNode(node)) {
651640 return getExpandedAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset);
652641 }
653642
@@ -673,10 +662,7 @@ function findSelectionPointAtOffset( · 19 unmodified lines
673662 node: LexicalNode,
674663 remainingRef: { value: number },
675664 ): { 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)) {
680666 return findSelectionPointForInlineToken(node, remainingRef);
681667 }
682668