diff --git a/components/CodeEditorOld/CodeEditor.js b/components/CodeEditorOld/CodeEditor.js deleted file mode 100644 index 394e57d..0000000 --- a/components/CodeEditorOld/CodeEditor.js +++ /dev/null @@ -1,164 +0,0 @@ -import { useRef, useEffect } from "react"; -// import dynamic from "next/dynamic"; -import classNames from "classnames"; -import { editorSetup } from "./CodeEditorSetup"; -import { EditorState, Compartment } from "@codemirror/state"; -import { EditorView, keymap } from "@codemirror/view"; -import { defaultTabBinding } from "@codemirror/commands"; -import { adjustIndentWidth, CodeMirrorLanguageByType } from "./CodeEditorUtils"; -import styles from "./CodeEditor.module.scss"; -import { LANGUAGES } from "../../data/languages"; - -// TODO: Attempt dynamic imports -// https://nextjs.org/docs/advanced-features/dynamic-import#with-named-exports -// const DynamicComponent = dynamic( -// () => -// import("../../themes/twilight").then((mod) => { -// console.log("mod", { mod }); -// return mod.twilight; -// }), -// { -// ssr: false, -// } -// ); -// console.log(DynamicComponent); - -// import { oneDark } from "@codemirror/theme-one-dark"; - -// TODO: Convert more themes, dynamically load them when requested. -import { twilight } from "../../themes/twilight"; -// TODO: In reality we won't load multiple themes at once. -import { oneDark } from "../../themes/oneDark"; - -// TODO: EditorSettings - rebuild with new settings or try to update compartments? - -export default function CodeEditor({ - className, - title, - language, - value, - editorSettings, - working, - workingNotes, - style, - ...props -}) { - const indentWidth = Number(editorSettings.indentWidth); - const container = useRef(); - const view = useRef(); - const compartments = { - language: new Compartment(), - tabSize: new Compartment(), - // indentUnit: new Compartment(), - }; - - // TODO: Dynamic language switching without destroying the whole instance. Do we need lifecycle Component methods? - useEffect(() => { - // To prevent Next.js fast refresh from adding additional editors - if (container.current.children[0]) container.current.children[0].remove(); - - const lang = CodeMirrorLanguageByType(language); - let langOptions = {}; - if (language === LANGUAGES.JSX) { - langOptions.jsx = true; - } - - let theme = twilight; - if (editorSettings.theme === "oneDark") { - theme = oneDark; - } - - let startState = EditorState.create({ - doc: value, - extensions: [ - editorSetup(editorSettings), - keymap.of(defaultTabBinding), - compartments.tabSize.of(EditorState.tabSize.of(indentWidth)), - // compartments.indentUnit.of(EditorState.indentUnit.of(indentUnit)), - compartments.language.of(lang && lang(langOptions)), - theme, - ], - }); - - let editorView = new EditorView({ - state: startState, - parent: container.current, - lineWrapping: editorSettings.lineWrapping, - }); - - view.current = editorView; - - return () => { - editorView.destroy(); - }; - }, [ - indentWidth, - editorSettings.indentUnit, - editorSettings.lineWrapping, - editorSettings.lineNumbers, - editorSettings.codeFolding, - editorSettings.autocomplete, - editorSettings.matchBrackets, - editorSettings.theme, - ]); - - useEffect(() => { - if (view.current) { - // TODO: Try to dispatch rather than rebuild. - // view.current.dispatch({ - // effects: compartments.tabSize.reconfigure( - // EditorState.tabSize.of(indentWidth) - // ), - // }); - - const formattedValue = adjustIndentWidth({ - indentWidth, - language, - value, - }); - view.current.dispatch( - view.current.state.update({ - changes: { - from: 0, - to: view.current.state.doc.length, - insert: formattedValue, - }, - }) - ); - } - }, [indentWidth, language, value]); - - const { fontSize, fontFamily } = editorSettings; - useEffect(() => { - // Is there a better way to "refresh" the view if font-size and such change? - view.current.requestMeasure(); - }, [fontSize, fontFamily]); - - // TODO: This doesn't work. But it feels like it would be better to dispatch a change to the theme. - const { theme } = editorSettings; - // useEffect(() => { - // if (theme === "twilight") { - // view.current.dispatch({ - // effects: compartments.theme.reconfigure(twilight), - // }); - // } - // if (theme === "oneDark") { - // view.current.dispatch({ - // effects: compartments.theme.reconfigure(oneDark), - // }); - // } - // }, [theme]); - - return ( -
- ); -} diff --git a/components/CodeEditorOld/CodeEditor.module.scss b/components/CodeEditorOld/CodeEditor.module.scss deleted file mode 100644 index 586d879..0000000 --- a/components/CodeEditorOld/CodeEditor.module.scss +++ /dev/null @@ -1,7 +0,0 @@ -.editor { - :global(.cm-scroller) { - font-size: var(--font-size); - /* For some reason, the default .cm-scroller styles have an !important font family declaration. */ - font-family: var(--font-family) !important; - } -} diff --git a/components/CodeEditorOld/CodeEditorSetup.js b/components/CodeEditorOld/CodeEditorSetup.js deleted file mode 100644 index e772c9f..0000000 --- a/components/CodeEditorOld/CodeEditorSetup.js +++ /dev/null @@ -1,93 +0,0 @@ -import { - highlightSpecialChars, - drawSelection, - highlightActiveLine, - keymap, -} from "@codemirror/view"; -export { EditorView } from "@codemirror/view"; -import { EditorState } from "@codemirror/state"; -export { EditorState } from "@codemirror/state"; -import { history, historyKeymap } from "@codemirror/history"; -import { foldGutter, foldKeymap } from "@codemirror/fold"; -import { indentOnInput } from "@codemirror/language"; -import { lineNumbers, highlightActiveLineGutter } from "@codemirror/gutter"; -import { defaultKeymap } from "@codemirror/commands"; -import { bracketMatching } from "@codemirror/matchbrackets"; -import { closeBrackets, closeBracketsKeymap } from "@codemirror/closebrackets"; -import { highlightSelectionMatches, searchKeymap } from "@codemirror/search"; -import { autocompletion, completionKeymap } from "@codemirror/autocomplete"; -import { commentKeymap } from "@codemirror/comment"; -import { rectangularSelection } from "@codemirror/rectangular-selection"; -import { defaultHighlightStyle } from "@codemirror/highlight"; -import { lintKeymap } from "@codemirror/lint"; - -/* - - [the default command bindings](https://codemirror.net/6/docs/ref/#commands.defaultKeymap) - - [line numbers](https://codemirror.net/6/docs/ref/#gutter.lineNumbers) - - [special character highlighting](https://codemirror.net/6/docs/ref/#view.highlightSpecialChars) - - [the undo history](https://codemirror.net/6/docs/ref/#history.history) - - [a fold gutter](https://codemirror.net/6/docs/ref/#fold.foldGutter) - - [custom selection drawing](https://codemirror.net/6/docs/ref/#view.drawSelection) - - [multiple selections](https://codemirror.net/6/docs/ref/#state.EditorState^allowMultipleSelections) - - [reindentation on input](https://codemirror.net/6/docs/ref/#language.indentOnInput) - - [the default highlight style](https://codemirror.net/6/docs/ref/#highlight.defaultHighlightStyle) (as fallback) - - [bracket matching](https://codemirror.net/6/docs/ref/#matchbrackets.bracketMatching) - - [bracket closing](https://codemirror.net/6/docs/ref/#closebrackets.closeBrackets) - - [autocompletion](https://codemirror.net/6/docs/ref/#autocomplete.autocompletion) - - [rectangular selection](https://codemirror.net/6/docs/ref/#rectangular-selection.rectangularSelection) - - [active line highlighting](https://codemirror.net/6/docs/ref/#view.highlightActiveLine) - - [active line gutter highlighting](https://codemirror.net/6/docs/ref/#gutter.highlightActiveLineGutter) - - [selection match highlighting](https://codemirror.net/6/docs/ref/#search.highlightSelectionMatches) - - [search](https://codemirror.net/6/docs/ref/#search.searchKeymap) - - [commenting](https://codemirror.net/6/docs/ref/#comment.commentKeymap) - - [linting](https://codemirror.net/6/docs/ref/#lint.lintKeymap) -*/ - -const alwaysOn = [ - highlightSpecialChars(), - history(), - drawSelection(), - EditorState.allowMultipleSelections.of(true), - indentOnInput(), - defaultHighlightStyle.fallback, - rectangularSelection(), - highlightActiveLine(), - highlightSelectionMatches(), - keymap.of([ - ...closeBracketsKeymap, - ...defaultKeymap, - ...searchKeymap, - ...historyKeymap, - ...foldKeymap, - ...commentKeymap, - ...completionKeymap, - ...lintKeymap, - ]), -]; - -function editorSetup(editorSettings) { - let completeSetup = [...alwaysOn]; - - // NOTE: Order matters here! - // If foldGutter went first, it will render to the left of the line numbers in the gutter. - if (editorSettings.lineNumbers) { - completeSetup.push(lineNumbers()); - completeSetup.push(highlightActiveLineGutter()); - } - - if (editorSettings.codeFolding) { - completeSetup.push(foldGutter()); - } - if (editorSettings.autocomplete) { - completeSetup.push(autocompletion()); - } - - if (editorSettings.matchBrackets) { - completeSetup.push(bracketMatching()); - completeSetup.push(closeBrackets()); - } - - return completeSetup; -} - -export { editorSetup }; diff --git a/components/CodeEditorOld/CodeEditorUtils.js b/components/CodeEditorOld/CodeEditorUtils.js deleted file mode 100644 index 5137cff..0000000 --- a/components/CodeEditorOld/CodeEditorUtils.js +++ /dev/null @@ -1,85 +0,0 @@ -import { LANGUAGES } from "../../data/languages"; - -// TODO: Dynamic imports -import { css } from "@codemirror/lang-css"; -import { html } from "@codemirror/lang-html"; -import { javascript } from "@codemirror/lang-javascript"; -import { markdown } from "@codemirror/lang-markdown"; - -import prettier from "prettier/standalone"; -import parserBabel from "prettier/parser-babel"; -import parserHtml from "prettier/parser-html"; -import parserCss from "prettier/parser-postcss"; - -export function CodeMirrorLanguageByType(type) { - switch (type) { - // These seem fine - case LANGUAGES.HTML: - return html; - case LANGUAGES.CSS: - return css; - case LANGUAGES.MARKDOWN: - return markdown; - - case LANGUAGES.JAVASCRIPT: - case LANGUAGES.JSX: - return javascript; - - // TODO: Not quite right. - case LANGUAGES.SCSS: - case LANGUAGES.SASS: - case LANGUAGES.LESS: - case LANGUAGES.STYLUS: - return css; - - // TODO entirely - case LANGUAGES.HAML: - case LANGUAGES.PUG: - case LANGUAGES.SLIM: - case LANGUAGES.NUNJUCKS: - return html; - - case LANGUAGES.COFFEESCRIPT: - case LANGUAGES.TYPESCRIPT: - case LANGUAGES.LIVESCRIPT: - return javascript; - } -} - -export function adjustIndentWidth({ language, value, indentWidth }) { - // TODO: Only do Prettier on an Indent Width change. - // TODO: See if CodeMirror has an official way of doing indentation changes. - // Do Prettier! - // https://prettier.io/docs/en/browser.html - let parser = "babel"; - if (language === "html") parser = "html"; - if (language === "scss" || language === "css" || parser === "less") - parser = "css"; - // TODO: Do Prettier on the other supported languages - if ( - language === "js" || - language === "html" || - language === "css" || - language === "scss" || - language === "less" - ) { - // prettier can throw hard errors if the parser fails. - try { - // replace entire document with prettified version - const formattedValue = prettier.format(value, { - parser: parser, - plugins: [parserBabel, parserHtml, parserCss], - tabWidth: indentWidth, - // semi: true, - trailingComma: "none", - // useTabs: indentWith === "tabs", - bracketSpacing: true, - jsxBracketSameLine: false, - }); - return formattedValue; - } catch (err) { - console.error(err); - } - } - return value; -} diff --git a/components/CodeEditorOld/index.js b/components/CodeEditorOld/index.js deleted file mode 100644 index 036450f..0000000 --- a/components/CodeEditorOld/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import CodeEditor from "./CodeEditor"; - -export default CodeEditor; diff --git a/components/CodeMirror6Instance/CodeMirror6Instance.js b/components/CodeMirror6Instance/CodeMirror6Instance.js index 9c5de6c..916bc66 100644 --- a/components/CodeMirror6Instance/CodeMirror6Instance.js +++ b/components/CodeMirror6Instance/CodeMirror6Instance.js @@ -11,7 +11,7 @@ CodeMirror6Instance.propTypes = { value: PropTypes.string, state: PropTypes.object, extensions: PropTypes.arrayOf( - PropTypes.oneOfType([PropTypes.object, PropTypes.func]) + PropTypes.oneOfType([PropTypes.array, PropTypes.object, PropTypes.func]) ), onInit: PropTypes.func, onChange: PropTypes.func, diff --git a/components/CodeMirror6Instance/CodeMirror6InstanceHooks.js b/components/CodeMirror6Instance/CodeMirror6InstanceHooks.js index 7a68811..09e41e6 100644 --- a/components/CodeMirror6Instance/CodeMirror6InstanceHooks.js +++ b/components/CodeMirror6Instance/CodeMirror6InstanceHooks.js @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from "react"; import { EditorState } from "@codemirror/state"; -import { EditorView } from "codemirror"; +import { EditorView } from "@codemirror/view"; import { useExtensions } from "./extensions/useExtensions"; export function useCodeMirror6Instance(props) { diff --git a/components/CodeMirror6Instance/extensions/defaultExtensions.js b/components/CodeMirror6Instance/extensions/defaultExtensions.js index 7a4f1b8..052df52 100644 --- a/components/CodeMirror6Instance/extensions/defaultExtensions.js +++ b/components/CodeMirror6Instance/extensions/defaultExtensions.js @@ -29,7 +29,7 @@ import { lintKeymap } from "@codemirror/lint"; export const defaultKeymaps = keymap.of([ ...defaultKeymap, ...searchKeymap, - ...historyKeymap, + // ...historyKeymap, ...lintKeymap, // NOTE: This keymap refers to the `tab` key, NOT tabs vs spaces. @@ -41,7 +41,7 @@ export const defaultKeymaps = keymap.of([ export const defaultExtensions = [ syntaxHighlighting(defaultHighlightStyle, { fallback: true }), highlightSpecialChars(), - history(), + // history(), drawSelection(), // Multi cursor/select diff --git a/package.json b/package.json index 4253409..919530b 100644 --- a/package.json +++ b/package.json @@ -12,15 +12,15 @@ }, "dependencies": { "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", + "@codemirror/commands": "^6.1.0", + "@codemirror/language": "^6.2.1", "@codemirror/language-data": "6.1.0", "@codemirror/legacy-modes": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", + "@codemirror/state": "^6.1.1", "@codemirror/theme-one-dark": "^6.0.0", - "@codemirror/view": "^6.0.0", + "@codemirror/view": "^6.2.1", "@emmetio/codemirror6-plugin": "^0.1.1", "@netlify/plugin-nextjs": "^4.9.1", "@next/bundle-analyzer": "^11.0.1", @@ -29,16 +29,17 @@ "cm6-theme-material-dark": "^0.2.0", "cm6-theme-solarized-dark": "^0.2.0", "cm6-theme-solarized-light": "^0.2.0", - "codemirror": "^6.0.0", + "codemirror": "^6.0.1", + "diff": "^5.1.0", "next": "^12.1.4", "prettier": "^2.3.1", "react": "17.0.2", "react-dom": "17.0.2", "sass": "^1.35.1", "thememirror": "^2.0.0", - "y-codemirror.next": "^0.3.0", + "y-codemirror.next": "^0.3.2", "y-webrtc": "^10.2.3", - "yjs": "^13.5.39" + "yjs": "^13.5.41" }, "devDependencies": { "eslint": "7.29.0", diff --git a/pages/yjs-multidoc.js b/pages/yjs-multidoc.js new file mode 100644 index 0000000..0a2caca --- /dev/null +++ b/pages/yjs-multidoc.js @@ -0,0 +1,192 @@ +import { useState, useEffect, useRef } from "react"; +import Head from "next/head"; + +import { EDITOR_SETTINGS_DEFAULTS } from "../data/editorSettings"; +import { LANGUAGES } from "../data/languages"; +import styles from "../styles/Home.module.scss"; + +import CodeMirror6Instance from "../components/CodeMirror6Instance"; + +import * as Y from "yjs"; +import { yCollab, yUndoManagerKeymap } from "y-codemirror.next"; +import { StateEffect } from "@codemirror/state"; +import { keymap } from "@codemirror/view"; + +// Diff like https://motif.land/blog/syncing-text-files-using-yjs-and-the-file-system-access-api ? + +function mergeYDocTexts(ydoc1, ydoc2) { + // Janky test to see if the initial doc has any history. + const hasDoc1BeenInitialized = ydoc1.store.clients.size > 0; + if (hasDoc1BeenInitialized) { + // If there's content in ydoc2, delete it all + const ytext2 = ydoc2.getText(); + ytext2.applyDelta([{ delete: ytext2.length }]); + + // Syncs the state from ydoc1 to ydoc2 + const state1 = Y.encodeStateAsUpdate(ydoc1); + Y.applyUpdate(ydoc2, state1); + } + + // Ensures the ydoc1 state matches the new ydoc2 state + const state2 = Y.encodeStateAsUpdate(ydoc2); + Y.applyUpdate(ydoc1, state2); + + // // NOTE: This diffing doesn't really do anything at this point. For our actual implementation, the first YDoc received should be considered the primary document and all other clients should clear and go off of that document state. + // const ydoc1Text = ydoc1.getText().toString(); + // const ydoc2Text = ydoc2.getText().toString(); + // const deltas = getDeltaOperations(ydoc1Text, ydoc2Text); + // ydoc1.getText().applyDelta(deltas); +} + +export default function SharedYjs() { + const [editorSettings] = useState(EDITOR_SETTINGS_DEFAULTS); + + const [fileValue, setFileValue] = useState( + `\n \n Hello World\n \n` + ); + const [submittedValue, setSubmittedValue] = useState(fileValue); + function onSubmit() { + setSubmittedValue(fileValue); + } + + const [, renderComponent] = useState(); + function forceRender() { + renderComponent(Date.now()); + } + + const controller = useRef(); + const yDocs = useRef([]); + + function logStates() { + console.group("Controller YDoc"); + const state = Y.encodeStateAsUpdate(controller.current); + Y.logUpdate(state); + console.groupEnd(); + + yDocs.current.map((ydoc, i) => { + console.group("YDoc " + i); + const state = Y.encodeStateAsUpdate(ydoc); + Y.logUpdate(state); + console.groupEnd(); + }); + } + + // Set up controller yDoc + useEffect(() => { + console.log("setting up controller yDoc"); + controller.current = new Y.Doc(); + controller.current.on("update", (update, _, originDoc) => { + if (originDoc.guid !== controller.current.guid) return; + console.log("controller update!!!!!", originDoc, controller.current); + yDocs.current.forEach((otherDoc) => { + Y.applyUpdate(otherDoc, update); + }); + }); + makeYDoc(fileValue); + forceRender(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + function makeYDoc(initialValue) { + const yDoc = new Y.Doc(); + const yText = yDoc.getText(); + + if (initialValue) { + yText.insert(0, initialValue); + } + + mergeYDocTexts(controller.current, yDoc); + + yDoc.on("update", (update, _, originDoc) => { + if (originDoc !== yDoc) return; + if (controller.current) { + console.log("update"); + Y.logUpdate(update); + Y.applyUpdate(controller.current, update, yDoc); + } + }); + + yDocs.current.push(yDoc); + + forceRender(); + } + + return ( +
+ + CodeMirror 6 Y.js Integration + + +
+
+

CodeMirror 6 Y.js Integration

+ Back to main +
+ +
+ {" "} + +
+

setFileValue("hello")}>File Contents

+