From 31db7b2b4e54f95e31ddd435bab0634a720e2e18 Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 11 Jun 2026 08:28:47 +0000 Subject: [PATCH] feat: add bold/italic shortcuts and toolbar buttons for Typst editor Co-Authored-By: Claude Sonnet 4.6 --- .../components/toolbar/toolbar-items.tsx | 33 ++++++++++++++++++- .../extensions/toolbar/commands.ts | 2 ++ .../source-editor/languages/typst/index.ts | 2 ++ .../languages/typst/shortcuts.ts | 22 +++++++++++++ .../utils/tree-operations/formatting.ts | 14 ++++++++ 5 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 services/web/frontend/js/features/source-editor/languages/typst/shortcuts.ts diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx index 9b7e518a65..925685393a 100644 --- a/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx +++ b/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx @@ -12,7 +12,10 @@ import { MathDropdown } from './math-dropdown' import { InsertListDropdown } from './insert-list-dropdown' import { TableDropdown } from './table-dropdown' 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 { useProjectContext } from '@/shared/context/project-context' import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context' @@ -41,6 +44,7 @@ export const ToolbarItems: FC<{ const { features } = useProjectContext() const permissions = usePermissionsContext() const isActive = withinFormattingCommand(state) + const isTypstActive = withinTypstFormatting(state) const symbolPaletteAvailable = getMeta('ol-symbolPaletteAvailable') const showGroup = (group: string) => !overflowed || overflowed.has(group) @@ -189,6 +193,33 @@ export const ToolbarItems: FC<{ )} )} + {languageName === 'typst' && ( + <> + {showGroup('group-format') && ( +
+ + +
+ )} + + )} ) }) diff --git a/services/web/frontend/js/features/source-editor/extensions/toolbar/commands.ts b/services/web/frontend/js/features/source-editor/extensions/toolbar/commands.ts index 4d05330032..83650d9c4d 100644 --- a/services/web/frontend/js/features/source-editor/extensions/toolbar/commands.ts +++ b/services/web/frontend/js/features/source-editor/extensions/toolbar/commands.ts @@ -25,6 +25,8 @@ import { sendSearchEvent } from '@/features/event-tracking/search-events' export const toggleBold = toggleRanges('\\textbf') export const toggleItalic = toggleRanges('\\textit') +export const toggleTypstBold = wrapRanges('*', '*') +export const toggleTypstItalic = wrapRanges('_', '_') // TODO: apply as a snippet? // TODO: read URL from clipboard? diff --git a/services/web/frontend/js/features/source-editor/languages/typst/index.ts b/services/web/frontend/js/features/source-editor/languages/typst/index.ts index bcdd507e60..b682c872b8 100644 --- a/services/web/frontend/js/features/source-editor/languages/typst/index.ts +++ b/services/web/frontend/js/features/source-editor/languages/typst/index.ts @@ -10,6 +10,7 @@ import { styleTags, tags as t } from '@lezer/highlight' import { parser } from '../../lezer-typst/typst.mjs' import { typstCompletions } from './complete' import { typstDocumentOutline } from './document-outline' +import { shortcuts } from './shortcuts' // 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. @@ -111,5 +112,6 @@ export const typst = () => { TypstLanguage.data.of({ autocomplete: typstCompletions }), typstDocumentOutline, syntaxHighlighting(typstHighlightStyle), + shortcuts(), ]) } diff --git a/services/web/frontend/js/features/source-editor/languages/typst/shortcuts.ts b/services/web/frontend/js/features/source-editor/languages/typst/shortcuts.ts new file mode 100644 index 0000000000..3b4ca1c49b --- /dev/null +++ b/services/web/frontend/js/features/source-editor/languages/typst/shortcuts.ts @@ -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('_', '_'), + }, + ]) + ) +} diff --git a/services/web/frontend/js/features/source-editor/utils/tree-operations/formatting.ts b/services/web/frontend/js/features/source-editor/utils/tree-operations/formatting.ts index dffdd246f5..1755feb8de 100644 --- a/services/web/frontend/js/features/source-editor/utils/tree-operations/formatting.ts +++ b/services/web/frontend/js/features/source-editor/utils/tree-operations/formatting.ts @@ -5,6 +5,20 @@ import { matchingAncestor, } 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 FormattingNodeType = string | number