From cc58dd292a67e143c0ba8a2fb7a41906ba711726 Mon Sep 17 00:00:00 2001 From: Heesu Suh Date: Thu, 17 Oct 2024 13:44:35 +0900 Subject: [PATCH] make mention copy and pastable (#30) * make mention copy and pastable * add mentionable if new mention node is created --- src/components/chat-view/Chat.tsx | 43 ++---- .../chat-view/chat-input/ChatUserInput.tsx | 94 ++++++++++--- .../chat-input/plugins/mention/MentionNode.ts | 128 +++++++++++------- .../plugins/mention/MentionPlugin.tsx | 16 +-- .../plugins/no-format/NoFormatPlugin.tsx | 17 +++ .../plugins/on-mutation/OnMutationPlugin.tsx | 7 +- .../chat-input/utils/get-mentionable-id.ts | 18 --- src/hooks/useChatHistory.ts | 87 +----------- src/types/mentionable.ts | 6 - src/utils/chatHistoryManager.ts | 7 +- src/utils/mentionable.ts | 99 ++++++++++++++ styles.css | 1 + 12 files changed, 304 insertions(+), 219 deletions(-) create mode 100644 src/components/chat-view/chat-input/plugins/no-format/NoFormatPlugin.tsx delete mode 100644 src/components/chat-view/chat-input/utils/get-mentionable-id.ts create mode 100644 src/utils/mentionable.ts diff --git a/src/components/chat-view/Chat.tsx b/src/components/chat-view/Chat.tsx index 28bb1f6..fb4564c 100644 --- a/src/components/chat-view/Chat.tsx +++ b/src/components/chat-view/Chat.tsx @@ -35,24 +35,19 @@ import { parseRequestMessages } from '../../utils/prompt' import ChatUserInput, { ChatUserInputRef } from './chat-input/ChatUserInput' import { editorStateToPlainText } from './chat-input/utils/editor-state-to-plain-text' -import { generateMentionableId } from './chat-input/utils/get-mentionable-id' import { ChatListDropdown } from './ChatListDropdown' import ReactMarkdown from './ReactMarkdown' // Add an empty line here const getNewInputMessage = (app: App): ChatUserMessage => { - const mentionable: Omit = { - type: 'current-file', - file: app.workspace.getActiveFile(), - } return { role: 'user', content: null, id: uuidv4(), mentionables: [ { - id: generateMentionableId(mentionable), - ...mentionable, + type: 'current-file', + file: app.workspace.getActiveFile(), }, ], } @@ -82,15 +77,11 @@ const Chat = forwardRef((props, ref) => { const [inputMessage, setInputMessage] = useState(() => { const newMessage = getNewInputMessage(app) if (props.selectedBlock) { - const blockMentionable: Omit = { - type: 'block', - ...props.selectedBlock, - } newMessage.mentionables = [ ...newMessage.mentionables, { - id: generateMentionableId(blockMentionable), - ...blockMentionable, + type: 'block', + ...props.selectedBlock, }, ] } @@ -326,10 +317,7 @@ const Chat = forwardRef((props, ref) => { setInputMessage((prevInputMessage) => ({ ...prevInputMessage, mentionables: [ - { - id: generateMentionableId(mentionable), - ...mentionable, - }, + mentionable, ...prevInputMessage.mentionables.filter( (mentionable) => mentionable.type !== 'current-file', ), @@ -342,10 +330,7 @@ const Chat = forwardRef((props, ref) => { ? { ...message, mentionables: [ - { - id: generateMentionableId(mentionable), - ...mentionable, - }, + mentionable, ...message.mentionables.filter( (mentionable) => mentionable.type !== 'current-file', ), @@ -374,13 +359,7 @@ const Chat = forwardRef((props, ref) => { if (focusedMessageId === inputMessage.id) { setInputMessage((prevInputMessage) => ({ ...prevInputMessage, - mentionables: [ - ...prevInputMessage.mentionables, - { - id: generateMentionableId(mentionable), - ...mentionable, - }, - ], + mentionables: [...prevInputMessage.mentionables, mentionable], })) } else { setChatMessages((prevChatHistory) => @@ -388,13 +367,7 @@ const Chat = forwardRef((props, ref) => { message.id === focusedMessageId && message.role === 'user' ? { ...message, - mentionables: [ - ...message.mentionables, - { - id: generateMentionableId(mentionable), - ...mentionable, - }, - ], + mentionables: [...message.mentionables, mentionable], } : message, ), diff --git a/src/components/chat-view/chat-input/ChatUserInput.tsx b/src/components/chat-view/chat-input/ChatUserInput.tsx index e5df49e..99233cd 100644 --- a/src/components/chat-view/chat-input/ChatUserInput.tsx +++ b/src/components/chat-view/chat-input/ChatUserInput.tsx @@ -8,7 +8,7 @@ import { EditorRefPlugin } from '@lexical/react/LexicalEditorRefPlugin' import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary' import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin' import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin' -import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin' +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin' import { $nodesOfType, LexicalEditor, SerializedEditorState } from 'lexical' import { forwardRef, @@ -17,15 +17,21 @@ import { useImperativeHandle, useRef, } from 'react' +import { + deserializeMentionable, + serializeMentionable, +} from 'src/utils/mentionable' import { useApp } from '../../../contexts/app-context' -import { Mentionable } from '../../../types/mentionable' +import { Mentionable, SerializedMentionable } from '../../../types/mentionable' import { fuzzySearch } from '../../../utils/fuzzy-search' +import { getMentionableKey } from '../../../utils/mentionable' import MentionableBadge from './MentionableBadge' import { ModelSelect } from './ModelSelect' import { MentionNode } from './plugins/mention/MentionNode' import MentionPlugin from './plugins/mention/MentionPlugin' +import NoFormatPlugin from './plugins/no-format/NoFormatPlugin' import OnEnterPlugin from './plugins/on-enter/OnEnterPlugin' import OnMutationPlugin, { NodeMutations, @@ -103,7 +109,14 @@ const ChatUserInput = forwardRef( ) const handleMentionFile = (mentionable: Mentionable) => { - if (mentionables.some((m) => m.id === mentionable.id)) { + const mentionableKey = getMentionableKey( + serializeMentionable(mentionable), + ) + if ( + mentionables.some( + (m) => getMentionableKey(serializeMentionable(m)) === mentionableKey, + ) + ) { return } setMentionables([...mentionables, mentionable]) @@ -112,33 +125,71 @@ const ChatUserInput = forwardRef( const handleMentionNodeMutation = ( mutations: NodeMutations, ) => { - const destroyedMentionableIds: string[] = [] + const destroyedMentionableKeys: string[] = [] + const addedMentionables: SerializedMentionable[] = [] mutations.forEach((mutation) => { - if (mutation.mutation !== 'destroyed') return + const mentionable = mutation.node.getMentionable() + const mentionableKey = getMentionableKey(mentionable) - const id = mutation.node.getId() + if (mutation.mutation === 'destroyed') { + const nodeWithSameMentionable = editorRef.current?.read(() => + $nodesOfType(MentionNode).find( + (node) => + getMentionableKey(node.getMentionable()) === mentionableKey, + ), + ) - const nodeWithSameId = editorRef.current?.read(() => - $nodesOfType(MentionNode).find((node) => node.getId() === id), - ) + if (!nodeWithSameMentionable) { + // remove mentionable only if it's not present in the editor state + destroyedMentionableKeys.push(mentionableKey) + } + } else if (mutation.mutation === 'created') { + if ( + mentionables.some( + (m) => + getMentionableKey(serializeMentionable(m)) === mentionableKey, + ) || + addedMentionables.some( + (m) => getMentionableKey(m) === mentionableKey, + ) + ) { + // do nothing if mentionable is already added + return + } - if (!nodeWithSameId) { - // remove mentionable only if it's not present in the editor state - destroyedMentionableIds.push(id) + addedMentionables.push(mentionable) } }) setMentionables( - mentionables.filter((m) => !destroyedMentionableIds.includes(m.id)), + mentionables + .filter( + (m) => + !destroyedMentionableKeys.includes( + getMentionableKey(serializeMentionable(m)), + ), + ) + .concat( + addedMentionables + .map((m) => deserializeMentionable(m, app)) + .filter((v) => !!v), + ), ) } const handleMentionableDelete = (mentionable: Mentionable) => { - setMentionables(mentionables.filter((m) => m.id !== mentionable.id)) + const mentionableKey = getMentionableKey( + serializeMentionable(mentionable), + ) + setMentionables( + mentionables.filter( + (m) => getMentionableKey(serializeMentionable(m)) !== mentionableKey, + ), + ) editorRef.current?.update(() => { $nodesOfType(MentionNode).forEach((node) => { - if (node.getId() === mentionable.id) { + if (getMentionableKey(node.getMentionable()) === mentionableKey) { node.remove() } }) @@ -151,7 +202,7 @@ const ChatUserInput = forwardRef(
{mentionables.map((m) => ( handleMentionableDelete(m)} /> @@ -160,7 +211,15 @@ const ChatUserInput = forwardRef( )} - ( onMutation={handleMentionNodeMutation} /> +
diff --git a/src/components/chat-view/chat-input/plugins/mention/MentionNode.ts b/src/components/chat-view/chat-input/plugins/mention/MentionNode.ts index 99a2b16..c31994b 100644 --- a/src/components/chat-view/chat-input/plugins/mention/MentionNode.ts +++ b/src/components/chat-view/chat-input/plugins/mention/MentionNode.ts @@ -8,6 +8,9 @@ import { $applyNodeReplacement, + DOMConversionMap, + DOMConversionOutput, + DOMExportOutput, type EditorConfig, type LexicalNode, type NodeKey, @@ -16,47 +19,61 @@ import { TextNode, } from 'lexical' +import { SerializedMentionable } from '../../../../../types/mentionable' + export const MENTION_NODE_TYPE = 'mention' export const MENTION_NODE_ATTRIBUTE = 'data-lexical-mention' +export const MENTION_NODE_MENTION_NAME_ATTRIBUTE = 'data-lexical-mention-name' +export const MENTION_NODE_MENTIONABLE_ATTRIBUTE = 'data-lexical-mentionable' export type SerializedMentionNode = Spread< { - id: string mentionName: string + mentionable: SerializedMentionable }, SerializedTextNode > -// function $convertMentionElement( -// domNode: HTMLElement, -// ): DOMConversionOutput | null { -// const textContent = domNode.textContent - -// if (textContent !== null) { -// const node = $createMentionNode(textContent) -// return { -// node, -// } -// } +function $convertMentionElement( + domNode: HTMLElement, +): DOMConversionOutput | null { + const textContent = domNode.textContent + const mentionName = + domNode.getAttribute(MENTION_NODE_MENTION_NAME_ATTRIBUTE) ?? + domNode.textContent ?? + '' + const mentionable = JSON.parse( + domNode.getAttribute(MENTION_NODE_MENTIONABLE_ATTRIBUTE) ?? '{}', + ) + + if (textContent !== null) { + const node = $createMentionNode( + mentionName, + mentionable as SerializedMentionable, + ) + return { + node, + } + } -// return null -// } + return null +} export class MentionNode extends TextNode { - __id: string - __mention: string + __mentionName: string + __mentionable: SerializedMentionable static getType(): string { return MENTION_NODE_TYPE } static clone(node: MentionNode): MentionNode { - return new MentionNode(node.__id, node.__mention, node.__text, node.__key) + return new MentionNode(node.__mentionName, node.__mentionable, node.__key) } static importJSON(serializedNode: SerializedMentionNode): MentionNode { const node = $createMentionNode( - serializedNode.id, serializedNode.mentionName, + serializedNode.mentionable, ) node.setTextContent(serializedNode.text) node.setFormat(serializedNode.format) @@ -66,18 +83,21 @@ export class MentionNode extends TextNode { return node } - constructor(id: string, mentionName: string, text?: string, key?: NodeKey) { - // super(text ?? mentionName, key); + constructor( + mentionName: string, + mentionable: SerializedMentionable, + key?: NodeKey, + ) { super(`@${mentionName}`, key) - this.__id = id - this.__mention = mentionName + this.__mentionName = mentionName + this.__mentionable = mentionable } exportJSON(): SerializedMentionNode { return { ...super.exportJSON(), - id: this.__id, - mentionName: this.__mention, + mentionName: this.__mentionName, + mentionable: this.__mentionable, type: MENTION_NODE_TYPE, version: 1, } @@ -89,26 +109,38 @@ export class MentionNode extends TextNode { return dom } - // exportDOM(): DOMExportOutput { - // const element = document.createElement('span') - // element.setAttribute(MENTION_NODE_ATTRIBUTE, 'true') - // element.textContent = this.__text - // return { element } - // } - - // static importDOM(): DOMConversionMap | null { - // return { - // span: (domNode: HTMLElement) => { - // if (!domNode.hasAttribute(MENTION_NODE_ATTRIBUTE)) { - // return null - // } - // return { - // conversion: $convertMentionElement, - // priority: 1, - // } - // }, - // } - // } + exportDOM(): DOMExportOutput { + const element = document.createElement('span') + element.setAttribute(MENTION_NODE_ATTRIBUTE, 'true') + element.setAttribute( + MENTION_NODE_MENTION_NAME_ATTRIBUTE, + this.__mentionName, + ) + element.setAttribute( + MENTION_NODE_MENTIONABLE_ATTRIBUTE, + JSON.stringify(this.__mentionable), + ) + element.textContent = this.__text + return { element } + } + + static importDOM(): DOMConversionMap | null { + return { + span: (domNode: HTMLElement) => { + if ( + !domNode.hasAttribute(MENTION_NODE_ATTRIBUTE) || + !domNode.hasAttribute(MENTION_NODE_MENTION_NAME_ATTRIBUTE) || + !domNode.hasAttribute(MENTION_NODE_MENTIONABLE_ATTRIBUTE) + ) { + return null + } + return { + conversion: $convertMentionElement, + priority: 1, + } + }, + } + } isTextEntity(): true { return true @@ -122,16 +154,16 @@ export class MentionNode extends TextNode { return false } - getId(): string { - return this.__id + getMentionable(): SerializedMentionable { + return this.__mentionable } } export function $createMentionNode( - id: string, mentionName: string, + mentionable: SerializedMentionable, ): MentionNode { - const mentionNode = new MentionNode(id, mentionName) + const mentionNode = new MentionNode(mentionName, mentionable) mentionNode.setMode('token').toggleDirectionless() return $applyNodeReplacement(mentionNode) } diff --git a/src/components/chat-view/chat-input/plugins/mention/MentionPlugin.tsx b/src/components/chat-view/chat-input/plugins/mention/MentionPlugin.tsx index d50fd26..28dab41 100644 --- a/src/components/chat-view/chat-input/plugins/mention/MentionPlugin.tsx +++ b/src/components/chat-view/chat-input/plugins/mention/MentionPlugin.tsx @@ -13,9 +13,10 @@ import { $createTextNode, COMMAND_PRIORITY_NORMAL, TextNode } from 'lexical' import { TFile } from 'obsidian' import { useCallback, useMemo, useState } from 'react' import { createPortal } from 'react-dom' +import { serializeMentionable } from 'src/utils/mentionable' import { Mentionable, MentionableFile } from '../../../../../types/mentionable' -import { generateMentionableId } from '../../utils/get-mentionable-id' +import { getMentionableName } from '../../../../../utils/mentionable' import { MenuOption, MenuTextMatch } from '../shared/LexicalMenu' import { LexicalTypeaheadMenuPlugin, @@ -177,16 +178,14 @@ export default function NewMentionsPlugin({ nodeToReplace: TextNode | null, closeMenu: () => void, ) => { - const mentionable: Omit = { + const mentionable: MentionableFile = { type: 'file', file: selectedOption.file, } - const mentionableId = generateMentionableId(mentionable) - editor.update(() => { const mentionNode = $createMentionNode( - mentionableId, - selectedOption.name, + getMentionableName(mentionable), + serializeMentionable(mentionable), ) if (nodeToReplace) { nodeToReplace.replace(mentionNode) @@ -199,10 +198,7 @@ export default function NewMentionsPlugin({ closeMenu() }) - onAddMention({ - id: mentionableId, - ...mentionable, - }) + onAddMention(mentionable) }, [editor, onAddMention], ) diff --git a/src/components/chat-view/chat-input/plugins/no-format/NoFormatPlugin.tsx b/src/components/chat-view/chat-input/plugins/no-format/NoFormatPlugin.tsx new file mode 100644 index 0000000..5301de6 --- /dev/null +++ b/src/components/chat-view/chat-input/plugins/no-format/NoFormatPlugin.tsx @@ -0,0 +1,17 @@ +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { TextNode } from 'lexical' +import { useEffect } from 'react' + +export default function NoFormatPlugin() { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + editor.registerNodeTransform(TextNode, (node) => { + if (node.getFormat() !== 0) { + node.setFormat(0) + } + }) + }, [editor]) + + return null +} diff --git a/src/components/chat-view/chat-input/plugins/on-mutation/OnMutationPlugin.tsx b/src/components/chat-view/chat-input/plugins/on-mutation/OnMutationPlugin.tsx index 07e959b..ce36950 100644 --- a/src/components/chat-view/chat-input/plugins/on-mutation/OnMutationPlugin.tsx +++ b/src/components/chat-view/chat-input/plugins/on-mutation/OnMutationPlugin.tsx @@ -17,6 +17,8 @@ export default function OnMutationPlugin({ const removeListener = editor.registerMutationListener( nodeClass, (mutatedNodes, payload) => { + const editorState = editor.getEditorState() + const mutations = new Map< NodeKey, { mutation: NodeMutation; node: T } @@ -24,7 +26,10 @@ export default function OnMutationPlugin({ for (const [key, mutation] of mutatedNodes) { mutations.set(key, { mutation, - node: payload.prevEditorState._nodeMap.get(key) as T, + node: + mutation === 'destroyed' + ? (payload.prevEditorState._nodeMap.get(key) as T) + : (editorState._nodeMap.get(key) as T), }) } diff --git a/src/components/chat-view/chat-input/utils/get-mentionable-id.ts b/src/components/chat-view/chat-input/utils/get-mentionable-id.ts deleted file mode 100644 index 4b8ee49..0000000 --- a/src/components/chat-view/chat-input/utils/get-mentionable-id.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { - Mentionable, - MentionableBlock, - MentionableFile, -} from '../../../../types/mentionable' - -export function generateMentionableId( - mentionable: Omit, -): string { - switch (mentionable.type) { - case 'file': - return (mentionable as MentionableFile).file.path - case 'current-file': - return 'current-file' - case 'block': - return `${(mentionable as MentionableBlock).file.path}:${(mentionable as MentionableBlock).startLine}:${(mentionable as MentionableBlock).endLine}` - } -} diff --git a/src/hooks/useChatHistory.ts b/src/hooks/useChatHistory.ts index 74f0274..cee12e0 100644 --- a/src/hooks/useChatHistory.ts +++ b/src/hooks/useChatHistory.ts @@ -10,89 +10,12 @@ import { ChatMessage, SerializedChatMessage, } from '../types/chat' -import { Mentionable, SerializedMentionable } from '../types/mentionable' +import { Mentionable } from '../types/mentionable' import { ChatConversationManager } from '../utils/chatHistoryManager' - -const serializeMentionable = ( - mentionable: Mentionable, -): SerializedMentionable => { - switch (mentionable.type) { - case 'file': - return { - id: mentionable.id, - type: 'file', - file: mentionable.file.path, - } - case 'current-file': - return { - id: mentionable.id, - type: 'current-file', - file: mentionable.file?.path ?? null, - } - case 'block': - return { - id: mentionable.id, - type: 'block', - content: mentionable.content, - file: mentionable.file.path, - startLine: mentionable.startLine, - endLine: mentionable.endLine, - } - } -} - -const deserializeMentionable = ( - mentionable: SerializedMentionable, - app: App, -): Mentionable | null => { - try { - switch (mentionable.type) { - case 'file': { - const file = app.vault.getFileByPath(mentionable.file) - if (!file) { - return null - } - return { - id: mentionable.id, - type: 'file', - file: file, - } - } - case 'current-file': { - if (!mentionable.file) { - return { - id: mentionable.id, - type: 'current-file', - file: null, - } - } - const file = app.vault.getFileByPath(mentionable.file) - return { - id: mentionable.id, - type: 'current-file', - file: file, - } - } - case 'block': { - const file = app.vault.getFileByPath(mentionable.file) - if (!file) { - return null - } - return { - id: mentionable.id, - type: 'block', - content: mentionable.content, - file: file, - startLine: mentionable.startLine, - endLine: mentionable.endLine, - } - } - } - } catch (e) { - console.error('Error deserializing mentionable', e) - return null - } -} +import { + deserializeMentionable, + serializeMentionable, +} from '../utils/mentionable' const serializeChatMessage = (message: ChatMessage): SerializedChatMessage => { switch (message.role) { diff --git a/src/types/mentionable.ts b/src/types/mentionable.ts index 853ad0f..fa40fc0 100644 --- a/src/types/mentionable.ts +++ b/src/types/mentionable.ts @@ -1,12 +1,10 @@ import { TFile } from 'obsidian' export type MentionableFile = { - id: string type: 'file' file: TFile } export type MentionableCurrentFile = { - id: string type: 'current-file' file: TFile | null } @@ -17,7 +15,6 @@ export type MentionableBlockData = { endLine: number } export type MentionableBlock = MentionableBlockData & { - id: string type: 'block' } export type Mentionable = @@ -26,17 +23,14 @@ export type Mentionable = | MentionableBlock export type SerializedMentionableFile = { - id: string type: 'file' file: string } export type SerializedMentionableCurrentFile = { - id: string type: 'current-file' file: string | null } export type SerializedMentionableBlock = { - id: string type: 'block' content: string file: string diff --git a/src/utils/chatHistoryManager.ts b/src/utils/chatHistoryManager.ts index 5dbd2dd..734c144 100644 --- a/src/utils/chatHistoryManager.ts +++ b/src/utils/chatHistoryManager.ts @@ -2,7 +2,7 @@ import { App, normalizePath } from 'obsidian' import { ChatConversation, ChatConversationMeta } from '../types/chat' -const CURRENT_SCHEMA_VERSION = 1 +const CURRENT_SCHEMA_VERSION = 2 const CHAT_HISTORY_DIR = '.smtcmp_chat_histories' const CHAT_LIST_FILE = 'chat_list.json' @@ -63,7 +63,10 @@ export class ChatConversationManager { const chatListPath = this.getChatListPath() if (await this.app.vault.adapter.exists(chatListPath)) { const content = await this.app.vault.adapter.read(chatListPath) - return JSON.parse(content) as ChatConversationMeta[] + const chatList = JSON.parse(content) as ChatConversationMeta[] + return chatList.filter( + (chat) => chat.schemaVersion === CURRENT_SCHEMA_VERSION, + ) } return [] } diff --git a/src/utils/mentionable.ts b/src/utils/mentionable.ts new file mode 100644 index 0000000..9b63bd4 --- /dev/null +++ b/src/utils/mentionable.ts @@ -0,0 +1,99 @@ +import { App } from 'obsidian' + +import { Mentionable, SerializedMentionable } from '../types/mentionable' + +export const serializeMentionable = ( + mentionable: Mentionable, +): SerializedMentionable => { + switch (mentionable.type) { + case 'file': + return { + type: 'file', + file: mentionable.file.path, + } + case 'current-file': + return { + type: 'current-file', + file: mentionable.file?.path ?? null, + } + case 'block': + return { + type: 'block', + content: mentionable.content, + file: mentionable.file.path, + startLine: mentionable.startLine, + endLine: mentionable.endLine, + } + } +} + +export const deserializeMentionable = ( + mentionable: SerializedMentionable, + app: App, +): Mentionable | null => { + try { + switch (mentionable.type) { + case 'file': { + const file = app.vault.getFileByPath(mentionable.file) + if (!file) { + return null + } + return { + type: 'file', + file: file, + } + } + case 'current-file': { + if (!mentionable.file) { + return { + type: 'current-file', + file: null, + } + } + const file = app.vault.getFileByPath(mentionable.file) + return { + type: 'current-file', + file: file, + } + } + case 'block': { + const file = app.vault.getFileByPath(mentionable.file) + if (!file) { + return null + } + return { + type: 'block', + content: mentionable.content, + file: file, + startLine: mentionable.startLine, + endLine: mentionable.endLine, + } + } + } + } catch (e) { + console.error('Error deserializing mentionable', e) + return null + } +} + +export function getMentionableKey(mentionable: SerializedMentionable): string { + switch (mentionable.type) { + case 'file': + return `file:${mentionable.file}` + case 'current-file': + return `current-file:${mentionable.file ?? 'current'}` + case 'block': + return `block:${mentionable.file}:${mentionable.startLine}:${mentionable.endLine}:${mentionable.content}` + } +} + +export function getMentionableName(mentionable: Mentionable): string { + switch (mentionable.type) { + case 'file': + return mentionable.file.name + case 'current-file': + return mentionable.file?.name ?? 'Current File' + case 'block': + return `${mentionable.file.name} (${mentionable.startLine}:${mentionable.endLine})` + } +} diff --git a/styles.css b/styles.css index 37d009c..abce5a2 100644 --- a/styles.css +++ b/styles.css @@ -265,6 +265,7 @@ button:not(.clickable-icon).smtcmp-chat-list-dropdown { color: var(--tag-color); padding: 0 calc(var(--size-2-1)); border-radius: var(--radius-s); + word-break: break-all; } .smtcmp-chat-input-paragraph {