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
1import type { EditOpInfo, OperationInfo } from '@yorkie-js/sdk';2import yorkie, { DocEventType } from '@yorkie-js/sdk';3import { basicSetup, EditorView } from 'codemirror';4import { Transaction, TransactionSpec } from '@codemirror/state';5import { network } from './network';6import { displayLog, displayPeers } from './utils';7import { YorkieDoc, YorkiePresence } from './type';8import './style.css';910const networkStatusElem = document.getElementById('network-status')!;11const peersElem = document.getElementById('peers')!;12const editorParentElem = document.getElementById('editor')!;1314async function main() {15 // 01. create client with RPCAddr then activate it.16 const client = new yorkie.Client({17 rpcAddr: import.meta.env.VITE_YORKIE_API_ADDR,18 apiKey: import.meta.env.VITE_YORKIE_API_KEY,19 });20 await client.activate();2122 // 02-1. create a document then attach it into the client.23 const doc = new yorkie.Document<YorkieDoc, YorkiePresence>(24 `codemirror6-${new Date()25 .toISOString()26 .substring(0, 10)27 .replace(/-/g, '')}`,28 {29 enableDevtools: true,30 },31 );32 doc.subscribe('connection', (event) => {33 network.statusListener(networkStatusElem)(event);34 });35 doc.subscribe('presence', (event) => {36 if (event.type !== DocEventType.PresenceChanged) {37 displayPeers(peersElem, doc.getPresences(), client.getID()!);38 }39 });40 await client.attach(doc, {41 initialPresence: {42 username: client.getID()!.slice(-2),43 },44 });45 doc.update((root) => {46 if (!root.content) {47 root.content = new yorkie.Text();48 }49 }, 'create content if not exists');5051 // 02-2. subscribe document event.52 const syncText = () => {53 const text = doc.getRoot().content;54 const selection = doc.getMyPresence().selection;55 const transactionSpec: TransactionSpec = {56 changes: { from: 0, to: view.state.doc.length, insert: text.toString() },57 annotations: [Transaction.remote.of(true)],58 };5960 if (selection) {61 // Restore the cursor position when the text is replaced.62 const cursor = text.posRangeToIndexRange(selection);63 transactionSpec['selection'] = {64 anchor: cursor[0],65 head: cursor[1],66 };67 }68 view.dispatch(transactionSpec);69 };70 doc.subscribe((event) => {71 if (event.type === 'snapshot') {72 // The text is replaced to snapshot and must be re-synced.73 syncText();74 }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 // Height: show about 10 lines without needing actual blank text lines.137 // Adjust the px value if font-size/line-height changes.138 const fixedHeightTheme = EditorView.theme({139 '.cm-content, .cm-gutter': { minHeight: '210px' }, // ~10 lines (≈21px per line including padding)140 });141 const view = new EditorView({142 doc: '',143 extensions: [basicSetup, fixedHeightTheme, updateListener],144 parent: editorParentElem,145 });146147 // 03-3. define event handler that apply remote changes to local148 function handleOperations(operations: Array<OperationInfo>) {149 for (const op of operations) {150 if (op.type === 'edit') {151 handleEditOp(op);152 }153 }154 }155 function handleEditOp(op: EditOpInfo) {156 const changes = [157 {158 from: Math.max(0, op.from),159 to: Math.max(0, op.to),160 insert: op.value!.content,161 },162 ];163164 view.dispatch({165 changes,166 annotations: [Transaction.remote.of(true)],167 });168 }169170 syncText();171}172173main();
- User 1
- User 2
- User 1
- User 2