diff --git a/docs/website/documentation/notes/index.md b/docs/website/documentation/notes/index.md
index b4e22aa3..fd7402c0 100644
--- a/docs/website/documentation/notes/index.md
+++ b/docs/website/documentation/notes/index.md
@@ -56,12 +56,23 @@ The editor is built on CodeMirror 6 and includes:
- Syntax highlighting for fenced code blocks
- Smart list indentation with automatic ordered list renumbering
- Tab / Shift-Tab indentation
+- Inline markdown formatting shortcuts in editable modes
- Table navigation between cells
- Internal links to notes and snippets
- Mermaid diagram support
- Image embedding
- Callout blocks
+## Formatting Shortcuts
+
+The following shortcuts work in **Editor** and **Live Preview** modes:
+
+- Cmd+B / Ctrl+B for **bold**
+- Cmd+I / Ctrl+I for *italic*
+- Cmd+Shift+S / Ctrl+Shift+S for ~~strikethrough~~
+
+Press the same shortcut again to remove the markdown markers from the current selection.
+
For visual diagrams in notes, see [Mermaid](/documentation/notes/mermaid).
For wiki-style links between notes and snippets, see [Internal Links](/documentation/notes/internal-links).
diff --git a/src/renderer/components/notes/NotesEditor.vue b/src/renderer/components/notes/NotesEditor.vue
index 306c3d7c..86c017a1 100644
--- a/src/renderer/components/notes/NotesEditor.vue
+++ b/src/renderer/components/notes/NotesEditor.vue
@@ -32,6 +32,7 @@ import { createInternalLinks } from './cm-extensions/internalLinks'
import { listIndent } from './cm-extensions/listIndent'
import { createListLineIndent } from './cm-extensions/listLineIndent'
import { createMarkdownDecorations } from './cm-extensions/markdownDecorations'
+import { markdownShortcuts } from './cm-extensions/markdownShortcuts'
import { createMermaidBlocks } from './cm-extensions/mermaidBlocks'
import { moveSelectionToAdjacentMermaidSource } from './cm-extensions/mermaidNavigation'
import { notesEditorScrollbarTheme } from './cm-extensions/scrollbarTheme'
@@ -177,6 +178,7 @@ function createEditorState(doc: string): EditorState {
history(),
Prec.highest(keymap.of(editable && !raw ? listIndent : [])),
keymap.of([
+ ...(editable ? markdownShortcuts : []),
...(editable && !raw ? navigationKeymap : []),
...defaultKeymap,
...historyKeymap,
diff --git a/src/renderer/components/notes/cm-extensions/__tests__/markdownShortcuts.test.ts b/src/renderer/components/notes/cm-extensions/__tests__/markdownShortcuts.test.ts
new file mode 100644
index 00000000..635527f0
--- /dev/null
+++ b/src/renderer/components/notes/cm-extensions/__tests__/markdownShortcuts.test.ts
@@ -0,0 +1,64 @@
+import { describe, expect, it } from 'vitest'
+import { markdownShortcuts, toggleInlineMarkdown } from '../markdownShortcuts'
+
+describe('toggleInlineMarkdown', () => {
+ it('wraps selected text with bold markers', () => {
+ expect(toggleInlineMarkdown('hello world', 0, 5, '**')).toEqual({
+ selection: {
+ from: 2,
+ to: 7,
+ },
+ text: '**hello** world',
+ })
+ })
+
+ it('unwraps selected text when bold markers are adjacent to the selection', () => {
+ expect(toggleInlineMarkdown('**hello** world', 2, 7, '**')).toEqual({
+ selection: {
+ from: 0,
+ to: 5,
+ },
+ text: 'hello world',
+ })
+ })
+
+ it('unwraps selected text when the selection already includes the markers', () => {
+ expect(toggleInlineMarkdown('**hello** world', 0, 9, '**')).toEqual({
+ selection: {
+ from: 0,
+ to: 5,
+ },
+ text: 'hello world',
+ })
+ })
+
+ it('inserts italic markers around an empty selection and places cursor between them', () => {
+ expect(toggleInlineMarkdown('hello', 5, 5, '*')).toEqual({
+ selection: {
+ from: 6,
+ to: 6,
+ },
+ text: 'hello**',
+ })
+ })
+
+ it('wraps selected text with strikethrough markers', () => {
+ expect(toggleInlineMarkdown('hello world', 6, 11, '~~')).toEqual({
+ selection: {
+ from: 8,
+ to: 13,
+ },
+ text: 'hello ~~world~~',
+ })
+ })
+})
+
+describe('markdownShortcuts', () => {
+ it('registers bold, italic, and strikethrough shortcuts', () => {
+ expect(markdownShortcuts.map(binding => binding.key)).toEqual([
+ 'Mod-b',
+ 'Mod-i',
+ 'Mod-Shift-s',
+ ])
+ })
+})
diff --git a/src/renderer/components/notes/cm-extensions/markdownShortcuts.ts b/src/renderer/components/notes/cm-extensions/markdownShortcuts.ts
new file mode 100644
index 00000000..bc329b59
--- /dev/null
+++ b/src/renderer/components/notes/cm-extensions/markdownShortcuts.ts
@@ -0,0 +1,154 @@
+import type { KeyBinding } from '@codemirror/view'
+import { EditorSelection } from '@codemirror/state'
+
+interface SelectionRange {
+ from: number
+ to: number
+}
+
+interface InlineMarkdownEdit {
+ change: {
+ from: number
+ insert: string
+ to: number
+ }
+ selection: SelectionRange
+}
+
+function createInlineMarkdownEdit(
+ text: string,
+ from: number,
+ to: number,
+ marker: string,
+): InlineMarkdownEdit {
+ const selection = text.slice(from, to)
+ const markerLength = marker.length
+
+ if (from === to) {
+ return {
+ change: {
+ from,
+ insert: `${marker}${marker}`,
+ to,
+ },
+ selection: {
+ from: from + markerLength,
+ to: from + markerLength,
+ },
+ }
+ }
+
+ if (
+ selection.length >= markerLength * 2
+ && selection.startsWith(marker)
+ && selection.endsWith(marker)
+ ) {
+ const unwrapped = selection.slice(
+ markerLength,
+ selection.length - markerLength,
+ )
+
+ return {
+ change: {
+ from,
+ insert: unwrapped,
+ to,
+ },
+ selection: {
+ from,
+ to: from + unwrapped.length,
+ },
+ }
+ }
+
+ const hasAdjacentMarkers
+ = from >= markerLength
+ && text.slice(from - markerLength, from) === marker
+ && text.slice(to, to + markerLength) === marker
+
+ if (hasAdjacentMarkers) {
+ return {
+ change: {
+ from: from - markerLength,
+ insert: selection,
+ to: to + markerLength,
+ },
+ selection: {
+ from: from - markerLength,
+ to: from - markerLength + selection.length,
+ },
+ }
+ }
+
+ return {
+ change: {
+ from,
+ insert: `${marker}${selection}${marker}`,
+ to,
+ },
+ selection: {
+ from: from + markerLength,
+ to: to + markerLength,
+ },
+ }
+}
+
+export function toggleInlineMarkdown(
+ text: string,
+ from: number,
+ to: number,
+ marker: string,
+) {
+ const edit = createInlineMarkdownEdit(text, from, to, marker)
+
+ return {
+ selection: edit.selection,
+ text:
+ text.slice(0, edit.change.from)
+ + edit.change.insert
+ + text.slice(edit.change.to),
+ }
+}
+
+function createInlineMarkdownCommand(marker: string) {
+ return (view: Parameters>[0]) => {
+ const docText = view.state.doc.toString()
+ const change = view.state.changeByRange((range) => {
+ const edit = createInlineMarkdownEdit(
+ docText,
+ range.from,
+ range.to,
+ marker,
+ )
+
+ return {
+ changes: edit.change,
+ range: EditorSelection.range(edit.selection.from, edit.selection.to),
+ }
+ })
+
+ view.dispatch(
+ view.state.update(change, {
+ scrollIntoView: true,
+ userEvent: 'input',
+ }),
+ )
+
+ return true
+ }
+}
+
+export const markdownShortcuts: KeyBinding[] = [
+ {
+ key: 'Mod-b',
+ run: createInlineMarkdownCommand('**'),
+ },
+ {
+ key: 'Mod-i',
+ run: createInlineMarkdownCommand('*'),
+ },
+ {
+ key: 'Mod-Shift-s',
+ run: createInlineMarkdownCommand('~~'),
+ },
+]