feat: add bold/italic shortcuts and toolbar buttons for Typst editor
Build and Deploy Verso / deploy (push) Successful in 14m39s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
claude
2026-06-11 08:28:47 +00:00
parent 4899afd45f
commit 31db7b2b4e
5 changed files with 72 additions and 1 deletions
@@ -12,7 +12,10 @@ import { MathDropdown } from './math-dropdown'
import { InsertListDropdown } from './insert-list-dropdown' import { InsertListDropdown } from './insert-list-dropdown'
import { TableDropdown } from './table-dropdown' import { TableDropdown } from './table-dropdown'
import { LegacyTableDropdown } from './table-inserter-dropdown-legacy' import { LegacyTableDropdown } from './table-inserter-dropdown-legacy'
import { withinFormattingCommand } from '@/features/source-editor/utils/tree-operations/formatting' import {
withinFormattingCommand,
withinTypstFormatting,
} from '@/features/source-editor/utils/tree-operations/formatting'
import { isMac } from '@/shared/utils/os' import { isMac } from '@/shared/utils/os'
import { useProjectContext } from '@/shared/context/project-context' import { useProjectContext } from '@/shared/context/project-context'
import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context' import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context'
@@ -41,6 +44,7 @@ export const ToolbarItems: FC<{
const { features } = useProjectContext() const { features } = useProjectContext()
const permissions = usePermissionsContext() const permissions = usePermissionsContext()
const isActive = withinFormattingCommand(state) const isActive = withinFormattingCommand(state)
const isTypstActive = withinTypstFormatting(state)
const symbolPaletteAvailable = getMeta('ol-symbolPaletteAvailable') const symbolPaletteAvailable = getMeta('ol-symbolPaletteAvailable')
const showGroup = (group: string) => !overflowed || overflowed.has(group) const showGroup = (group: string) => !overflowed || overflowed.has(group)
@@ -189,6 +193,33 @@ export const ToolbarItems: FC<{
)} )}
</> </>
)} )}
{languageName === 'typst' && (
<>
{showGroup('group-format') && (
<div
className="ol-cm-toolbar-button-group"
aria-label={t('toolbar_text_style')}
>
<ToolbarButton
id="toolbar-format-bold"
label={t('toolbar_bold')}
command={commands.toggleTypstBold}
active={isTypstActive('Strong')}
icon="format_bold"
shortcut={isMac ? '⌘B' : 'Ctrl+B'}
/>
<ToolbarButton
id="toolbar-format-italic"
label={t('toolbar_italic')}
command={commands.toggleTypstItalic}
active={isTypstActive('Emphasis')}
icon="format_italic"
shortcut={isMac ? '⌘I' : 'Ctrl+I'}
/>
</div>
)}
</>
)}
</> </>
) )
}) })
@@ -25,6 +25,8 @@ import { sendSearchEvent } from '@/features/event-tracking/search-events'
export const toggleBold = toggleRanges('\\textbf') export const toggleBold = toggleRanges('\\textbf')
export const toggleItalic = toggleRanges('\\textit') export const toggleItalic = toggleRanges('\\textit')
export const toggleTypstBold = wrapRanges('*', '*')
export const toggleTypstItalic = wrapRanges('_', '_')
// TODO: apply as a snippet? // TODO: apply as a snippet?
// TODO: read URL from clipboard? // TODO: read URL from clipboard?
@@ -10,6 +10,7 @@ import { styleTags, tags as t } from '@lezer/highlight'
import { parser } from '../../lezer-typst/typst.mjs' import { parser } from '../../lezer-typst/typst.mjs'
import { typstCompletions } from './complete' import { typstCompletions } from './complete'
import { typstDocumentOutline } from './document-outline' import { typstDocumentOutline } from './document-outline'
import { shortcuts } from './shortcuts'
// Note on tree structure: rules starting with a lowercase letter in the grammar // Note on tree structure: rules starting with a lowercase letter in the grammar
// are inline (no tree node), so their children are promoted to the parent. // are inline (no tree node), so their children are promoted to the parent.
@@ -111,5 +112,6 @@ export const typst = () => {
TypstLanguage.data.of({ autocomplete: typstCompletions }), TypstLanguage.data.of({ autocomplete: typstCompletions }),
typstDocumentOutline, typstDocumentOutline,
syntaxHighlighting(typstHighlightStyle), syntaxHighlighting(typstHighlightStyle),
shortcuts(),
]) ])
} }
@@ -0,0 +1,22 @@
import { Prec } from '@codemirror/state'
import { keymap } from '@codemirror/view'
import { wrapRanges } from '../../commands/ranges'
export const shortcuts = () => {
return Prec.high(
keymap.of([
{
key: 'Ctrl-b',
mac: 'Mod-b',
preventDefault: true,
run: wrapRanges('*', '*'),
},
{
key: 'Ctrl-i',
mac: 'Mod-i',
preventDefault: true,
run: wrapRanges('_', '_'),
},
])
)
}
@@ -5,6 +5,20 @@ import {
matchingAncestor, matchingAncestor,
} from '@/features/source-editor/utils/tree-operations/ancestors' } from '@/features/source-editor/utils/tree-operations/ancestors'
export type TypstFormattingNode = 'Strong' | 'Emphasis'
export const withinTypstFormatting = (state: EditorState) => {
const tree = syntaxTree(state)
return (nodeTypeName: TypstFormattingNode): boolean => {
const isFormatted = (range: SelectionRange): boolean => {
const node = tree.resolveInner(range.from, -1)
return Boolean(matchingAncestor(node, n => n.type.name === nodeTypeName))
}
return state.selection.ranges.every(isFormatted)
}
}
export type FormattingCommand = '\\textbf' | '\\textit' export type FormattingCommand = '\\textbf' | '\\textit'
export type FormattingNodeType = string | number export type FormattingNodeType = string | number