Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 48 additions & 62 deletions apps/web/src/components/ComposerPromptEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,8 @@ import {
type ElementNode,
type LexicalNode,
type SerializedLexicalNode,
TextNode,
type EditorConfig,
type EditorState,
type NodeKey,
type SerializedTextNode,
type Spread,
} from "lexical";
import {
Expand Down Expand Up @@ -103,7 +100,7 @@ type SerializedComposerMentionNode = Spread<
type: "composer-mention";
version: 1;
},
SerializedTextNode
SerializedLexicalNode
>;

type SerializedComposerSkillNode = Spread<
Expand Down Expand Up @@ -132,7 +129,40 @@ const ComposerTerminalContextActionsContext = createContext<{
onRemoveTerminalContext: () => {},
});

class ComposerMentionNode extends TextNode {
function ComposerMentionDecorator(props: { path: string }) {
const theme = resolvedThemeFromDocument();
const chip = (
<span
className={COMPOSER_INLINE_CHIP_CLASS_NAME}
contentEditable={false}
spellCheck={false}
data-composer-mention-chip="true"
>
<img
alt=""
aria-hidden="true"
className={COMPOSER_INLINE_CHIP_ICON_CLASS_NAME}
loading="lazy"
src={getVscodeIconUrlForEntry(props.path, inferEntryKindFromPath(props.path), theme)}
/>
<span className={COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME}>{basenameOfPath(props.path)}</span>
</span>
);

return (
<Tooltip>
<TooltipTrigger render={chip} />
<TooltipPopup
side="top"
className="max-w-[30rem] whitespace-normal leading-tight wrap-anywhere"
>
{props.path}
</TooltipPopup>
</Tooltip>
);
}

class ComposerMentionNode extends DecoratorNode<ReactElement> {
__path: string;

static override getType(): string {
Expand All @@ -144,12 +174,12 @@ class ComposerMentionNode extends TextNode {
}

static override importJSON(serializedNode: SerializedComposerMentionNode): ComposerMentionNode {
return $createComposerMentionNode(serializedNode.path);
return $createComposerMentionNode(serializedNode.path).updateFromJSON(serializedNode);
}

constructor(path: string, key?: NodeKey) {
super(key);
const normalizedPath = path.startsWith("@") ? path.slice(1) : path;
super(`@${normalizedPath}`, key);
this.__path = normalizedPath;
}

Expand All @@ -162,41 +192,26 @@ class ComposerMentionNode extends TextNode {
};
}

override createDOM(_config: EditorConfig): HTMLElement {
override createDOM(): HTMLElement {
const dom = document.createElement("span");
dom.className = COMPOSER_INLINE_CHIP_CLASS_NAME;
dom.contentEditable = "false";
dom.setAttribute("spellcheck", "false");
renderMentionChipDom(dom, this.__path);
dom.className = "inline-flex align-middle leading-none";
return dom;
}

override updateDOM(
prevNode: ComposerMentionNode,
dom: HTMLElement,
_config: EditorConfig,
): boolean {
dom.contentEditable = "false";
if (prevNode.__text !== this.__text || prevNode.__path !== this.__path) {
renderMentionChipDom(dom, this.__path);
}
return false;
}

override canInsertTextBefore(): false {
override updateDOM(): false {
return false;
}

override canInsertTextAfter(): false {
return false;
override getTextContent(): string {
return `@${this.__path}`;
}

override isTextEntity(): true {
override isInline(): true {
return true;
}

override isToken(): true {
return true;
override decorate(): ReactElement {
return <ComposerMentionDecorator path={this.__path} />;
}
}

Expand Down Expand Up @@ -434,26 +449,6 @@ function resolvedThemeFromDocument(): "light" | "dark" {
return document.documentElement.classList.contains("dark") ? "dark" : "light";
}

function renderMentionChipDom(container: HTMLElement, pathValue: string): void {
container.textContent = "";
container.style.setProperty("user-select", "none");
container.style.setProperty("-webkit-user-select", "none");

const theme = resolvedThemeFromDocument();
const icon = document.createElement("img");
icon.alt = "";
icon.ariaHidden = "true";
icon.className = COMPOSER_INLINE_CHIP_ICON_CLASS_NAME;
icon.loading = "lazy";
icon.src = getVscodeIconUrlForEntry(pathValue, inferEntryKindFromPath(pathValue), theme);

const label = document.createElement("span");
label.className = COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME;
label.textContent = basenameOfPath(pathValue);

container.append(icon, label);
}

function terminalContextSignature(contexts: ReadonlyArray<TerminalContextDraft>): string {
return contexts
.map((context) =>
Expand Down Expand Up @@ -595,12 +590,9 @@ function getAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: number): numb
}

if ($isTextNode(node)) {
if (node instanceof ComposerMentionNode) {
return getAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset);
}
return offset + Math.min(pointOffset, node.getTextContentSize());
}
if (node instanceof ComposerSkillNode || node instanceof ComposerTerminalContextNode) {
if (isComposerInlineTokenNode(node)) {
return getAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset);
}

Expand Down Expand Up @@ -642,12 +634,9 @@ function getExpandedAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: numbe
}

if ($isTextNode(node)) {
if (node instanceof ComposerMentionNode) {
return getExpandedAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset);
}
return offset + Math.min(pointOffset, node.getTextContentSize());
}
if (node instanceof ComposerSkillNode || node instanceof ComposerTerminalContextNode) {
if (isComposerInlineTokenNode(node)) {
return getExpandedAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset);
}

Expand All @@ -673,10 +662,7 @@ function findSelectionPointAtOffset(
node: LexicalNode,
remainingRef: { value: number },
): { key: string; offset: number; type: "text" | "element" } | null {
if (node instanceof ComposerMentionNode || node instanceof ComposerSkillNode) {
return findSelectionPointForInlineToken(node, remainingRef);
}
if (node instanceof ComposerTerminalContextNode) {
if (isComposerInlineTokenNode(node)) {
return findSelectionPointForInlineToken(node, remainingRef);
}

Expand Down
Loading