diff --git a/services/web/frontend/js/features/source-editor/components/codemirror-command-tooltip.tsx b/services/web/frontend/js/features/source-editor/components/codemirror-command-tooltip.tsx index cb31899c25..203b42c148 100644 --- a/services/web/frontend/js/features/source-editor/components/codemirror-command-tooltip.tsx +++ b/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( - , + , tooltipView.dom ) }) const CodeMirrorCommandTooltipContent = memo<{ command: string -}>(function CodeMirrorCommandTooltipContent({ command }) { + handlers: readonly CommandTooltipHandler[] +}>(function CodeMirrorCommandTooltipContent({ command, handlers }) { switch (command) { case 'HrefCommand': return @@ -89,7 +95,10 @@ const CodeMirrorCommandTooltipContent = memo<{ return case 'Subfile': return - 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 + } } }) diff --git a/services/web/frontend/js/features/source-editor/extensions/command-tooltip.ts b/services/web/frontend/js/features/source-editor/extensions/command-tooltip.ts index 567374bdf0..b24f0b948a 100644 --- a/services/web/frontend/js/features/source-editor/extensions/command-tooltip.ts +++ b/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() + 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 ) =>