CommandHandler.tsx 1.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. import { useCallback } from "react"
  2. import type { LexicalEditor } from "lexical"
  3. import { $getSelection, $isRangeSelection, $isTextNode, TextNode } from "lexical"
  4. export interface CommandMetadata {
  5. name: string
  6. description?: string
  7. source?: "command" | "mcp" | "skill"
  8. }
  9. export function useCommandHandler(
  10. editor: LexicalEditor,
  11. commandStartOffset: number | null,
  12. resetState: () => void,
  13. ) {
  14. const insertCommand = useCallback(
  15. (metadata: CommandMetadata) => {
  16. const res = { inserted: false }
  17. editor.update(() => {
  18. const selection = $getSelection()
  19. if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
  20. return
  21. }
  22. const anchor = selection.anchor
  23. const node = anchor.getNode()
  24. if (!$isTextNode(node)) {
  25. return
  26. }
  27. const offset = anchor.offset
  28. const textContent = node.getTextContent()
  29. const beforeCursor = textContent.slice(0, offset)
  30. const start = commandStartOffset ?? beforeCursor.lastIndexOf("/")
  31. if (start === -1) {
  32. return
  33. }
  34. const before = textContent.slice(0, start)
  35. const after = textContent.slice(offset)
  36. const commandText = `/${metadata.name}`
  37. const newText = before + commandText + after
  38. const textNode = node as TextNode
  39. textNode.setTextContent(newText)
  40. const newOffset = start + commandText.length
  41. textNode.select(newOffset, newOffset)
  42. res.inserted = true
  43. resetState()
  44. })
  45. if (res.inserted) return
  46. },
  47. [editor, commandStartOffset, resetState],
  48. )
  49. return { insertCommand }
  50. }