ソースを参照

Add markdown link tooltip support to the visual editor

GitOrigin-RevId: 7ab4fd0793f057c86e4fc0307d81df376efe4d34
Domagoj Kriskovic 2 ヶ月 前
コミット
8ccdc0fbc4

+ 13 - 4
services/web/frontend/js/features/source-editor/components/codemirror-command-tooltip.tsx

@@ -5,8 +5,10 @@ import {
 } from './codemirror-context'
 import {
   closeCommandTooltip,
+  commandTooltipHandlers,
   commandTooltipState,
 } from '../extensions/command-tooltip'
+import type { CommandTooltipHandler } from '../extensions/command-tooltip'
 import ReactDOM from 'react-dom'
 import { HrefTooltipContent } from './command-tooltip/href-tooltip'
 import { UrlTooltipContent } from './command-tooltip/url-tooltip'
@@ -68,14 +70,18 @@ export const CodeMirrorCommandTooltip = memo(function CodeMirrorLinkTooltip() {
   }
 
   return ReactDOM.createPortal(
-    <CodeMirrorCommandTooltipContent command={tooltipState.command} />,
+    <CodeMirrorCommandTooltipContent
+      command={tooltipState.command}
+      handlers={state.facet(commandTooltipHandlers)}
+    />,
     tooltipView.dom
   )
 })
 
 const CodeMirrorCommandTooltipContent = memo<{
   command: string
-}>(function CodeMirrorCommandTooltipContent({ command }) {
+  handlers: readonly CommandTooltipHandler[]
+}>(function CodeMirrorCommandTooltipContent({ command, handlers }) {
   switch (command) {
     case 'HrefCommand':
       return <HrefTooltipContent />
@@ -89,7 +95,10 @@ const CodeMirrorCommandTooltipContent = memo<{
       return <InputTooltipContent />
     case 'Subfile':
       return <SubfileTooltipContent />
-    default:
-      return null
+    default: {
+      // Tooltips contributed by modules via the `commandTooltipHandlers` facet
+      const handler = handlers.find(handler => handler.command === command)
+      return handler ? handler.render() : null
+    }
   }
 })

+ 38 - 4
services/web/frontend/js/features/source-editor/extensions/command-tooltip.ts

@@ -11,11 +11,13 @@ import {
 import {
   EditorSelection,
   EditorState,
+  Facet,
   Prec,
   SelectionRange,
   StateEffect,
   StateField,
 } from '@codemirror/state'
+import type { ReactNode } from 'react'
 import { ancestorOfNodeWithType } from '../utils/tree-query'
 import { ensureSyntaxTree, syntaxTree } from '@codemirror/language'
 import {
@@ -32,12 +34,30 @@ import {
 } from '@codemirror/autocomplete'
 import { SyntaxNode } from '@lezer/common'
 
-type ActiveTooltip = {
+export type ActiveTooltip = {
   range: SelectionRange
   tooltip: Tooltip
   command: string
 } | null
 
+export type CommandTooltipContext = {
+  state: EditorState
+  pos: number
+  value: ActiveTooltip
+  commandNode: SyntaxNode
+}
+
+// A command-tooltip handler lets a module add a tooltip for a syntax node type
+// that isn't part of the LaTeX `$CommandTooltipCommand` group (e.g. the
+// markdown `Link` node)
+export type CommandTooltipHandler = {
+  command: string
+  createTooltip: (context: CommandTooltipContext) => ActiveTooltip
+  render: () => ReactNode
+}
+
+export const commandTooltipHandlers = Facet.define<CommandTooltipHandler>()
+
 const createTooltipView = (
   update?: (update: ViewUpdate) => void
 ): TooltipView => {
@@ -47,7 +67,7 @@ const createTooltipView = (
   return { dom, update }
 }
 
-const buildTooltip = (
+export const buildTooltip = (
   command: string,
   pos: number,
   value: ActiveTooltip,
@@ -89,7 +109,12 @@ const createTooltipState = (
   const { main } = state.selection
   const pos = main.head
   const node = syntaxTree(state).resolveInner(pos, 0)
-  const commandNode = ancestorOfNodeWithType(node, '$CommandTooltipCommand')
+  const moduleHandlers = state.facet(commandTooltipHandlers)
+  const commandNode = ancestorOfNodeWithType(
+    node,
+    '$CommandTooltipCommand',
+    ...moduleHandlers.map(handler => handler.command)
+  )
   if (!commandNode) {
     return null
   }
@@ -99,6 +124,15 @@ const createTooltipState = (
   }
   const commandName = commandNode.name
 
+  // Module-provided handlers (e.g. the markdown link tooltip) take priority
+  // over the built-in LaTeX commands below.
+  const handler = moduleHandlers.find(
+    handler => handler.command === commandName
+  )
+  if (handler) {
+    return handler.createTooltip({ state, pos, value, commandNode })
+  }
+
   switch (commandName) {
     // a hyperlink (\href)
     case 'HrefCommand': {
@@ -317,7 +351,7 @@ export const commandTooltipState = (
   state: EditorState
 ): ActiveTooltip | undefined => state.field(commandTooltipStateField, false)
 
-const firstInteractiveElement = (
+export const firstInteractiveElement = (
   view: EditorView,
   tooltipState: NonNullable<ActiveTooltip>
 ) =>