diff --git a/.nx/cache/18.3.4-nx.linux-x64-gnu.node b/.nx/cache/18.3.4-nx.linux-x64-gnu.node
new file mode 100644
index 0000000..29fb952
Binary files /dev/null and b/.nx/cache/18.3.4-nx.linux-x64-gnu.node differ
diff --git a/package-lock.json b/package-lock.json
index f140234..b4f06de 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
"@angular/platform-browser": "^17.3.7",
"@angular/platform-browser-dynamic": "^17.3.7",
"@angular/router": "^17.3.7",
+ "@codemirror/collab": "^6.1.1",
"@codemirror/lang-cpp": "^6.0.2",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/state": "^6.4.1",
@@ -2447,6 +2448,15 @@
"@lezer/common": "^1.0.0"
}
},
+ "node_modules/@codemirror/collab": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/collab/-/collab-6.1.1.tgz",
+ "integrity": "sha512-tkIn9Jguh98ie12dbBuba3lE8LHUkaMrIFuCVeVGhncSczFdKmX25vC12+58+yqQW5AXi3py6jWY0W+jelyglA==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0"
+ }
+ },
"node_modules/@codemirror/commands": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.5.0.tgz",
diff --git a/package.json b/package.json
index afaa066..d23a824 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"@angular/platform-browser": "^17.3.7",
"@angular/platform-browser-dynamic": "^17.3.7",
"@angular/router": "^17.3.7",
+ "@codemirror/collab": "^6.1.1",
"@codemirror/lang-cpp": "^6.0.2",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/state": "^6.4.1",
diff --git a/src/app/app-routing.module.ts b/src/app/app.routes.ts
similarity index 81%
rename from src/app/app-routing.module.ts
rename to src/app/app.routes.ts
index 2ec202c..e0ca603 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app.routes.ts
@@ -1,6 +1,6 @@
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
+import { Routes } from '@angular/router';
import { EditorComponent } from './components/editor/editor.component';
+import { EditorLiveComponent } from './components/editor-live/editor-live.component';
import { LandingPageComponent } from './components/landing-page/landing-page.component';
import { DocumentationComponent } from './components/documentation/documentation.component';
import { FormComponent } from './components/form/form.component';
@@ -9,18 +9,13 @@ import { OurStoryComponent } from './components/our-story/our-story.component';
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
// Toutes les routes de l'application sont définies ici
-const routes: Routes = [
+export const routes: Routes = [
{ path: '', component: LandingPageComponent },
{ path: 'editor', component: EditorComponent },
+ { path: 'editor-live/:idRoom', component: EditorLiveComponent },
{ path: 'documentation', component: DocumentationComponent },
{ path: 'contact', component: FormComponent },
{ path: 'our-story', component: OurStoryComponent },
{ path: 'terms-of-service', component: TermsOfServiceComponent },
{ path: 'privacy-policy', component: PrivacyPolicyComponent },
];
-
-@NgModule({
- imports: [RouterModule.forRoot(routes)],
- exports: [RouterModule],
-})
-export class AppRoutingModule {}
diff --git a/src/app/components/editor-live/editor-live.component.html b/src/app/components/editor-live/editor-live.component.html
new file mode 100644
index 0000000..e1a834e
--- /dev/null
+++ b/src/app/components/editor-live/editor-live.component.html
@@ -0,0 +1,115 @@
+
diff --git a/src/app/components/editor-live/editor-live.component.scss b/src/app/components/editor-live/editor-live.component.scss
new file mode 100644
index 0000000..3e19580
--- /dev/null
+++ b/src/app/components/editor-live/editor-live.component.scss
@@ -0,0 +1,85 @@
+#editor-bar-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px;
+ color: #fff;
+}
+
+.editor-section-bar-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
+.button-icon {
+ border: 0;
+ background: transparent;
+ cursor: pointer;
+}
+
+svg {
+ width: 30px;
+ height: auto;
+ aspect-ratio: 1;
+ cursor: pointer;
+}
+
+.button-run {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background-color: #04aa6d;
+ border: none;
+ color: white;
+ padding: 12px 16px;
+ font-size: 16px;
+ width: 100px;
+ cursor: pointer;
+ border-radius: 10px;
+}
+
+.button-join {
+ background-color: #1c53bf;
+ border: none;
+ color: white;
+ padding: 12px 16px;
+ font-size: 16px;
+ width: 100px;
+ cursor: pointer;
+ border-radius: 10px;
+}
+
+select {
+ background-color: #0000f0;
+ border: none;
+ color: white;
+ padding: 12px 16px;
+ font-size: 16px;
+ cursor: pointer;
+ border-radius: 10px;
+}
+
+/*editor*/
+
+.editor-center {
+ height: 700px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin: 0px 0px 0px 0px;
+}
+
+.editor-child-element {
+ min-height: 100px;
+ height: 100%;
+ width: 1000px;
+ background-color: black;
+ color: #fff;
+ border-right: 10px solid gray;
+}
+
+::ng-deep .codemirror6-editor {
+ height: 100%;
+}
diff --git a/src/app/components/editor-live/editor-live.component.spec.ts b/src/app/components/editor-live/editor-live.component.spec.ts
new file mode 100644
index 0000000..aec1b0b
--- /dev/null
+++ b/src/app/components/editor-live/editor-live.component.spec.ts
@@ -0,0 +1,21 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { EditorLiveComponent } from './editor-live.component';
+
+describe('EditorLiveComponent', () => {
+ let component: EditorLiveComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [EditorLiveComponent],
+ });
+ fixture = TestBed.createComponent(EditorLiveComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/components/editor-live/editor-live.component.ts b/src/app/components/editor-live/editor-live.component.ts
new file mode 100644
index 0000000..4fea997
--- /dev/null
+++ b/src/app/components/editor-live/editor-live.component.ts
@@ -0,0 +1,328 @@
+import { Component, Input, ViewChild } from '@angular/core';
+import { CodeExecutionService } from 'src/app/services/codeExecution.service';
+import { Compartment, StateEffect } from '@codemirror/state';
+import { CodeMirrorComponent } from '@sandkasten/codemirror6-editor';
+import { LanguageDescription } from '@codemirror/language';
+import { CODE_DEFAULTS, LANGUAGES } from '../languages';
+import { SafeHTMLPipe } from '../../safe-html.pipe';
+import { ReactiveFormsModule, FormsModule } from '@angular/forms';
+import {
+ keymap,
+ highlightSpecialChars,
+ drawSelection,
+ highlightActiveLine,
+ dropCursor,
+ rectangularSelection,
+ crosshairCursor,
+ lineNumbers,
+ highlightActiveLineGutter,
+ gutter,
+} from '@codemirror/view';
+import { Extension, EditorState } from '@codemirror/state';
+import {
+ defaultHighlightStyle,
+ syntaxHighlighting,
+ indentOnInput,
+ bracketMatching,
+ foldGutter,
+ foldKeymap,
+} from '@codemirror/language';
+import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
+import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
+import {
+ autocompletion,
+ completionKeymap,
+ closeBrackets,
+ closeBracketsKeymap,
+} from '@codemirror/autocomplete';
+import { lintKeymap } from '@codemirror/lint';
+
+const basicSetup: Extension = (() => [
+ highlightActiveLineGutter(),
+ highlightSpecialChars(),
+ history(),
+ foldGutter(),
+ drawSelection(),
+ dropCursor(),
+ EditorState.allowMultipleSelections.of(true),
+ indentOnInput(),
+ syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
+ bracketMatching(),
+ closeBrackets(),
+ autocompletion(),
+ rectangularSelection(),
+ crosshairCursor(),
+ highlightActiveLine(),
+ highlightSelectionMatches(),
+ keymap.of([
+ ...closeBracketsKeymap,
+ ...defaultKeymap,
+ ...searchKeymap,
+ ...historyKeymap,
+ ...foldKeymap,
+ ...completionKeymap,
+ ...lintKeymap,
+ ]),
+])();
+
+@Component({
+ selector: 'app-editor-live',
+ templateUrl: './editor-live.component.html',
+ styleUrls: ['./editor-live.component.scss'],
+ standalone: true,
+ imports: [
+ CodeMirrorComponent,
+ ReactiveFormsModule,
+ FormsModule,
+ SafeHTMLPipe,
+ ],
+})
+export class EditorLiveComponent {
+ isLoaded: boolean = false; // Pour vérifier si le chargement est terminé
+
+
+ readonly languages: LanguageDescription[] = LANGUAGES;
+ // Mode par défaut
+ private _selectedLanguage = this.languages.find(
+ (lang) => lang.name === 'JavaScript'
+ )!;
+ get selectedLanguage(): LanguageDescription {
+ return this._selectedLanguage;
+ }
+ set selectedLanguage(value: LanguageDescription) {
+ this._selectedLanguage = value;
+ if (value.name in CODE_DEFAULTS) {
+ this.editorContent =
+ CODE_DEFAULTS[value.name as keyof typeof CODE_DEFAULTS];
+ }
+ this.selectedLanguage.load().then((language) => {
+ this.codemirror.editor?.dispatch({
+ effects: this.languageCompartment.reconfigure(language),
+ });
+ });
+ }
+ private _linesNumbers: boolean = true;
+ get linesNumbers() {
+ return this._linesNumbers;
+ }
+ set linesNumbers(lines: boolean) {
+ this._linesNumbers = lines;
+ this.codemirror.editor?.dispatch({
+ effects: this.gutterCompartment.reconfigure(
+ lines ? lineNumbers() : gutter({})
+ ),
+ });
+ }
+
+ // Contenu de l'éditeur que l'on passera au serveur
+ editorContent: string =
+ CODE_DEFAULTS[this.selectedLanguage.name as keyof typeof CODE_DEFAULTS];
+ resultContent: string = '';
+
+ // Message d'erreur
+ errorMessage: string = '';
+
+ @ViewChild(CodeMirrorComponent) private codemirror!: CodeMirrorComponent;
+
+ private readonly languageCompartment = new Compartment();
+ private readonly gutterCompartment = new Compartment();
+ protected readonly extensions: Extension[] = [
+ basicSetup,
+ this.gutterCompartment.of(lineNumbers()),
+ this.languageCompartment.of(this.selectedLanguage.support!),
+ ];
+
+ private client: WebSocket | undefined;
+ @Input()
+ set idRoom(idRoom: string) {
+ this.client = new WebSocket(`ws://127.0.0.1:3000/live/${idRoom}`);
+ this.client.addEventListener('open', async () => {
+ let conn = new Connection(this.client!);
+ console.log('open')
+ let {version, doc} = await getDocument(conn);
+ console.log('res')
+ this.codemirror.editor?.dispatch({
+ changes: {
+ from: 0,
+ to: this.codemirror.editor.state.doc.length,
+ insert: doc
+ }
+ })
+ this.codemirror.editor?.dispatch({
+ effects: StateEffect.appendConfig.of([peerExtension(version, conn)]),
+ })
+ });
+ }
+
+
+ constructor(private codeExecutionService: CodeExecutionService) {}
+
+ // Efface le contenu de l'éditeur
+ clear(): void {
+ this.editorContent = '';
+ }
+
+ onRunButtonClicked() {
+ // Le code à exécuter est le contenu de l'éditeur
+ const codeToExecute = this.editorContent;
+ this.codeExecutionService.executeCode(
+ codeToExecute,
+ this.selectedLanguage.name
+ );
+
+ this.resultContent = '';
+ }
+
+ loadFromFile(event: Event) {
+ const file = (event.target as HTMLInputElement).files![0];
+ for (const language of this.languages) {
+ if (language.extensions.some((ext) => file.name.endsWith(`.${ext}`))) {
+ this.selectedLanguage = language;
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ this.editorContent = event.target!.result as string;
+ this.errorMessage = '';
+ };
+ reader.readAsText(file);
+ return;
+ }
+ }
+ const extensions = this.languages.flatMap((lang) => lang.extensions);
+ this.errorMessage = `Unsupported language. Please select one of the following languages: ${extensions.join(', ')}.`;
+ console.error(this.errorMessage);
+ }
+
+ saveToFile() {
+ const blob = new Blob([this.editorContent], { type: 'text/plain' });
+ const a = document.createElement('a');
+ a.download = `code.${this.selectedLanguage.extensions![0]}`;
+ a.href = URL.createObjectURL(blob);
+ a.click();
+ }
+}
+
+/*live*/
+import { ChangeSet, Text } from '@codemirror/state';
+import {EditorView} from "codemirror"
+import { Update, collab, getSyncedVersion, receiveUpdates, sendableUpdates } from '@codemirror/collab';
+import { ViewPlugin, ViewUpdate } from '@codemirror/view';
+
+class Connection {
+ private requestId = 0;
+ private resolves: Record void> = {};
+
+ constructor(private client: WebSocket) {
+ client.addEventListener('message', (event) => {
+ const response = JSON.parse(event.data);
+ if ('_request' in response) {
+ const resolve = this.resolves[response._request];
+ if (resolve) {
+ resolve(response.payload);
+ } else {
+ console.error('Received response for unknown or already used request', response._request);
+ }
+ } else {
+ console.error('Received invalid response', response._request);
+ }
+ })
+ }
+
+ request(body: Record): Promise {
+ body['_request'] = this.requestId;
+ this.client.send(JSON.stringify(body));
+ return new Promise((resolve) => this.resolves[this.requestId++] = resolve);
+ }
+}
+
+function pushUpdates(
+ connection: Connection,
+ version: number,
+ fullUpdates: readonly Update[]
+): Promise {
+ // Strip off transaction data
+ let updates = fullUpdates.map(u => ({
+ clientID: u.clientID,
+ changes: u.changes.toJSON()
+ }));
+ console.log("test1");
+ return connection.request({type: "pushUpdates", version, updates});
+}
+
+function pullUpdates(
+ connection: Connection,
+ version: number
+): Promise {
+ console.log("test2");
+
+ return connection.request({type: "pullUpdates", version})
+ .then(updates => updates.map((u: any) => ({
+ changes: ChangeSet.fromJSON(u.changes),
+ clientID: u.clientID
+ })));
+}
+
+function getDocument(
+ connection: Connection
+): Promise<{version: number, doc: Text}> {
+ console.log("test3");
+
+ return connection.request({ type: "getDocument" }).then(data => ({
+ version: data.version,
+ doc: Text.of(data.doc.split("\n"))
+ }));
+}
+
+function peerExtension(startVersion: number, connection: Connection) {
+ console.log(connection)
+ let plugin = ViewPlugin.fromClass(class {
+ private pushing = false
+ private done = false
+
+ constructor(private view: EditorView) { this.pull() }
+
+ update(update: ViewUpdate) {
+ if (update.docChanged) this.push()
+ }
+
+ async push() {
+ let updates = sendableUpdates(this.view.state)
+ console.log("push");
+ if (this.pushing || !updates.length) return
+ this.pushing = true
+ let version = getSyncedVersion(this.view.state)
+ await pushUpdates(connection, version, updates)
+ this.pushing = false
+ console.log("push2");
+
+ // Regardless of whether the push failed or new updates came in
+ // while it was running, try again if there's updates remaining
+ if (sendableUpdates(this.view.state).length)
+ setTimeout(() => this.push(), 100)
+ }
+
+ async pull() {
+ while (!this.done) {
+ let version = getSyncedVersion(this.view.state)
+ let updates = await pullUpdates(connection, version)
+ console.log(updates)
+ this.view.dispatch(receiveUpdates(this.view.state, updates))
+ }
+ }
+
+ destroy() { this.done = true }
+ })
+ return [collab({startVersion}), plugin]
+}
+
+async function createPeer(connection: Connection) {
+ let {version, doc} = await getDocument(connection)
+ console.log(doc);
+ let state = EditorState.create({
+ doc,
+ extensions: [basicSetup, peerExtension(version, connection)],
+ })
+ return new EditorView({state, parent: document.body})
+}
+
+//const client = new WebSocket('ws://127.0.0.1:3000/live/test');
+//client.addEventListener('open', () => createPeer(new Connection(client)));
\ No newline at end of file
diff --git a/src/app/components/editor/editor.component.ts b/src/app/components/editor/editor.component.ts
index d8793e5..540a284 100644
--- a/src/app/components/editor/editor.component.ts
+++ b/src/app/components/editor/editor.component.ts
@@ -176,4 +176,4 @@ export class EditorComponent {
a.href = URL.createObjectURL(blob);
a.click();
}
-}
+}
\ No newline at end of file
diff --git a/src/main.ts b/src/main.ts
index 4d15ebd..504e152 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -8,15 +8,16 @@ import {
HttpClient,
} from '@angular/common/http';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
-import { AppRoutingModule } from './app/app-routing.module';
import { BrowserModule, bootstrapApplication } from '@angular/platform-browser';
import { TranslationService } from './app/services/translation.service';
+import { provideRouter, withComponentInputBinding } from '@angular/router';
+import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
+ provideRouter(routes, withComponentInputBinding()),
importProvidersFrom(
BrowserModule,
- AppRoutingModule,
ReactiveFormsModule,
FormsModule,
TranslateModule.forRoot({