CodeMirror
This is a real-time collaborative example of the CodeMirror 6 editor. It uses the Text, a custom CRDT type from Yorkie.
main.ts
1/* eslint-disable jsdoc/require-jsdoc */2import type { EditOpInfo, OperationInfo } from '@yorkie-js/sdk';3import yorkie, { DocEventType } from '@yorkie-js/sdk';4import { basicSetup, EditorView } from 'codemirror';5import { Transaction, TransactionSpec } from '@codemirror/state';6import { network } from './network';7import { displayLog, displayPeers } from './utils';8import { YorkieDoc, YorkiePresence } from './type';9import './style.css';1011const editorParentElem = document.getElementById('editor')!;12const peersElem = document.getElementById('peers')!;13const documentElem = document.getElementById('document')!;14const documentTextElem = document.getElementById('document-text')!;15const networkStatusElem = document.getElementById('network-status')!;1617async function main() {18 // 01. create client with RPCAddr then activate it.19 const client = new yorkie.Client({20 rpcAddr: import.meta.env.VITE_YORKIE_API_ADDR,21 apiKey: import.meta.env.VITE_YORKIE_API_KEY,22 });23 await client.activate();2425 // 02-1. create a document then attach it into the client.26 const doc = new yorkie.Document<YorkieDoc, YorkiePresence>(27 `codemirror6-${new Date()28 .toISOString()29 .substring(0, 10)30 .replace(/-/g, '')}`,31 {32 enableDevtools: true,33 },34 );35 doc.subscribe('connection', (event) => {36 network.statusListener(networkStatusElem)(event);37 });38 doc.subscribe('presence', (event) => {39 if (event.type !== DocEventType.PresenceChanged) {40 displayPeers(peersElem, doc.getPresences(), client.getID()!);41 }42 });43 await client.attach(doc);44 doc.update((root) => {45 if (!root.content) {46 root.content = new yorkie.Text();47 }48 }, 'create content if not exists');4950 // 02-2. subscribe document event.51 const syncText = () => {52 const text = doc.getRoot().content;53 const selection = doc.getMyPresence().selection;54 const transactionSpec: TransactionSpec = {55 changes: { from: 0, to: view.state.doc.length, insert: text.toString() },56 annotations: [Transaction.remote.of(true)],57 };5859 if (selection) {60 // Restore the cursor position when the text is replaced.61 const cursor = text.posRangeToIndexRange(selection);62 transactionSpec['selection'] = {63 anchor: cursor[0],64 head: cursor[1],65 };66 }67 view.dispatch(transactionSpec);68 };69 doc.subscribe((event) => {70 if (event.type === 'snapshot') {71 // The text is replaced to snapshot and must be re-synced.72 syncText();73 }74 displayLog(documentElem, documentTextElem, doc);75 });7677 doc.subscribe('$.content', (event) => {78 if (event.type === 'remote-change') {79 const { operations } = event.value;80 handleOperations(operations);81 }82 });8384 await client.sync();8586 // 03-1. define function that bind the document with the codemirror(broadcast local changes to peers)87 const updateListener = EditorView.updateListener.of((viewUpdate) => {88 if (viewUpdate.docChanged) {89 for (const tr of viewUpdate.transactions) {90 const events = ['select', 'input', 'delete', 'move', 'undo', 'redo'];91 if (!events.map((event) => tr.isUserEvent(event)).some(Boolean)) {92 continue;93 }94 if (tr.annotation(Transaction.remote)) {95 continue;96 }97 let adj = 0;98 tr.changes.iterChanges((fromA, toA, _, __, inserted) => {99 const insertText = inserted.toJSON().join('\n');100 doc.update((root) => {101 root.content.edit(fromA + adj, toA + adj, insertText);102 }, `update content byA ${client.getID()}`);103 adj += insertText.length - (toA - fromA);104 });105 }106 }107108 const hasFocus =109 viewUpdate.view.hasFocus && viewUpdate.view.dom.ownerDocument.hasFocus();110 const sel = hasFocus ? viewUpdate.state.selection.main : null;111112 doc.update((root, presence) => {113 if (sel && root.content) {114 const selection = root.content.indexRangeToPosRange([115 sel.anchor,116 sel.head,117 ]);118119 if (120 JSON.stringify(selection) !==121 JSON.stringify(presence.get('selection'))122 ) {123 presence.set({124 selection,125 });126 }127 } else if (presence.get('selection')) {128 presence.set({129 selection: undefined,130 });131 }132 });133 });134135 // 03-2. create codemirror instance136 const view = new EditorView({137 doc: '',138 extensions: [basicSetup, updateListener],139 parent: editorParentElem,140 });141142 // 03-3. define event handler that apply remote changes to local143 function handleOperations(operations: Array<OperationInfo>) {144 for (const op of operations) {145 if (op.type === 'edit') {146 handleEditOp(op);147 }148 }149 }150 function handleEditOp(op: EditOpInfo) {151 const changes = [152 {153 from: Math.max(0, op.from),154 to: Math.max(0, op.to),155 insert: op.value!.content,156 },157 ];158159 view.dispatch({160 changes,161 annotations: [Transaction.remote.of(true)],162 });163 }164165 syncText();166 displayLog(documentElem, documentTextElem, doc);167}168169main();
- User 1
- User 2
- User 1
- User 2
Event Log