Skip to content

Commit 4b6f986

Browse files
authored
feat(collaboration service): language support
Add multi-language support and syntax highlighting Add support for multiple programming languages (Python, JavaScript, Java, C++) and Plain Text Add language dropdown with real-time synchronisation between collaborators for language switching functionality Configure CodeMirror with syntax highlighting for all supported languages Add code autocompletion for Python (other languages do not have built-in autocompletion support in their packages) Partially completes requirement N2H1.1 (Enhanced Code Editor)
2 parents 64090c2 + e2e3ce8 commit 4b6f986

File tree

5 files changed

+344
-1698
lines changed

5 files changed

+344
-1698
lines changed

frontend/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
"preview": "vite preview"
1313
},
1414
"dependencies": {
15+
"@codemirror/lang-cpp": "^6.0.3",
16+
"@codemirror/lang-java": "^6.0.2",
17+
"@codemirror/lang-javascript": "^6.2.4",
18+
"@codemirror/lang-python": "^6.2.1",
1519
"@tanstack/react-query": "^5.90.2",
1620
"@tanstack/react-query-devtools": "^5.90.2",
1721
"axios": "^1.12.2",

frontend/src/collaboration/components/CodeMirror.tsx

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// src/codemirror.tsx
22
import React, {useEffect, useRef} from 'react';
3-
import {EditorState} from '@codemirror/state';
3+
import {EditorState, Compartment, type Extension} from '@codemirror/state';
44
import {
55
EditorView,
66
keymap,
@@ -21,7 +21,13 @@ import {
2121
foldGutter,
2222
foldKeymap,
2323
} from '@codemirror/language';
24-
import {defaultKeymap, history, historyKeymap} from '@codemirror/commands';
24+
25+
import {javascript} from '@codemirror/lang-javascript';
26+
import {python} from '@codemirror/lang-python';
27+
import {cpp} from '@codemirror/lang-cpp';
28+
import {java} from '@codemirror/lang-java';
29+
30+
import {defaultKeymap, history, historyKeymap, indentMore, indentLess} from '@codemirror/commands';
2531
import {searchKeymap, highlightSelectionMatches} from '@codemirror/search';
2632
import {
2733
autocompletion,
@@ -34,18 +40,32 @@ import {yCollab} from 'y-codemirror.next';
3440
import * as Y from 'yjs';
3541
import {Awareness} from 'y-protocols/awareness';
3642

43+
const languageCompartment = new Compartment();
44+
45+
const languageMap: { [key: string]: () => Extension } = {
46+
javascript: () => javascript(),
47+
python: () => python(),
48+
cpp: () => cpp(),
49+
java: () => java(),
50+
default: () => [], // plain text mode
51+
};
52+
3753
interface CodeMirrorProps {
3854
ytext: Y.Text;
3955
awareness: Awareness;
56+
languageConfig: string;
4057
}
4158

42-
export default function CodeMirror({ytext, awareness}: CodeMirrorProps) {
59+
export default function CodeMirror({ytext, awareness, languageConfig}: CodeMirrorProps) {
4360
const editorRef = useRef<HTMLDivElement>(null);
4461
const viewRef = useRef<EditorView | null>(null);
4562

4663
useEffect(() => {
4764
if (!editorRef.current || !ytext || !awareness) return;
4865

66+
const lazyInitialLang = languageMap[languageConfig.toLowerCase()] || languageMap.default;
67+
const initialLanguageExtension = lazyInitialLang();
68+
4969
// Create the editor view with yCollab extension
5070
const view = new EditorView({
5171
doc: ytext.toString(),
@@ -68,7 +88,9 @@ export default function CodeMirror({ytext, awareness}: CodeMirrorProps) {
6888
// Re-indent lines when typing specific input
6989
indentOnInput(),
7090
// Highlight syntax with a default style
71-
syntaxHighlighting(defaultHighlightStyle),
91+
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
92+
// Dyanmic configuration for language mode
93+
languageCompartment.of(initialLanguageExtension),
7294
// Highlight matching brackets near cursor
7395
bracketMatching(),
7496
// Automatically close brackets
@@ -88,6 +110,17 @@ export default function CodeMirror({ytext, awareness}: CodeMirrorProps) {
88110
// Yjs collaboration extension for CodeMirror 6
89111
yCollab(ytext, awareness),
90112
keymap.of([
113+
{
114+
key: "Tab",
115+
preventDefault: true,
116+
run: indentMore,
117+
},
118+
{
119+
key: "Shift-Tab",
120+
preventDefault: true,
121+
run: indentLess,
122+
},
123+
91124
// Closed-brackets aware backspace
92125
...closeBracketsKeymap,
93126
// A large set of basic bindings
@@ -117,6 +150,20 @@ export default function CodeMirror({ytext, awareness}: CodeMirrorProps) {
117150
};
118151
}, [ytext, awareness]);
119152

153+
// Effect to handle language changes
154+
useEffect(() => {
155+
const view = viewRef.current;
156+
if (!view) return;
157+
158+
const newLazyLangFn = languageMap[languageConfig.toLowerCase()] || languageMap.default;
159+
const newExtension = newLazyLangFn();
160+
161+
// Reconfigures compartment with the new language extension
162+
view.dispatch({
163+
effects: languageCompartment.reconfigure(newExtension),
164+
});
165+
}, [languageConfig]);
166+
120167
return (
121168
<div
122169
ref={editorRef}
Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,45 @@
11
import useCollabEditor from '../hooks/useCollabEditor';
22
import CodeMirror from './CodeMirror';
3+
import React from 'react';
4+
5+
const LANGUAGE_OPTIONS = [
6+
{ value: 'python', label: 'Python' },
7+
{ value: 'javascript', label: 'JavaScript' },
8+
{ value: 'cpp', label: 'C++' },
9+
{ value: 'java', label: 'Java' },
10+
{ value: 'default', label: 'Plain Text' },
11+
];
312

413
export default function CollabEditor({roomId}: {roomId: string}) {
5-
const {ytext, awareness, isReady} = useCollabEditor({roomId});
14+
const {ytext, awareness, isReady, languageConfig, setSharedLanguage} = useCollabEditor({roomId});
615
if (!isReady || !ytext) {
716
return <div>Loading...</div>;
817
}
918

19+
const handleLanguageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
20+
setSharedLanguage(event.target.value);
21+
};
22+
1023
return (
1124
<div style={{padding: '20px'}}>
12-
<h2>Happy Coding :D</h2>
13-
<CodeMirror ytext={ytext} awareness={awareness} />
25+
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px'}}>
26+
<h2>Happy Coding :D</h2>
27+
<label>
28+
Language:
29+
<select
30+
value={languageConfig}
31+
onChange={handleLanguageChange}
32+
style={{marginLeft: '10px', padding: '5px', borderRadius: '4px', border: '1px solid #ccc'}}
33+
>
34+
{LANGUAGE_OPTIONS.map(option => (
35+
<option key={option.value} value={option.value}>
36+
{option.label}
37+
</option>
38+
))}
39+
</select>
40+
</label>
41+
</div>
42+
<CodeMirror ytext={ytext} awareness={awareness} languageConfig={languageConfig} />
1443
</div>
1544
);
1645
}

frontend/src/collaboration/hooks/useCollabEditor.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// src/codemirroreditor.tsx
2-
import React, {useEffect, useRef, useState} from 'react';
2+
import React, {useEffect, useRef, useState, useCallback} from 'react';
33
import YPartyKitProvider from 'y-partykit/provider';
44
import * as Y from 'yjs';
55
import toast from 'react-hot-toast';
@@ -92,15 +92,32 @@ const handleUsersRemoved = (
9292
});
9393
};
9494

95+
interface CollabEditorHook {
96+
provider: YPartyKitProvider | null;
97+
ytext: Y.Text | null;
98+
awareness: any;
99+
isReady: boolean;
100+
languageConfig: string; // current selected language
101+
setSharedLanguage: (newLang: string) => void;
102+
}
95103

96-
export default function useCollabEditor({roomId}: {roomId: string}) {
104+
export default function useCollabEditor({roomId}: {roomId: string}): CollabEditorHook {
97105
const [isReady, setIsReady] = useState<boolean>(false);
106+
const [languageConfig, setLanguageConfig] = useState<string>('python'); // Default to Python
107+
const configMapRef = useRef<Y.Map<string> | null>(null);
98108
const ytextRef = useRef<Y.Text | null>(null);
99109
const providerRef = useRef<YPartyKitProvider | null>(null);
100110
const awarenessRef = useRef<any>(null);
101111
const isFirstChangeRef = useRef<boolean>(true);
102112
const userNamesRef = useRef<Map<number, string>>(new Map());
103113
const recentlyRemovedRef = useRef<Set<number>>(new Set());
114+
115+
const setSharedLanguage = useCallback((newLang: string) => {
116+
if (configMapRef.current) {
117+
configMapRef.current.set('language', newLang);
118+
}
119+
}, []);
120+
104121
const timeoutRefs = useRef(new Map<number, NodeJS.Timeout>());
105122

106123
useEffect(() => {
@@ -124,6 +141,23 @@ export default function useCollabEditor({roomId}: {roomId: string}) {
124141
const ytext = provider.doc.getText('codemirror');
125142
ytextRef.current = ytext;
126143

144+
const configMap = provider.doc.getMap<string>('config');
145+
configMapRef.current = configMap;
146+
147+
if (!configMap.get('language')) {
148+
console.log('Setting default language: python');
149+
configMap.set('language', 'python');
150+
}
151+
setLanguageConfig(configMap.get('language') || 'python');
152+
153+
const configMapHandler = () => {
154+
const newLang = configMap.get('language') || 'python';
155+
setLanguageConfig(newLang);
156+
console.log('Shared language updated to:', newLang);
157+
};
158+
159+
configMap.observe(configMapHandler);
160+
127161
// Sets up user awareness
128162
const currUserId = provider.awareness.clientID;
129163
const username = getRandomName();
@@ -180,6 +214,7 @@ export default function useCollabEditor({roomId}: {roomId: string}) {
180214
return () => {
181215
console.log('Cleanup! destroying provider for room:', roomId);
182216

217+
configMap.unobserve(configMapHandler);
183218
provider.awareness.off('change', awarenessChangeHandler);
184219
userNamesRef.current.clear();
185220
recentlyRemovedRef.current.clear();
@@ -197,13 +232,16 @@ export default function useCollabEditor({roomId}: {roomId: string}) {
197232
providerRef.current = null;
198233
ytextRef.current = null;
199234
awarenessRef.current = null;
235+
configMapRef.current = null;
200236
};
201-
}, [roomId]);
237+
}, [roomId, setSharedLanguage]);
202238

203239
return {
204240
provider: providerRef.current,
205241
ytext: ytextRef.current,
206242
awareness: awarenessRef.current,
207243
isReady,
244+
languageConfig,
245+
setSharedLanguage,
208246
};
209247
}

0 commit comments

Comments
 (0)