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