Browse Source

Merge pull request #8432 from weseek/126524-139354-support-keybind

feat: Support Vim/Emacs/VS Code keybind
Yuki Takei 2 years ago
parent
commit
3286917305

BIN
apps/app/public/images/icons/sublime.png


BIN
apps/app/public/images/icons/vscode.png


+ 2 - 2
apps/app/src/components/PageEditor/OptionsSelector.tsx

@@ -10,7 +10,7 @@ import {
 import { useIsIndentSizeForced } from '~/stores/context';
 import { useEditorSettings, useCurrentIndentSize } from '~/stores/editor';
 
-import { DEFAULT_THEME, KeyMapMode } from '../../interfaces/editor-settings';
+import { DEFAULT_THEME, type KeyMapMode } from '../../interfaces/editor-settings';
 
 
 const AVAILABLE_THEMES = [
@@ -73,7 +73,7 @@ const KEYMAP_LABEL_MAP: KeyMapModeToLabel = {
   default: 'Default',
   vim: 'Vim',
   emacs: 'Emacs',
-  sublime: 'Sublime Text',
+  vscode: 'Visual Studio Code',
 };
 
 const KeymapSelector = memo((): JSX.Element => {

+ 1 - 0
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -502,6 +502,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
             initialValue={initialValue}
             onOpenEditor={markdown => setMarkdownToPreview(markdown)}
             editorTheme={editorSettings?.theme}
+            editorKeymap={editorSettings?.keymapMode}
           />
         </div>
         <div ref={previewRef} onScroll={scrollPreviewHandlerThrottle} className="page-editor-preview-container flex-expand-vert d-none d-lg-flex">

+ 1 - 1
apps/app/src/interfaces/editor-settings.ts

@@ -4,7 +4,7 @@ const KeyMapMode = {
   default: 'default',
   vim: 'vim',
   emacs: 'emacs',
-  sublime: 'sublime',
+  vscode: 'vscode',
 } as const;
 
 export type KeyMapMode = typeof KeyMapMode[keyof typeof KeyMapMode];

+ 3 - 0
packages/editor/package.json

@@ -27,6 +27,9 @@
     "@codemirror/state": "^6.2.1",
     "@codemirror/view": "^6.15.3",
     "@popperjs/core": "^2.11.8",
+    "@replit/codemirror-emacs": "^6.0.1",
+    "@replit/codemirror-vim": "6.0.14",
+    "@replit/codemirror-vscode-keymap": "^6.0.2",
     "@types/react": "^18.2.14",
     "@types/react-dom": "^18.2.6",
     "@uiw/codemirror-theme-eclipse": "^4.21.21",

+ 16 - 1
packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx

@@ -9,7 +9,7 @@ import type { ReactCodeMirrorProps } from '@uiw/react-codemirror';
 
 import { GlobalCodeMirrorEditorKey, AcceptedUploadFileType } from '../../consts';
 import {
-  useFileDropzone, FileDropzoneOverlay, getEditorTheme, type EditorTheme,
+  useFileDropzone, FileDropzoneOverlay, getEditorTheme, type EditorTheme, getKeyMap, type KeyMapMode,
 } from '../../services';
 import {
   adjustPasteData, getStrFromBol,
@@ -31,10 +31,12 @@ type Props = {
   editorKey: string | GlobalCodeMirrorEditorKey,
   acceptedFileType: AcceptedUploadFileType,
   onChange?: (value: string) => void,
+  onSave?: () => void,
   onUpload?: (files: File[]) => void,
   onScroll?: () => void,
   indentSize?: number,
   editorTheme?: string,
+  editorKeymap?: string,
 }
 
 export const CodeMirrorEditor = (props: Props): JSX.Element => {
@@ -42,10 +44,12 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
     editorKey,
     acceptedFileType,
     onChange,
+    onSave,
     onUpload,
     onScroll,
     indentSize,
     editorTheme,
+    editorKeymap,
   } = props;
 
   const containerRef = useRef(null);
@@ -160,6 +164,17 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
     return cleanupFunction;
   }, [codeMirrorEditor, themeExtension]);
 
+
+  useEffect(() => {
+    const keymap = (editorKeymap ?? 'default') as KeyMapMode;
+    const extension = getKeyMap(keymap, onSave);
+
+    // Prevent these Keybind from overwriting the originally defined keymap.
+    const cleanupFunction = codeMirrorEditor?.appendExtensions(Prec.low(extension));
+    return cleanupFunction;
+
+  }, [codeMirrorEditor, editorKeymap, onSave]);
+
   const {
     getRootProps,
     isDragActive,

+ 4 - 1
packages/editor/src/components/CodeMirrorEditorMain.tsx

@@ -28,11 +28,12 @@ type Props = {
   initialValue?: string,
   onOpenEditor?: (markdown: string) => void,
   editorTheme?: string,
+  editorKeymap?: string,
 }
 
 export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
   const {
-    onSave, onChange, onUpload, onScroll, acceptedFileType, indentSize, userName, pageId, initialValue, onOpenEditor, editorTheme,
+    onSave, onChange, onUpload, onScroll, acceptedFileType, indentSize, userName, pageId, initialValue, onOpenEditor, editorTheme, editorKeymap,
   } = props;
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
@@ -76,11 +77,13 @@ export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
     <CodeMirrorEditor
       editorKey={GlobalCodeMirrorEditorKey.MAIN}
       onChange={onChange}
+      onSave={onSave}
       onUpload={onUpload}
       onScroll={onScroll}
       acceptedFileType={acceptedFileTypeNoOpt}
       indentSize={indentSize}
       editorTheme={editorTheme}
+      editorKeymap={editorKeymap}
     />
   );
 };

+ 3 - 1
packages/editor/src/components/playground/Playground.tsx

@@ -15,6 +15,7 @@ export const Playground = (): JSX.Element => {
 
   const [markdownToPreview, setMarkdownToPreview] = useState('');
   const [editorTheme, setEditorTheme] = useState('');
+  const [editorKeymap, setEditorKeymap] = useState('');
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
 
@@ -63,11 +64,12 @@ export const Playground = (): JSX.Element => {
             indentSize={4}
             acceptedFileType={AcceptedUploadFileType.ALL}
             editorTheme={editorTheme}
+            editorKeymap={editorKeymap}
           />
         </div>
         <div className="flex-expand-vert d-none d-lg-flex bg-light text-dark border-start border-dark-subtle p-3">
           <Preview markdown={markdownToPreview} />
-          <PlaygroundController setEditorTheme={setEditorTheme} />
+          <PlaygroundController setEditorTheme={setEditorTheme} setEditorKeymap={setEditorKeymap} />
         </div>
       </div>
       <div className="flex-expand-vert justify-content-center align-items-center bg-dark" style={{ minHeight: '50px' }}>

+ 27 - 28
packages/editor/src/components/playground/PlaygroundController.tsx

@@ -3,7 +3,7 @@ import { useCallback } from 'react';
 import { useForm } from 'react-hook-form';
 
 import { GlobalCodeMirrorEditorKey } from '../../consts';
-import { AllEditorTheme } from '../../services';
+import { AllEditorTheme, AllKeyMap } from '../../services';
 import { useCodeMirrorEditorIsolated } from '../../stores';
 
 export const InitEditorValueRow = (): JSX.Element => {
@@ -69,38 +69,35 @@ export const SetCaretLineRow = (): JSX.Element => {
   );
 };
 
-type SetThemeRowProps = {
-  setEditorTheme: (value: string) => void,
+
+type SetParamRowProps = {
+    update: (value: string) => void,
+    items: string[],
 }
-const SetThemeRow = (props: SetThemeRowProps): JSX.Element => {
-
-  const { setEditorTheme } = props;
-
-  const createItems = (items: string[]): JSX.Element => {
-    return (
-      <div>
-        { items.map((theme) => {
-          return (
-            <button
-              type="button"
-              className="btn btn-outline-secondary"
-              onClick={() => {
-                setEditorTheme(theme);
-              }}
-            >{theme}
-            </button>
-          );
-        }) }
-      </div>
-    );
-  };
 
+const SetParamRow = (
+    props: SetParamRowProps,
+): JSX.Element => {
+  const { update, items } = props;
   return (
     <>
       <div className="row mt-3">
         <h2>default</h2>
         <div className="col">
-          {createItems(AllEditorTheme)}
+          <div>
+            { items.map((item) => {
+              return (
+                <button
+                  type="button"
+                  className="btn btn-outline-secondary"
+                  onClick={() => {
+                    update(item);
+                  }}
+                >{item}
+                </button>
+              );
+            }) }
+          </div>
         </div>
       </div>
     </>
@@ -110,15 +107,17 @@ const SetThemeRow = (props: SetThemeRowProps): JSX.Element => {
 
 type PlaygroundControllerProps = {
   setEditorTheme: (value: string) => void
+  setEditorKeymap: (value: string) => void
 };
 
 export const PlaygroundController = (props: PlaygroundControllerProps): JSX.Element => {
-  const { setEditorTheme } = props;
+  const { setEditorTheme, setEditorKeymap } = props;
   return (
     <div className="container mt-5">
       <InitEditorValueRow />
       <SetCaretLineRow />
-      <SetThemeRow setEditorTheme={setEditorTheme} />
+      <SetParamRow update={setEditorTheme} items={AllEditorTheme} />
+      <SetParamRow update={setEditorKeymap} items={AllKeyMap} />
     </div>
   );
 };

+ 1 - 0
packages/editor/src/services/index.ts

@@ -1,3 +1,4 @@
 export * from './codemirror-editor';
 export * from './file-dropzone';
 export * from './editor-theme';
+export * from './keymaps';

+ 31 - 0
packages/editor/src/services/keymaps/index.ts

@@ -0,0 +1,31 @@
+import { defaultKeymap } from '@codemirror/commands';
+import { Extension } from '@codemirror/state';
+import { keymap } from '@codemirror/view';
+import { emacs } from '@replit/codemirror-emacs';
+import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
+
+import { vimKeymap } from './vim';
+
+
+export const getKeyMap = (keyMapName: KeyMapMode, onSave?: () => void): Extension => {
+  switch (keyMapName) {
+    case 'vim':
+      return vimKeymap(onSave);
+    case 'emacs':
+      return emacs();
+    case 'vscode':
+      return keymap.of(vscodeKeymap);
+    case 'default':
+      return keymap.of(defaultKeymap);
+  }
+};
+
+const KeyMapMode = {
+  default: 'default',
+  vim: 'vim',
+  emacs: 'emacs',
+  vscode: 'vscode',
+} as const;
+
+export const AllKeyMap = Object.values(KeyMapMode);
+export type KeyMapMode = typeof KeyMapMode[keyof typeof KeyMapMode];

+ 13 - 0
packages/editor/src/services/keymaps/vim.ts

@@ -0,0 +1,13 @@
+import { Extension } from '@codemirror/state';
+import { Vim, vim } from '@replit/codemirror-vim';
+
+// vim useful keymap custom
+Vim.map('jj', '<Esc>', 'insert');
+Vim.map('jk', '<Esc>', 'insert');
+
+export const vimKeymap = (onSave?: () => void): Extension => {
+  if (onSave != null) {
+    Vim.defineEx('write', 'w', onSave);
+  }
+  return vim();
+};

+ 15 - 0
yarn.lock

@@ -2858,6 +2858,21 @@
   resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a"
   integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==
 
+"@replit/codemirror-emacs@^6.0.1":
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/@replit/codemirror-emacs/-/codemirror-emacs-6.0.1.tgz#6e74453e456f40cbb18ed1d15030fa0dbd218098"
+  integrity sha512-2WYkODZGH1QVAXWuOxTMCwktkoZyv/BjYdJi2A5w4fRrmOQFuIACzb6pO9dgU3J+Pm2naeiX2C8veZr/3/r6AA==
+
+"@replit/codemirror-vim@6.0.14":
+  version "6.0.14"
+  resolved "https://registry.yarnpkg.com/@replit/codemirror-vim/-/codemirror-vim-6.0.14.tgz#8f44740b0497406b551726946c9b30f21c867671"
+  integrity sha512-wwhqhvL76FdRTdwfUWpKCbv0hkp2fvivfMosDVlL/popqOiNLtUhL02ThgHZH8mus/NkVr5Mj582lyFZqQrjOA==
+
+"@replit/codemirror-vscode-keymap@^6.0.2":
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/@replit/codemirror-vscode-keymap/-/codemirror-vscode-keymap-6.0.2.tgz#cc9b9092db5afb9800fda5a03801b4f6600b427e"
+  integrity sha512-j45qTwGxzpsv82lMD/NreGDORFKSctMDVkGRopaP+OrzSzv+pXDQuU3LnFvKpasyjVT0lf+PKG1v2DSCn/vxxg==
+
 "@restart/hooks@^0.3.26":
   version "0.3.27"
   resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.3.27.tgz#91f356d66d4699a8cd8b3d008402708b6a9dc505"