Przeglądaj źródła

Merge branch 'dev/7.0.x' into imprv/126529-129067-tab-indent

reiji-h 2 lat temu
rodzic
commit
d52979af19

+ 1 - 1
apps/app/package.json

@@ -172,7 +172,7 @@
     "react-multiline-clamp": "^2.0.0",
     "react-scroll": "^1.8.7",
     "react-syntax-highlighter": "^15.5.0",
-    "react-toastify": "^9.1.1",
+    "react-toastify": "^9.1.3",
     "react-use-ripple": "^1.5.2",
     "reactstrap": "^9.2.0",
     "reconnecting-websocket": "^4.4.0",

+ 64 - 63
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -5,10 +5,11 @@ import React, {
 import EventEmitter from 'events';
 import nodePath from 'path';
 
-
+import { keymap } from '@codemirror/view';
 import type { IPageHasId } from '@growi/core';
 import { pathUtils } from '@growi/core/dist/utils';
 import { CodeMirrorEditorContainer, useCodeMirrorEditorMain } from '@growi/editor';
+import { ReactCodeMirrorProps } from '@uiw/react-codemirror';
 import detectIndent from 'detect-indent';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
@@ -17,7 +18,6 @@ import { throttle, debounce } from 'throttle-debounce';
 import { useUpdateStateAfterSave, useSaveOrUpdate } from '~/client/services/page-operation';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
-import { IEditorMethods } from '~/interfaces/editor-methods';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { SocketEventName } from '~/interfaces/websocket';
 import {
@@ -57,7 +57,6 @@ import loggerFactory from '~/utils/logger';
 import Preview from './Preview';
 import scrollSyncHelper from './ScrollSyncHelper';
 
-
 import '@growi/editor/dist/style.css';
 
 
@@ -115,15 +114,16 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   const { mutate: mutateRemoteRevisionLastUpdatedAt } = useRemoteRevisionLastUpdatedAt();
   const { mutate: mutateRemoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
 
-  const { data: codemirrorEditor } = useCodeMirrorEditorMain(codeMirrorEditorContainerRef.current);
-  const { initDoc, focus: focusToEditor, setCaretLine, setIndentSize } = codemirrorEditor ?? {};
+  const { data: socket } = useGlobalSocket();
 
   const { data: rendererOptions } = usePreviewOptions();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
-  const saveOrUpdate = useSaveOrUpdate();
+  const { mutate: mutateIsConflict } = useIsConflict();
 
+  const saveOrUpdate = useSaveOrUpdate();
   const updateStateAfterSave = useUpdateStateAfterSave(pageId, { supressEditingMarkdownMutation: true });
 
+
   // TODO: remove workaround
   // for https://redmine.weseek.co.jp/issues/125923
   const [createdPageRevisionIdWithAttachment, setCreatedPageRevisionIdWithAttachment] = useState();
@@ -150,10 +150,26 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   }, [isNotFound, currentPathname, editingMarkdown, isEnabledAttachTitleHeader, templateBodyData]);
 
   const [markdownToPreview, setMarkdownToPreview] = useState<string>(initialValue);
-
-  const { data: socket } = useGlobalSocket();
-
-  const { mutate: mutateIsConflict } = useIsConflict();
+  const setMarkdownPreviewWithDebounce = useMemo(() => debounce(100, throttle(150, (value: string) => {
+    setMarkdownToPreview(value);
+  })), []);
+  const mutateIsEnabledUnsavedWarningWithDebounce = useMemo(() => debounce(600, throttle(900, (value: string) => {
+    // Displays an unsaved warning alert
+    mutateIsEnabledUnsavedWarning(value !== initialValue);
+  })), [initialValue, mutateIsEnabledUnsavedWarning]);
+
+  const useCodeMirrorEditorMainProps = useMemo<ReactCodeMirrorProps>(() => {
+    return {
+      onChange: (value) => {
+        setMarkdownPreviewWithDebounce(value);
+        mutateIsEnabledUnsavedWarningWithDebounce(value);
+      },
+    };
+  }, [mutateIsEnabledUnsavedWarningWithDebounce, setMarkdownPreviewWithDebounce]);
+  const { data: codeMirrorEditor } = useCodeMirrorEditorMain(
+    codeMirrorEditorContainerRef.current,
+    useCodeMirrorEditorMainProps,
+  );
 
 
   const checkIsConflict = useCallback((data) => {
@@ -197,17 +213,6 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     return optionsToSave;
   }, [grantData, isSlackEnabled, pageTags]);
 
-  const setMarkdownWithDebounce = useMemo(() => debounce(100, throttle(150, (value: string, isClean: boolean) => {
-    setMarkdownToPreview(value);
-
-    // Displays an unsaved warning alert
-    mutateIsEnabledUnsavedWarning(!isClean);
-  })), [mutateIsEnabledUnsavedWarning]);
-
-
-  const markdownChangedHandler = useCallback((value: string, isClean: boolean): void => {
-    setMarkdownWithDebounce(value, isClean);
-  }, [setMarkdownWithDebounce]);
 
   const save = useCallback(async(opts?: {slackChannels: string, overwriteScopesOfDescendants?: boolean}): Promise<IPageHasId | null> => {
     if (currentPathname == null || optionsToSave == null) {
@@ -221,10 +226,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
       mutateWaitingSaveProcessing(true);
 
       const { page } = await saveOrUpdate(
-        // TODO: get contents from the custom hook
-        // refs: https://redmine.weseek.co.jp/issues/128973
-        // markdownToSave.current,
-        '',
+        codeMirrorEditor?.getDoc() ?? '',
         { pageId, path: currentPagePath || currentPathname, revisionId: currentRevisionId },
         options,
       );
@@ -250,16 +252,13 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     }
 
   }, [
+    codeMirrorEditor,
     currentPathname, optionsToSave, grantData, isSlackEnabled, saveOrUpdate, pageId,
     currentPagePath, currentRevisionId,
     mutateWaitingSaveProcessing, mutateRemotePageId, mutateRemoteRevisionId, mutateRemoteRevisionLastUpdatedAt, mutateRemoteRevisionLastUpdateUser,
   ]);
 
   const saveAndReturnToViewHandler = useCallback(async(opts: {slackChannels: string, overwriteScopesOfDescendants?: boolean}) => {
-    if (editorMode !== EditorMode.Editor) {
-      return;
-    }
-
     const page = await save(opts);
     if (page == null) {
       return;
@@ -272,13 +271,9 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
       updateStateAfterSave?.();
     }
     mutateEditorMode(EditorMode.View);
-  }, [editorMode, save, isNotFound, mutateEditorMode, router, updateStateAfterSave]);
+  }, [save, isNotFound, mutateEditorMode, router, updateStateAfterSave]);
 
   const saveWithShortcut = useCallback(async() => {
-    if (editorMode !== EditorMode.Editor) {
-      return;
-    }
-
     const page = await save();
     if (page == null) {
       return;
@@ -293,7 +288,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     toastSuccess(t('toaster.save_succeeded'));
     mutateEditorMode(EditorMode.Editor);
 
-  }, [editorMode, isNotFound, mutateEditorMode, router, save, t, updateStateAfterSave]);
+  }, [isNotFound, mutateEditorMode, router, save, t, updateStateAfterSave]);
 
 
   /**
@@ -301,12 +296,6 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
    * @param {any} file
    */
   const uploadHandler = useCallback(async(file) => {
-    // TODO: implement
-    // refs: https://redmine.weseek.co.jp/issues/126528
-    // if (editorRef.current == null) {
-    //   return;
-    // }
-
     try {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       let res: any = await apiGet('/attachments.limit', {
@@ -326,11 +315,9 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
       if (pageId != null) {
         formData.append('page_id', pageId);
       }
-      // TODO: get contents from the custom hook
-      // refs: https://redmine.weseek.co.jp/issues/128973
-      // if (pageId == null && markdownToSave.current != null) {
-      //   formData.append('page_body', markdownToSave.current);
-      // }
+      if (pageId == null) {
+        formData.append('page_body', codeMirrorEditor?.getDoc() ?? '');
+      }
 
       res = await apiPostForm('/attachments.add', formData);
       const attachment = res.attachment;
@@ -366,7 +353,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
       // refs: https://redmine.weseek.co.jp/issues/126528
       // editorRef.current.terminateUploadingState();
     }
-  }, [currentPagePath, mutateCurrentPage, mutateCurrentPageId, mutateIsLatestRevision, pageId]);
+  }, [codeMirrorEditor, currentPagePath, mutateCurrentPage, mutateCurrentPageId, mutateIsLatestRevision, pageId]);
 
 
   const scrollPreviewByEditorLine = useCallback((line: number) => {
@@ -485,20 +472,20 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
       return;
     }
     // markdownToSave.current = initialValue;
-    initDoc?.(initialValue);
+    codeMirrorEditor?.initDoc(initialValue);
     setMarkdownToPreview(initialValue);
     mutateIsEnabledUnsavedWarning(false);
-  }, [initDoc, initialValue, mutateIsEnabledUnsavedWarning]);
+  }, [codeMirrorEditor, initialValue, mutateIsEnabledUnsavedWarning]);
 
   // initial caret line
   useEffect(() => {
-    setCaretLine?.();
-  }, [setCaretLine]);
+    codeMirrorEditor?.setCaretLine();
+  }, [codeMirrorEditor]);
 
   // set handler to set caret line
   useEffect(() => {
     const handler = (line) => {
-      setCaretLine?.(line);
+      codeMirrorEditor?.setCaretLine(line);
 
       if (previewRef.current != null) {
         scrollSyncHelper.scrollPreview(previewRef.current, line);
@@ -509,7 +496,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     return function cleanup() {
       globalEmitter.removeListener('setCaretLine', handler);
     };
-  }, [setCaretLine]);
+  }, [codeMirrorEditor]);
 
   // set handler to save and return to View
   useEffect(() => {
@@ -520,17 +507,31 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     };
   }, [saveAndReturnToViewHandler]);
 
+  // set handler to save with shortcut key
+  useEffect(() => {
+    const extension = keymap.of([
+      {
+        key: 'Mod-s',
+        preventDefault: true,
+        run: () => {
+          saveWithShortcut();
+          return true;
+        },
+      },
+    ]);
+
+    const cleanupFunction = codeMirrorEditor?.appendExtension(extension);
+
+    return cleanupFunction;
+
+  }, [codeMirrorEditor, saveWithShortcut]);
+
   // set handler to focus
   useLayoutEffect(() => {
     if (editorMode === EditorMode.Editor) {
-      focusToEditor?.();
+      codeMirrorEditor?.focus();
     }
-  }, [editorMode, focusToEditor]);
-
-  // Set indent size by current indent
-  useEffect(() => {
-    setIndentSize?.(currentIndentSize);
-  }, [setIndentSize, currentIndentSize]);
+  }, [codeMirrorEditor, editorMode]);
 
   // Detect indent size from contents (only when users are allowed to change it)
   useEffect(() => {
@@ -551,9 +552,9 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   // when transitioning to a different page, if the initialValue is the same,
   // UnControlled CodeMirror value does not reset, so explicitly set the value to initialValue
   const onRouterChangeComplete = useCallback(() => {
-    initDoc?.(initialValue);
-    setCaretLine?.();
-  }, [initDoc, initialValue, setCaretLine]);
+    codeMirrorEditor?.initDoc(initialValue);
+    codeMirrorEditor?.setCaretLine();
+  }, [codeMirrorEditor, initialValue]);
 
   useEffect(() => {
     router.events.on('routeChangeComplete', onRouterChangeComplete);

+ 1 - 0
packages/editor/package.json

@@ -33,6 +33,7 @@
     "codemirror": "^6.0.1",
     "eslint-plugin-react-refresh": "^0.4.1",
     "react-hook-form": "^7.45.4",
+    "react-toastify": "^9.1.3",
     "swr": "^2.0.3"
   }
 }

+ 37 - 2
packages/editor/src/components/playground/Playground.tsx

@@ -1,15 +1,49 @@
-import { useRef } from 'react';
+import {
+  useEffect, useMemo, useRef, useState,
+} from 'react';
+
+import { keymap } from '@codemirror/view';
+import { ReactCodeMirrorProps } from '@uiw/react-codemirror';
+import { toast } from 'react-toastify';
 
 import { CodeMirrorEditorContainer } from '..';
 import { useCodeMirrorEditorMain } from '../../stores';
 
 import { PlaygroundController } from './PlaygroundController';
+import { Preview } from './Preview';
 
 export const Playground = (): JSX.Element => {
 
+  const [markdownToPreview, setMarkdownToPreview] = useState('');
+
   const containerRef = useRef(null);
 
-  useCodeMirrorEditorMain(containerRef.current);
+  const props = useMemo<ReactCodeMirrorProps>(() => {
+    return {
+      onChange: setMarkdownToPreview,
+    };
+  }, []);
+  const { data: codeMirrorEditor } = useCodeMirrorEditorMain(containerRef.current, props);
+
+  // set handler to save with shortcut key
+  useEffect(() => {
+    const extension = keymap.of([
+      {
+        key: 'Mod-s',
+        preventDefault: true,
+        run: () => {
+          // eslint-disable-next-line no-console
+          console.log({ doc: codeMirrorEditor?.getDoc() });
+          toast.success('Saved.', { autoClose: 2000 });
+          return true;
+        },
+      },
+    ]);
+
+    const cleanupFunction = codeMirrorEditor?.appendExtension?.(extension);
+
+    return cleanupFunction;
+  }, [codeMirrorEditor]);
 
   return (
     <>
@@ -21,6 +55,7 @@ export const Playground = (): JSX.Element => {
           <CodeMirrorEditorContainer ref={containerRef} />
         </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 />
         </div>
       </div>

+ 1 - 1
packages/editor/src/components/playground/PlaygroundController.tsx

@@ -69,7 +69,7 @@ export const SetCaretLineRow = (): JSX.Element => {
 
 export const PlaygroundController = (): JSX.Element => {
   return (
-    <div className="container">
+    <div className="container mt-5">
       <InitEditorValueRow />
       <SetCaretLineRow />
     </div>

+ 14 - 0
packages/editor/src/components/playground/Preview.tsx

@@ -0,0 +1,14 @@
+type Props = {
+  markdown?: string,
+}
+
+export const Preview = (props: Props): JSX.Element => {
+  return (
+    <div className="container">
+      <h3>Preview</h3>
+      <div className="card" style={{ minHeight: '200px' }}>
+        {props.markdown ?? ''}
+      </div>
+    </div>
+  );
+};

+ 1 - 0
packages/editor/src/main.scss

@@ -1,3 +1,4 @@
 @import 'bootstrap';
+@import 'react-toastify/scss/main';
 
 @import '@growi/core/scss/flex-expand';

+ 2 - 0
packages/editor/src/main.tsx

@@ -1,6 +1,7 @@
 import React from 'react';
 
 import ReactDOM from 'react-dom/client';
+import { ToastContainer } from 'react-toastify';
 
 import { Playground } from './components/playground';
 
@@ -13,5 +14,6 @@ const rootElem = document.getElementById('root');
 ReactDOM.createRoot(rootElem!).render(
   <React.StrictMode>
     <Playground />
+    <ToastContainer />
   </React.StrictMode>,
 );

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

@@ -1 +1 @@
-export * from './use-codemirror-editor';
+export * from './use-codemirror-editor/use-codemirror-editor';

+ 0 - 99
packages/editor/src/services/codemirror-editor/use-codemirror-editor.ts

@@ -1,99 +0,0 @@
-import { useCallback, useEffect } from 'react';
-
-import { indentWithTab } from '@codemirror/commands';
-import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
-import { languages } from '@codemirror/language-data';
-import { EditorState, type EditorStateConfig, type Extension } from '@codemirror/state';
-import { keymap } from '@codemirror/view';
-import { basicSetup, useCodeMirror, type UseCodeMirror } from '@uiw/react-codemirror';
-
-import { UseCodeMirrorEditorStates } from './interfaces/react-codemirror';
-
-export type UseCodeMirrorEditor = UseCodeMirror;
-
-type UseCodeMirrorEditorUtils = {
-  initState: (config?: EditorStateConfig) => void,
-  initDoc: (doc?: string) => void,
-  focus: () => void,
-  setCaretLine: (lineNumber?: number) => void,
-}
-
-export type UseCodeMirrorEditorResponse = UseCodeMirrorEditorStates & UseCodeMirrorEditorUtils;
-
-const defaultExtensions: Extension[] = [
-  markdown({ base: markdownLanguage, codeLanguages: languages }),
-  keymap.of([indentWithTab]),
-];
-
-const defaultExtensionsToInit: Extension[] = [
-  ...basicSetup(),
-  ...defaultExtensions,
-];
-
-export const useCodeMirrorEditor = (props?: UseCodeMirrorEditor): UseCodeMirrorEditorResponse => {
-
-  const codemirror = useCodeMirror({
-    extensions: defaultExtensions,
-    ...props,
-  });
-
-  const { view, setContainer } = codemirror;
-
-  // implement initState method
-  const initState = useCallback((config?: EditorStateConfig): void => {
-    if (view == null) {
-      return;
-    }
-
-    const newState = EditorState.create({
-      ...config,
-      extensions: [
-        ...defaultExtensionsToInit,
-        ...(props?.extensions ?? []),
-      ],
-    });
-
-    view.setState(newState);
-  }, [props?.extensions, view]);
-
-  // implement initDoc method
-  const initDoc = useCallback((doc?: string): void => {
-    initState({ doc });
-  }, [initState]);
-
-  // implement focus method
-  const focus = useCallback((): void => {
-    view?.focus();
-  }, [view]);
-
-  // implement setCaretLine method
-  const setCaretLine = useCallback((lineNumber?: number): void => {
-    if (view == null) {
-      return;
-    }
-
-    const posOfLineEnd = view.state.doc.line(lineNumber ?? 1).to;
-    view.dispatch({
-      selection: {
-        anchor: posOfLineEnd,
-        head: posOfLineEnd,
-      },
-    });
-    // focus
-    view.focus();
-  }, [view]);
-
-  useEffect(() => {
-    if (props?.container != null) {
-      setContainer(props.container);
-    }
-  }, [props?.container, setContainer]);
-
-  return {
-    ...codemirror,
-    initState,
-    initDoc,
-    focus,
-    setCaretLine,
-  };
-};

+ 60 - 0
packages/editor/src/services/codemirror-editor/use-codemirror-editor/use-codemirror-editor.ts

@@ -0,0 +1,60 @@
+import { useMemo } from 'react';
+
+import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
+import { languages } from '@codemirror/language-data';
+import { type Extension } from '@codemirror/state';
+import { useCodeMirror, type UseCodeMirror } from '@uiw/react-codemirror';
+
+import type { UseCodeMirrorEditorStates } from '../interfaces/react-codemirror';
+
+import { AppendExtension, useAppendExtension } from './utils/append-extension';
+import { useFocus, type Focus } from './utils/focus';
+import { useGetDoc, type GetDoc } from './utils/get-doc';
+import { useInitDoc, type InitDoc } from './utils/init-doc';
+import { useSetCaretLine, type SetCaretLine } from './utils/set-caret-line';
+
+type UseCodeMirrorEditorUtils = {
+  initDoc: InitDoc,
+  appendExtension: AppendExtension,
+  getDoc: GetDoc,
+  focus: Focus,
+  setCaretLine: SetCaretLine,
+}
+export type UseCodeMirrorEditor = UseCodeMirrorEditorStates & UseCodeMirrorEditorUtils;
+
+
+const defaultExtensions: Extension[] = [
+  markdown({ base: markdownLanguage, codeLanguages: languages }),
+];
+
+export const useCodeMirrorEditor = (props?: UseCodeMirror): UseCodeMirrorEditor => {
+
+  const mergedProps = useMemo<UseCodeMirror>(() => {
+    return {
+      ...props,
+      extensions: [
+        ...defaultExtensions,
+        ...(props?.extensions ?? []),
+      ],
+    };
+  }, [props]);
+
+  const codemirror = useCodeMirror(mergedProps);
+
+  const { view } = codemirror;
+
+  const initDoc = useInitDoc(view);
+  const appendExtension = useAppendExtension(view);
+  const getDoc = useGetDoc(view);
+  const focus = useFocus(view);
+  const setCaretLine = useSetCaretLine(view);
+
+  return {
+    ...codemirror,
+    initDoc,
+    appendExtension,
+    getDoc,
+    focus,
+    setCaretLine,
+  };
+};

+ 27 - 0
packages/editor/src/services/codemirror-editor/use-codemirror-editor/utils/append-extension.ts

@@ -0,0 +1,27 @@
+import { useCallback } from 'react';
+
+import { Compartment, Extension, StateEffect } from '@codemirror/state';
+import { EditorView } from '@codemirror/view';
+
+type CleanupFunction = () => void;
+export type AppendExtension = (extension: Extension) => CleanupFunction | undefined;
+
+export const useAppendExtension = (view?: EditorView): AppendExtension => {
+
+  return useCallback((extension) => {
+    const compartment = new Compartment();
+    view?.dispatch({
+      effects: StateEffect.appendConfig.of(
+        compartment.of(extension),
+      ),
+    });
+
+    // return cleanup function
+    return () => {
+      view?.dispatch({
+        effects: compartment.reconfigure([]),
+      });
+    };
+  }, [view]);
+
+};

+ 13 - 0
packages/editor/src/services/codemirror-editor/use-codemirror-editor/utils/focus.ts

@@ -0,0 +1,13 @@
+import { useCallback } from 'react';
+
+import { EditorView } from '@codemirror/view';
+
+export type Focus = () => void;
+
+export const useFocus = (view?: EditorView): Focus => {
+
+  return useCallback(() => {
+    view?.focus?.();
+  }, [view]);
+
+};

+ 13 - 0
packages/editor/src/services/codemirror-editor/use-codemirror-editor/utils/get-doc.ts

@@ -0,0 +1,13 @@
+import { useCallback } from 'react';
+
+import { EditorView } from '@codemirror/view';
+
+export type GetDoc = () => string;
+
+export const useGetDoc = (view?: EditorView): GetDoc => {
+
+  return useCallback(() => {
+    return view?.state.doc.toString() ?? '';
+  }, [view]);
+
+};

+ 21 - 0
packages/editor/src/services/codemirror-editor/use-codemirror-editor/utils/init-doc.ts

@@ -0,0 +1,21 @@
+import { useCallback } from 'react';
+
+import { Transaction } from '@codemirror/state';
+import { EditorView } from '@codemirror/view';
+
+export type InitDoc = (doc?: string) => void;
+
+export const useInitDoc = (view?: EditorView): InitDoc => {
+
+  return useCallback((doc) => {
+    view?.dispatch({
+      changes: {
+        from: 0,
+        to: view?.state.doc.length,
+        insert: doc,
+      },
+      annotations: Transaction.addToHistory.of(false),
+    });
+  }, [view]);
+
+};

+ 27 - 0
packages/editor/src/services/codemirror-editor/use-codemirror-editor/utils/set-caret-line.ts

@@ -0,0 +1,27 @@
+import { useCallback } from 'react';
+
+import { EditorView } from '@codemirror/view';
+
+export type SetCaretLine = (lineNumber?: number) => void;
+
+export const useSetCaretLine = (view?: EditorView): SetCaretLine => {
+
+  return useCallback((lineNumber) => {
+    const doc = view?.state.doc;
+
+    if (doc == null) {
+      return;
+    }
+
+    const posOfLineEnd = doc.line(lineNumber ?? 1).to;
+    view?.dispatch({
+      selection: {
+        anchor: posOfLineEnd,
+        head: posOfLineEnd,
+      },
+    });
+    // focus
+    view?.focus();
+  }, [view]);
+
+};

+ 11 - 35
packages/editor/src/stores/codemirror-editor.ts

@@ -3,54 +3,30 @@ import { useCallback, useMemo, useState } from 'react';
 import { indentUnit } from '@codemirror/language';
 import { type Extension } from '@codemirror/state';
 import { scrollPastEnd } from '@codemirror/view';
-import {
-  type SWRResponseWithUtils, withUtils, useSWRStatic,
-} from '@growi/core/dist/swr';
+import { useSWRStatic } from '@growi/core/dist/swr';
+import type { ReactCodeMirrorProps, UseCodeMirror } from '@uiw/react-codemirror';
+import type { SWRResponse } from 'swr';
 
-import type { UseCodeMirrorEditor, UseCodeMirrorEditorResponse } from '../services';
+import type { UseCodeMirrorEditor } from '../services';
 import { useCodeMirrorEditor } from '../services';
 
 const defaultExtensionsMain: Extension[] = [
   scrollPastEnd(),
 ];
 
-type MainEditorUtils = {
-  // impl something
-  setIndentSize: (indentSize?: number) => void,
-};
-
-export const useCodeMirrorEditorMain = (container?: HTMLDivElement | null): SWRResponseWithUtils<MainEditorUtils, UseCodeMirrorEditorResponse> => {
-
-  const [indentUnitString, setIndentUnitString] = useState<string>('  ');
-  const extentsionsMain: Extension[] = useMemo(() => [
-    indentUnit.of(indentUnitString),
-  ],
-  [indentUnitString]);
-
-  const props = useMemo<UseCodeMirrorEditor>(() => {
+export const useCodeMirrorEditorMain = (container?: HTMLDivElement | null, props?: ReactCodeMirrorProps): SWRResponse<UseCodeMirrorEditor> => {
+  const mergedProps = useMemo<UseCodeMirror>(() => {
     return {
+      ...props,
       container,
-      autoFocus: true,
       extensions: [
+        ...(props?.extensions ?? []),
         ...defaultExtensionsMain,
-        ...extentsionsMain,
       ],
     };
-  }, [container, extentsionsMain]);
-
-  const states = useCodeMirrorEditor(props);
-
-  const swrResponse = useSWRStatic('codeMirrorEditorMain', container != null ? states : undefined);
+  }, [container, props]);
 
-  // implement setIndentSize method
-  const setIndentSize = useCallback((indentSize?: number): void => {
-    if (indentSize != null) {
-      setIndentUnitString(' '.repeat(indentSize));
-    }
-  }, [setIndentUnitString]);
+  const states = useCodeMirrorEditor(mergedProps);
 
-  return withUtils(swrResponse, {
-    // impl something
-    setIndentSize,
-  });
+  return useSWRStatic('codeMirrorEditorMain', props != null ? states : undefined);
 };

+ 4 - 4
yarn.lock

@@ -14472,10 +14472,10 @@ react-syntax-highlighter@^15.5.0:
     prismjs "^1.27.0"
     refractor "^3.6.0"
 
-react-toastify@^9.1.1:
-  version "9.1.1"
-  resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-9.1.1.tgz#9280caea4a13dc1739c350d90660a630807bf10b"
-  integrity sha512-pkFCla1z3ve045qvjEmn2xOJOy4ZciwRXm1oMPULVkELi5aJdHCN/FHnuqXq8IwGDLB7PPk2/J6uP9D8ejuiRw==
+react-toastify@^9.1.3:
+  version "9.1.3"
+  resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-9.1.3.tgz#1e798d260d606f50e0fab5ee31daaae1d628c5ff"
+  integrity sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==
   dependencies:
     clsx "^1.1.1"