Browse Source

Merge pull request #9512 from weseek/feat/97800-159429-add-shortcuts-when-editing

feat: Add shortcuts when editing
Yuki Takei 9 months ago
parent
commit
9dfcf6b183
16 changed files with 582 additions and 28 deletions
  1. 2 2
      packages/editor/src/client/services/use-codemirror-editor/use-codemirror-editor.ts
  2. 62 0
      packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/add-multi-cursor.ts
  3. 31 0
      packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/generate-add-markdown-symbol-command.ts
  4. 17 0
      packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/insert-blockquote.ts
  5. 17 0
      packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/insert-bullet-list.ts
  6. 17 0
      packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/insert-link.ts
  7. 17 0
      packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/insert-numbered-list.ts
  8. 24 0
      packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/make-text-bold.ts
  9. 84 0
      packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/make-text-code-block.ts
  10. 17 0
      packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/make-text-code.ts
  11. 17 0
      packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/make-text-italic.ts
  12. 17 0
      packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/make-text-strikethrough.ts
  13. 39 10
      packages/editor/src/client/services/use-codemirror-editor/utils/insert-markdown-elements.ts
  14. 143 16
      packages/editor/src/client/services/use-codemirror-editor/utils/insert-prefix.ts
  15. 3 0
      packages/editor/src/client/stores/use-editor-settings.ts
  16. 75 0
      packages/editor/src/client/stores/use-editor-shortcuts.ts

+ 2 - 2
packages/editor/src/client/services/use-codemirror-editor/use-codemirror-editor.ts

@@ -14,7 +14,7 @@ import { useFoldDrawio } from './utils/fold-drawio';
 import type { GetDocString } from './utils/get-doc';
 import { useGetDoc, type GetDoc, useGetDocString } from './utils/get-doc';
 import { useInitDoc, type InitDoc } from './utils/init-doc';
-import { useInsertMarkdownElements, type InsertMarkdowElements } from './utils/insert-markdown-elements';
+import { useInsertMarkdownElements, type InsertMarkdownElements } from './utils/insert-markdown-elements';
 import { useInsertPrefix, type InsertPrefix } from './utils/insert-prefix';
 import { useInsertText, type InsertText } from './utils/insert-text';
 import { useReplaceText, type ReplaceText } from './utils/replace-text';
@@ -30,7 +30,7 @@ type UseCodeMirrorEditorUtils = {
   setCaretLine: SetCaretLine,
   insertText: InsertText,
   replaceText: ReplaceText,
-  insertMarkdownElements: InsertMarkdowElements,
+  insertMarkdownElements: InsertMarkdownElements,
   insertPrefix: InsertPrefix,
   foldDrawio: FoldDrawio,
 }

+ 62 - 0
packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/add-multi-cursor.ts

@@ -0,0 +1,62 @@
+import { useCallback } from 'react';
+
+import type { SelectionRange } from '@codemirror/state';
+import { EditorSelection } from '@codemirror/state';
+import type { EditorView, Command, KeyBinding } from '@codemirror/view';
+
+
+const addMultiCursor = (view: EditorView, direction: 'up' | 'down') => {
+
+  const selection = view.state.selection;
+  const doc = view.state.doc;
+  const ranges = selection.ranges;
+  const newRanges: SelectionRange[] = [];
+
+  ranges.forEach((range) => {
+
+    const head = range.head;
+    const line = doc.lineAt(head);
+    const targetLine = direction === 'up' ? line.number - 1 : line.number + 1;
+
+    if (targetLine < 1 || targetLine > doc.lines) return;
+
+    const targetLineText = doc.line(targetLine);
+
+    const col = Math.min(range.head - line.from, targetLineText.length);
+    const cursorPos = targetLineText.from + col;
+
+    newRanges.push(EditorSelection.cursor(cursorPos));
+
+  });
+
+  if (newRanges.length) {
+    const transaction = {
+      selection: EditorSelection.create([...ranges, ...newRanges]),
+    };
+
+    view.dispatch(transaction);
+  }
+
+  return true;
+};
+
+const useAddMultiCursorCommand = (direction: 'up' | 'down'): Command => {
+  return useCallback((view?: EditorView) => {
+    if (view == null) return false;
+    addMultiCursor(view, direction);
+    return true;
+  }, [direction]);
+};
+
+export const useAddMultiCursorKeyBindings = (): KeyBinding[] => {
+
+  const upMultiCursorCommand = useAddMultiCursorCommand('up');
+  const downMultiCursorCommand = useAddMultiCursorCommand('down');
+
+  const keyBindings = [
+    { key: 'mod-alt-ArrowUp', run: upMultiCursorCommand },
+    { key: 'mod-alt-ArrowDown', run: downMultiCursorCommand },
+  ];
+
+  return keyBindings;
+};

+ 31 - 0
packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/generate-add-markdown-symbol-command.ts

@@ -0,0 +1,31 @@
+import type { Command } from '@codemirror/view';
+
+import type { InsertMarkdownElements } from '../insert-markdown-elements';
+import type { InsertPrefix } from '../insert-prefix';
+
+export const generateAddMarkdownSymbolCommand = (
+    insertMarkdown: InsertMarkdownElements | InsertPrefix,
+    prefix: string,
+    suffix?: string,
+): Command => {
+
+  const isInsertMarkdownElements = (
+      fn: InsertMarkdownElements | InsertPrefix,
+  ): fn is InsertMarkdownElements => {
+    return fn.length === 2;
+  };
+
+  const addMarkdownSymbolCommand: Command = () => {
+    if (isInsertMarkdownElements(insertMarkdown)) {
+      if (suffix == null) return false;
+      insertMarkdown(prefix, suffix);
+    }
+    else {
+      insertMarkdown(prefix);
+    }
+
+    return true;
+  };
+
+  return addMarkdownSymbolCommand;
+};

+ 17 - 0
packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/insert-blockquote.ts

@@ -0,0 +1,17 @@
+import type { EditorView, KeyBinding } from '@codemirror/view';
+
+import { useInsertPrefix } from '../insert-prefix';
+
+import { generateAddMarkdownSymbolCommand } from './generate-add-markdown-symbol-command';
+
+
+export const useInsertBlockquoteKeyBinding = (view?: EditorView): KeyBinding => {
+
+  const insertPrefix = useInsertPrefix(view);
+
+  const insertBlockquoteCommand = generateAddMarkdownSymbolCommand(insertPrefix, '>');
+
+  const insertBlockquoteKeyBinding = { key: 'mod-shift-9', run: insertBlockquoteCommand };
+
+  return insertBlockquoteKeyBinding;
+};

+ 17 - 0
packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/insert-bullet-list.ts

@@ -0,0 +1,17 @@
+import type { EditorView, KeyBinding } from '@codemirror/view';
+
+import { useInsertPrefix } from '../insert-prefix';
+
+import { generateAddMarkdownSymbolCommand } from './generate-add-markdown-symbol-command';
+
+
+export const useInsertBulletListKeyBinding = (view?: EditorView): KeyBinding => {
+
+  const insertPrefix = useInsertPrefix(view);
+
+  const insertBulletListCommand = generateAddMarkdownSymbolCommand(insertPrefix, '-');
+
+  const insertBulletListKeyBinding = { key: 'mod-shift-8', run: insertBulletListCommand };
+
+  return insertBulletListKeyBinding;
+};

+ 17 - 0
packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/insert-link.ts

@@ -0,0 +1,17 @@
+import type { EditorView, KeyBinding } from '@codemirror/view';
+
+import { useInsertMarkdownElements } from '../insert-markdown-elements';
+
+import { generateAddMarkdownSymbolCommand } from './generate-add-markdown-symbol-command';
+
+
+export const useInsertLinkKeyBinding = (view?: EditorView): KeyBinding => {
+
+  const insertMarkdownElements = useInsertMarkdownElements(view);
+
+  const InsertLinkCommand = generateAddMarkdownSymbolCommand(insertMarkdownElements, '[', ']()');
+
+  const InsertLinkKeyBinding = { key: 'mod-shift-u', run: InsertLinkCommand };
+
+  return InsertLinkKeyBinding;
+};

+ 17 - 0
packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/insert-numbered-list.ts

@@ -0,0 +1,17 @@
+import type { EditorView, KeyBinding } from '@codemirror/view';
+
+import { useInsertPrefix } from '../insert-prefix';
+
+import { generateAddMarkdownSymbolCommand } from './generate-add-markdown-symbol-command';
+
+
+export const useInsertNumberedKeyBinding = (view?: EditorView): KeyBinding => {
+
+  const insertPrefix = useInsertPrefix(view);
+
+  const insertNumberedCommand = generateAddMarkdownSymbolCommand(insertPrefix, '1.');
+
+  const insertNumberedKeyBinding = { key: 'mod-shift-7', run: insertNumberedCommand };
+
+  return insertNumberedKeyBinding;
+};

+ 24 - 0
packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/make-text-bold.ts

@@ -0,0 +1,24 @@
+import type { EditorView, KeyBinding } from '@codemirror/view';
+
+import { useInsertMarkdownElements } from '../insert-markdown-elements';
+
+import { generateAddMarkdownSymbolCommand } from './generate-add-markdown-symbol-command';
+
+import type { KeyMapMode } from 'src/consts';
+
+
+export const useMakeTextBoldKeyBinding = (view?: EditorView, keyMapName?: KeyMapMode): KeyBinding => {
+
+  const insertMarkdownElements = useInsertMarkdownElements(view);
+
+  let makeTextBoldKeyBinding: KeyBinding;
+  switch (keyMapName) {
+    case 'vim':
+      makeTextBoldKeyBinding = { key: 'mod-shift-b', run: generateAddMarkdownSymbolCommand(insertMarkdownElements, '**', '**') };
+      break;
+    default:
+      makeTextBoldKeyBinding = { key: 'mod-b', run: generateAddMarkdownSymbolCommand(insertMarkdownElements, '**', '**') };
+  }
+
+  return makeTextBoldKeyBinding;
+};

+ 84 - 0
packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/make-text-code-block.ts

@@ -0,0 +1,84 @@
+import { EditorSelection } from '@codemirror/state';
+import type { Extension, ChangeSpec, SelectionRange } from '@codemirror/state';
+import type { Command } from '@codemirror/view';
+import { EditorView } from '@codemirror/view';
+
+const makeTextCodeBlock: Command = (view: EditorView) => {
+  const state = view.state;
+  const doc = state.doc;
+  const changes: ChangeSpec[] = [];
+  const newSelections: SelectionRange[] = [];
+
+  state.selection.ranges.forEach((range) => {
+    const startLine = doc.lineAt(range.from);
+    const endLine = doc.lineAt(range.to);
+    const selectedText = doc.sliceString(range.from, range.to, '');
+    const isAlreadyWrapped = selectedText.startsWith('```') && selectedText.endsWith('```');
+
+    const codeBlockMarkerLength = 4;
+
+    if (isAlreadyWrapped) {
+      const startMarkerEnd = startLine.from + codeBlockMarkerLength;
+      const endMarkerStart = endLine.to - codeBlockMarkerLength;
+
+      changes.push({
+        from: startLine.from,
+        to: startMarkerEnd,
+        insert: '',
+      });
+
+      changes.push({
+        from: endMarkerStart,
+        to: endLine.to,
+        insert: '',
+      });
+
+      newSelections.push(EditorSelection.range(startLine.from, endMarkerStart - codeBlockMarkerLength));
+    }
+    else {
+      // Add code block markers
+      changes.push({
+        from: startLine.from,
+        insert: '```\n',
+      });
+
+      changes.push({
+        from: endLine.to,
+        insert: '\n```',
+      });
+
+      if (selectedText.length === 0) {
+        newSelections.push(EditorSelection.cursor(startLine.from + codeBlockMarkerLength));
+      }
+      else {
+        newSelections.push(EditorSelection.range(startLine.from, endLine.to + codeBlockMarkerLength * 2));
+      }
+    }
+  });
+
+  view.dispatch({
+    changes,
+    selection: EditorSelection.create(newSelections),
+  });
+
+  return true;
+};
+
+const makeCodeBlockExtension: Extension = EditorView.domEventHandlers({
+  keydown: (event, view) => {
+
+    const isModKey = event.ctrlKey || event.metaKey;
+
+    if (event.code === 'KeyC' && event.shiftKey && event.altKey && isModKey) {
+      event.preventDefault();
+      makeTextCodeBlock(view);
+      return true;
+    }
+
+    return false;
+  },
+});
+
+export const useMakeCodeBlockExtension = (): Extension => {
+  return makeCodeBlockExtension;
+};

+ 17 - 0
packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/make-text-code.ts

@@ -0,0 +1,17 @@
+import type { EditorView, KeyBinding } from '@codemirror/view';
+
+import { useInsertMarkdownElements } from '../insert-markdown-elements';
+
+import { generateAddMarkdownSymbolCommand } from './generate-add-markdown-symbol-command';
+
+
+export const useMakeTextCodeKeyBinding = (view?: EditorView): KeyBinding => {
+
+  const insertMarkdownElements = useInsertMarkdownElements(view);
+
+  const makeTextCodeCommand = generateAddMarkdownSymbolCommand(insertMarkdownElements, '`', '`');
+
+  const makeTextCodeKeyBinding = { key: 'mod-shift-c', run: makeTextCodeCommand };
+
+  return makeTextCodeKeyBinding;
+};

+ 17 - 0
packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/make-text-italic.ts

@@ -0,0 +1,17 @@
+import type { EditorView, KeyBinding } from '@codemirror/view';
+
+import { useInsertMarkdownElements } from '../insert-markdown-elements';
+
+import { generateAddMarkdownSymbolCommand } from './generate-add-markdown-symbol-command';
+
+
+export const useMakeTextItalicKeyBinding = (view?: EditorView): KeyBinding => {
+
+  const insertMarkdownElements = useInsertMarkdownElements(view);
+
+  const makeTextItalicCommand = generateAddMarkdownSymbolCommand(insertMarkdownElements, '*', '*');
+
+  const makeTextItalicKeyBinding = { key: 'mod-shift-i', run: makeTextItalicCommand };
+
+  return makeTextItalicKeyBinding;
+};

+ 17 - 0
packages/editor/src/client/services/use-codemirror-editor/utils/editor-shortcuts/make-text-strikethrough.ts

@@ -0,0 +1,17 @@
+import type { EditorView, KeyBinding } from '@codemirror/view';
+
+import { useInsertMarkdownElements } from '../insert-markdown-elements';
+
+import { generateAddMarkdownSymbolCommand } from './generate-add-markdown-symbol-command';
+
+
+export const useMakeTextStrikethroughKeyBinding = (view?: EditorView): KeyBinding => {
+
+  const insertMarkdownElements = useInsertMarkdownElements(view);
+
+  const makeTextStrikethroughCommand = generateAddMarkdownSymbolCommand(insertMarkdownElements, '~~', '~~');
+
+  const makeTextStrikethroughKeyBinding = { key: 'mod-shift-x', run: makeTextStrikethroughCommand };
+
+  return makeTextStrikethroughKeyBinding;
+};

+ 39 - 10
packages/editor/src/client/services/use-codemirror-editor/utils/insert-markdown-elements.ts

@@ -2,26 +2,55 @@ import { useCallback } from 'react';
 
 import type { EditorView } from '@codemirror/view';
 
-export type InsertMarkdowElements = (
+
+export type InsertMarkdownElements = (
   prefix: string,
   suffix: string,
 ) => void;
 
-export const useInsertMarkdownElements = (view?: EditorView): InsertMarkdowElements => {
+const removeSymbol = (text: string, prefix: string, suffix: string): string => {
+  let result = text;
+
+  if (result.startsWith(prefix)) {
+    result = result.slice(prefix.length);
+  }
+
+  if (result.endsWith(suffix)) {
+    result = result.slice(0, -suffix.length);
+  }
+
+  return result;
+};
+
+export const useInsertMarkdownElements = (view?: EditorView): InsertMarkdownElements => {
 
   return useCallback((prefix, suffix) => {
-    const selection = view?.state.sliceDoc(
-      view?.state.selection.main.from,
-      view?.state.selection.main.to,
-    );
+    if (view == null) return;
+
+    const from = view?.state.selection.main.from;
+    const to = view?.state.selection.main.to;
+
+    const selectedText = view?.state.sliceDoc(from, to);
     const cursorPos = view?.state.selection.main.head;
-    const insertText = view?.state.replaceSelection(prefix + selection + suffix);
 
-    if (insertText == null || cursorPos == null) {
+    let insertText: string;
+
+    if (selectedText?.startsWith(prefix) && selectedText?.endsWith(suffix)) {
+      insertText = removeSymbol(selectedText, prefix, suffix);
+    }
+    else {
+      insertText = prefix + selectedText + suffix;
+    }
+
+    const selection = (from === to) ? { anchor: from + prefix.length } : { anchor: from, head: from + insertText.length };
+
+    const transaction = view?.state.replaceSelection(insertText);
+
+    if (transaction == null || cursorPos == null) {
       return;
     }
-    view?.dispatch(insertText);
-    view?.dispatch({ selection: { anchor: cursorPos + prefix.length } });
+    view?.dispatch(transaction);
+    view?.dispatch({ selection });
     view?.focus();
   }, [view]);
 };

+ 143 - 16
packages/editor/src/client/services/use-codemirror-editor/utils/insert-prefix.ts

@@ -1,36 +1,163 @@
 import { useCallback } from 'react';
 
-import type { ChangeSpec } from '@codemirror/state';
+import type { ChangeSpec, Line, Text } from '@codemirror/state';
 import type { EditorView } from '@codemirror/view';
 
 export type InsertPrefix = (prefix: string, noSpaceIfPrefixExists?: boolean) => void;
 
+// https:// regex101.com/r/5ILXUX/1
+const LEADING_SPACES = /^\s*/;
+// https://regex101.com/r/ScAXzy/1
+const createPrefixPattern = (prefix: string) => new RegExp(`^\\s*(${prefix}+)\\s*`);
+
+const removePrefix = (text: string, prefix: string): string => {
+  if (text.startsWith(prefix)) {
+    return text.slice(prefix.length).trimStart();
+  }
+  return text;
+};
+
+const allLinesEmpty = (doc: Text, startLine: Line, endLine: Line) => {
+  for (let i = startLine.number; i <= endLine.number; i++) {
+    const line = doc.line(i);
+    if (line.text.trim() !== '') {
+      return false;
+    }
+  }
+  return true;
+};
+
+const allLinesHavePrefix = (doc: Text, startLine: Line, endLine: Line, prefix: string) => {
+  let hasNonEmptyLine = false;
+
+  for (let i = startLine.number; i <= endLine.number; i++) {
+    const line = doc.line(i);
+    const trimmedLine = line.text.trim();
+
+    if (trimmedLine !== '') {
+      hasNonEmptyLine = true;
+      if (!trimmedLine.startsWith(prefix)) {
+        return false;
+      }
+    }
+  }
+
+  return hasNonEmptyLine;
+};
+
 export const useInsertPrefix = (view?: EditorView): InsertPrefix => {
   return useCallback((prefix: string, noSpaceIfPrefixExists = false) => {
     if (view == null) {
       return;
     }
 
-    // get the line numbers of the selected range
     const { from, to } = view.state.selection.main;
-    const startLine = view.state.doc.lineAt(from);
-    const endLine = view.state.doc.lineAt(to);
+    const doc = view.state.doc;
+    const startLine = doc.lineAt(from);
+    const endLine = doc.lineAt(to);
+
+    const changes: ChangeSpec[] = [];
+    let totalLengthChange = 0;
+
+    const isPrefixRemoval = allLinesHavePrefix(doc, startLine, endLine, prefix);
+
+    if (allLinesEmpty(doc, startLine, endLine)) {
+      for (let i = startLine.number; i <= endLine.number; i++) {
+        const line = view.state.doc.line(i);
+        const leadingSpaces = line.text.match(LEADING_SPACES)?.[0] || '';
+        const insertText = `${leadingSpaces}${prefix} `;
+
+        const change = {
+          from: line.from,
+          to: line.to,
+          insert: insertText,
+        };
+
+        changes.push(change);
+        totalLengthChange += insertText.length - (line.to - line.from);
+      }
+
+      view.dispatch({ changes });
+      view.dispatch({
+        selection: {
+          anchor: from + totalLengthChange,
+          head: to + totalLengthChange,
+        },
+      });
+      view.focus();
+      return;
+    }
 
-    // Insert prefix for each line
-    const lines: ChangeSpec[] = [];
-    let insertTextLength = 0;
     for (let i = startLine.number; i <= endLine.number; i++) {
       const line = view.state.doc.line(i);
-      const insertText = noSpaceIfPrefixExists && line.text.startsWith(prefix)
-        ? prefix
-        : `${prefix} `;
-      insertTextLength += insertText.length;
-      lines.push({ from: line.from, insert: insertText });
+      const trimmedLine = line.text.trim();
+      const leadingSpaces = line.text.match(LEADING_SPACES)?.[0] || '';
+      const contentTrimmed = line.text.trimStart();
+
+      if (trimmedLine === '') {
+        continue;
+      }
+
+      let newLine = '';
+      let lengthChange = 0;
+
+      if (isPrefixRemoval) {
+        const prefixPattern = createPrefixPattern(prefix);
+        const contentStartMatch = line.text.match(prefixPattern);
+
+        if (contentStartMatch) {
+          if (noSpaceIfPrefixExists) {
+            const existingPrefixes = contentStartMatch[1];
+            const indentLevel = Math.floor(leadingSpaces.length / 2) * 2;
+            const newIndent = ' '.repeat(indentLevel);
+            newLine = `${newIndent}${existingPrefixes}${prefix} ${line.text.slice(contentStartMatch[0].length)}`;
+          }
+          else {
+            const indentLevel = Math.floor(leadingSpaces.length / 2) * 2;
+            const newIndent = ' '.repeat(indentLevel);
+            const prefixRemovedText = removePrefix(contentTrimmed, prefix);
+            newLine = `${newIndent}${prefixRemovedText}`;
+          }
+
+          lengthChange = newLine.length - (line.to - line.from);
+
+          changes.push({
+            from: line.from,
+            to: line.to,
+            insert: newLine,
+          });
+        }
+      }
+      else {
+        if (noSpaceIfPrefixExists && contentTrimmed.startsWith(prefix)) {
+          newLine = `${leadingSpaces}${prefix}${contentTrimmed}`;
+        }
+        else {
+          newLine = `${leadingSpaces}${prefix} ${contentTrimmed}`;
+        }
+
+        lengthChange = newLine.length - (line.to - line.from);
+
+        changes.push({
+          from: line.from,
+          to: line.to,
+          insert: newLine,
+        });
+      }
+
+      totalLengthChange += lengthChange;
     }
-    view.dispatch({ changes: lines });
 
-    // move the cursor to the end of the selected line
-    view.dispatch({ selection: { anchor: endLine.to + insertTextLength } });
-    view.focus();
+    if (changes.length > 0) {
+      view.dispatch({ changes });
+
+      view.dispatch({
+        selection: {
+          anchor: from,
+          head: to + totalLengthChange,
+        },
+      });
+      view.focus();
+    }
   }, [view]);
 };

+ 3 - 0
packages/editor/src/client/stores/use-editor-settings.ts

@@ -14,6 +14,8 @@ import {
   getEditorTheme, getKeymap, insertNewlineContinueMarkup, insertNewRowToMarkdownTable, isInTable,
 } from '../services-internal';
 
+import { useEditorShortcuts } from './use-editor-shortcuts';
+
 const useStyleActiveLine = (
     codeMirrorEditor?: UseCodeMirrorEditor,
     styleActiveLine?: boolean,
@@ -100,6 +102,7 @@ export const useEditorSettings = (
     editorSettings?: EditorSettings,
     onSave?: () => void,
 ): void => {
+  useEditorShortcuts(codeMirrorEditor, editorSettings?.keymapMode);
   useStyleActiveLine(codeMirrorEditor, editorSettings?.styleActiveLine);
   useEnterKeyHandler(codeMirrorEditor, editorSettings?.autoFormatMarkdownTable);
   useThemeExtension(codeMirrorEditor, editorSettings?.theme);

+ 75 - 0
packages/editor/src/client/stores/use-editor-shortcuts.ts

@@ -0,0 +1,75 @@
+import { useEffect } from 'react';
+
+import type { EditorView } from '@codemirror/view';
+import {
+  keymap, type KeyBinding,
+} from '@codemirror/view';
+
+import type { UseCodeMirrorEditor } from '../services';
+import { useAddMultiCursorKeyBindings } from '../services/use-codemirror-editor/utils/editor-shortcuts/add-multi-cursor';
+import { useInsertBlockquoteKeyBinding } from '../services/use-codemirror-editor/utils/editor-shortcuts/insert-blockquote';
+import { useInsertBulletListKeyBinding } from '../services/use-codemirror-editor/utils/editor-shortcuts/insert-bullet-list';
+import { useInsertLinkKeyBinding } from '../services/use-codemirror-editor/utils/editor-shortcuts/insert-link';
+import { useInsertNumberedKeyBinding } from '../services/use-codemirror-editor/utils/editor-shortcuts/insert-numbered-list';
+import { useMakeTextBoldKeyBinding } from '../services/use-codemirror-editor/utils/editor-shortcuts/make-text-bold';
+import { useMakeTextCodeKeyBinding } from '../services/use-codemirror-editor/utils/editor-shortcuts/make-text-code';
+import { useMakeCodeBlockExtension } from '../services/use-codemirror-editor/utils/editor-shortcuts/make-text-code-block';
+import { useMakeTextItalicKeyBinding } from '../services/use-codemirror-editor/utils/editor-shortcuts/make-text-italic';
+import { useMakeTextStrikethroughKeyBinding } from '../services/use-codemirror-editor/utils/editor-shortcuts/make-text-strikethrough';
+
+
+import type { KeyMapMode } from 'src/consts';
+
+const useKeyBindings = (view?: EditorView, keymapModeName?: KeyMapMode): KeyBinding[] => {
+
+  const makeTextBoldKeyBinding = useMakeTextBoldKeyBinding(view, keymapModeName);
+  const makeTextItalicKeyBinding = useMakeTextItalicKeyBinding(view);
+  const makeTextStrikethroughKeyBinding = useMakeTextStrikethroughKeyBinding(view);
+  const makeTextCodeCommand = useMakeTextCodeKeyBinding(view);
+  const insertNumberedKeyBinding = useInsertNumberedKeyBinding(view);
+  const insertBulletListKeyBinding = useInsertBulletListKeyBinding(view);
+  const insertBlockquoteKeyBinding = useInsertBlockquoteKeyBinding(view);
+  const InsertLinkKeyBinding = useInsertLinkKeyBinding(view);
+  const multiCursorKeyBindings = useAddMultiCursorKeyBindings();
+
+  const keyBindings: KeyBinding[] = [
+    makeTextBoldKeyBinding,
+    makeTextItalicKeyBinding,
+    makeTextStrikethroughKeyBinding,
+    makeTextCodeCommand,
+    insertNumberedKeyBinding,
+    insertBulletListKeyBinding,
+    insertBlockquoteKeyBinding,
+    InsertLinkKeyBinding,
+    ...multiCursorKeyBindings,
+  ];
+
+  return keyBindings;
+};
+
+export const useEditorShortcuts = (codeMirrorEditor?: UseCodeMirrorEditor, keymapModeName?: KeyMapMode): void => {
+
+  const keyBindings = useKeyBindings(codeMirrorEditor?.view, keymapModeName);
+
+  // Since key combinations of 4 or more keys cannot be implemented with CodeMirror's keybinding, they are implemented as Extensions.
+  const makeCodeBlockExtension = useMakeCodeBlockExtension();
+
+  useEffect(() => {
+    const cleanupFunction = codeMirrorEditor?.appendExtensions?.([makeCodeBlockExtension]);
+    return cleanupFunction;
+  }, [codeMirrorEditor, makeCodeBlockExtension]);
+
+  useEffect(() => {
+
+    if (keyBindings == null) {
+      return;
+    }
+
+    const keyboardShortcutsExtension = keymap.of(keyBindings);
+
+    const cleanupFunction = codeMirrorEditor?.appendExtensions?.(keyboardShortcutsExtension);
+    return cleanupFunction;
+
+  }, [codeMirrorEditor, keyBindings]);
+
+};