diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index e0ca603..29b9581 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,6 +1,5 @@ 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'; @@ -12,7 +11,7 @@ import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-poli export const routes: Routes = [ { path: '', component: LandingPageComponent }, { path: 'editor', component: EditorComponent }, - { path: 'editor-live/:idRoom', component: EditorLiveComponent }, + { path: 'editor-live/:idRoom', component: EditorComponent }, { path: 'documentation', component: DocumentationComponent }, { path: 'contact', component: FormComponent }, { path: 'our-story', component: OurStoryComponent }, diff --git a/src/app/components/editor-live/editor-live.component.html b/src/app/components/editor-live/editor-live.component.html deleted file mode 100644 index e1a834e..0000000 --- a/src/app/components/editor-live/editor-live.component.html +++ /dev/null @@ -1,115 +0,0 @@ -
-
-
-
- - -
- -
- -
-
-
- @if (errorMessage) { -
-

{{ errorMessage }}

-
- } - - - - - -
- -
-
-
- -
-
- - -
-
-

-    
-
-
diff --git a/src/app/components/editor-live/editor-live.component.scss b/src/app/components/editor-live/editor-live.component.scss deleted file mode 100644 index 3e19580..0000000 --- a/src/app/components/editor-live/editor-live.component.scss +++ /dev/null @@ -1,85 +0,0 @@ -#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 deleted file mode 100644 index aec1b0b..0000000 --- a/src/app/components/editor-live/editor-live.component.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 4fea997..0000000 --- a/src/app/components/editor-live/editor-live.component.ts +++ /dev/null @@ -1,328 +0,0 @@ -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 540a284..42f9550 100644 --- a/src/app/components/editor/editor.component.ts +++ b/src/app/components/editor/editor.component.ts @@ -1,6 +1,6 @@ -import { Component, ViewChild } from '@angular/core'; +import { Component,Input, ViewChild } from '@angular/core'; import { CodeExecutionService } from 'src/app/services/codeExecution.service'; -import { Compartment } from '@codemirror/state'; +import { Compartment,StateEffect } from '@codemirror/state'; import { CodeMirrorComponent } from '@sandkasten/codemirror6-editor'; import { LanguageDescription } from '@codemirror/language'; import { CODE_DEFAULTS, LANGUAGES } from '../languages'; @@ -36,6 +36,7 @@ import { closeBracketsKeymap, } from '@codemirror/autocomplete'; import { lintKeymap } from '@codemirror/lint'; +import {Connection, getDocument,peerExtension} from '../../services/connection.service' const basicSetup: Extension = (() => [ highlightActiveLineGutter(), @@ -131,6 +132,27 @@ export class EditorComponent { 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!); + let {version, doc} = await getDocument(conn); + + 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 diff --git a/src/app/services/connection.service.ts b/src/app/services/connection.service.ts new file mode 100644 index 0000000..ee29fee --- /dev/null +++ b/src/app/services/connection.service.ts @@ -0,0 +1,104 @@ +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'; + +export 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() + })); + return connection.request({type: "pushUpdates", version, updates}); +} + +function pullUpdates( + connection: Connection, + version: number +): Promise { + + return connection.request({type: "pullUpdates", version}) + .then(updates => updates.map((u: any) => ({ + changes: ChangeSet.fromJSON(u.changes), + clientID: u.clientID + }))); +} + +export function getDocument( + connection: Connection +): Promise<{version: number, doc: Text}> { + + return connection.request({ type: "getDocument" }).then(data => ({ + version: data.version, + doc: Text.of(data.doc.split("\n")) + })); +} + +export function peerExtension(startVersion: number, connection: 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) + + if (this.pushing || !updates.length) return + this.pushing = true + let version = getSyncedVersion(this.view.state) + await pushUpdates(connection, version, updates) + this.pushing = false + // 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) + this.view.dispatch(receiveUpdates(this.view.state, updates)) + } + } + + destroy() { this.done = true } + }) + return [collab({startVersion}), plugin] +} \ No newline at end of file