Add markdown link tooltip support to the visual editor

GitOrigin-RevId: 7ab4fd0793f057c86e4fc0307d81df376efe4d34
This commit is contained in:
Domagoj Kriskovic
2026-06-16 08:05:46 +00:00
committed by Copybot
parent 93a4f0c88a
commit 8ccdc0fbc4
2 changed files with 51 additions and 8 deletions
@@ -5,8 +5,10 @@ import {
} from './codemirror-context' } from './codemirror-context'
import { import {
closeCommandTooltip, closeCommandTooltip,
commandTooltipHandlers,
commandTooltipState, commandTooltipState,
} from '../extensions/command-tooltip' } from '../extensions/command-tooltip'
import type { CommandTooltipHandler } from '../extensions/command-tooltip'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { HrefTooltipContent } from './command-tooltip/href-tooltip' import { HrefTooltipContent } from './command-tooltip/href-tooltip'
import { UrlTooltipContent } from './command-tooltip/url-tooltip' import { UrlTooltipContent } from './command-tooltip/url-tooltip'
@@ -68,14 +70,18 @@ export const CodeMirrorCommandTooltip = memo(function CodeMirrorLinkTooltip() {
} }
return ReactDOM.createPortal( return ReactDOM.createPortal(
<CodeMirrorCommandTooltipContent command={tooltipState.command} />, <CodeMirrorCommandTooltipContent
command={tooltipState.command}
handlers={state.facet(commandTooltipHandlers)}
/>,
tooltipView.dom tooltipView.dom
) )
}) })
const CodeMirrorCommandTooltipContent = memo<{ const CodeMirrorCommandTooltipContent = memo<{
command: string command: string
}>(function CodeMirrorCommandTooltipContent({ command }) { handlers: readonly CommandTooltipHandler[]
}>(function CodeMirrorCommandTooltipContent({ command, handlers }) {
switch (command) { switch (command) {
case 'HrefCommand': case 'HrefCommand':
return <HrefTooltipContent /> return <HrefTooltipContent />
@@ -89,7 +95,10 @@ const CodeMirrorCommandTooltipContent = memo<{
return <InputTooltipContent /> return <InputTooltipContent />
case 'Subfile': case 'Subfile':
return <SubfileTooltipContent /> return <SubfileTooltipContent />
default: default: {
return null // Tooltips contributed by modules via the `commandTooltipHandlers` facet
const handler = handlers.find(handler => handler.command === command)
return handler ? handler.render() : null
}
} }
}) })
@@ -11,11 +11,13 @@ import {
import { import {
EditorSelection, EditorSelection,
EditorState, EditorState,
Facet,
Prec, Prec,
SelectionRange, SelectionRange,
StateEffect, StateEffect,
StateField, StateField,
} from '@codemirror/state' } from '@codemirror/state'
import type { ReactNode } from 'react'
import { ancestorOfNodeWithType } from '../utils/tree-query' import { ancestorOfNodeWithType } from '../utils/tree-query'
import { ensureSyntaxTree, syntaxTree } from '@codemirror/language' import { ensureSyntaxTree, syntaxTree } from '@codemirror/language'
import { import {
@@ -32,12 +34,30 @@ import {
} from '@codemirror/autocomplete' } from '@codemirror/autocomplete'
import { SyntaxNode } from '@lezer/common' import { SyntaxNode } from '@lezer/common'
type ActiveTooltip = { export type ActiveTooltip = {
range: SelectionRange range: SelectionRange
tooltip: Tooltip tooltip: Tooltip
command: string command: string
} | null } | 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 = ( const createTooltipView = (
update?: (update: ViewUpdate) => void update?: (update: ViewUpdate) => void
): TooltipView => { ): TooltipView => {
@@ -47,7 +67,7 @@ const createTooltipView = (
return { dom, update } return { dom, update }
} }
const buildTooltip = ( export const buildTooltip = (
command: string, command: string,
pos: number, pos: number,
value: ActiveTooltip, value: ActiveTooltip,
@@ -89,7 +109,12 @@ const createTooltipState = (
const { main } = state.selection const { main } = state.selection
const pos = main.head const pos = main.head
const node = syntaxTree(state).resolveInner(pos, 0) 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) { if (!commandNode) {
return null return null
} }
@@ -99,6 +124,15 @@ const createTooltipState = (
} }
const commandName = commandNode.name 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) { switch (commandName) {
// a hyperlink (\href) // a hyperlink (\href)
case 'HrefCommand': { case 'HrefCommand': {
@@ -317,7 +351,7 @@ export const commandTooltipState = (
state: EditorState state: EditorState
): ActiveTooltip | undefined => state.field(commandTooltipStateField, false) ): ActiveTooltip | undefined => state.field(commandTooltipStateField, false)
const firstInteractiveElement = ( export const firstInteractiveElement = (
view: EditorView, view: EditorView,
tooltipState: NonNullable<ActiveTooltip> tooltipState: NonNullable<ActiveTooltip>
) => ) =>