add editor live
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details

pull/11/head
Bastien OLLIER 11 months ago
parent 57ee11bfea
commit 819c0e2c74

Binary file not shown.

10
package-lock.json generated

@ -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",

@ -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",

@ -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 {}

@ -0,0 +1,115 @@
<div id="editor">
<div id="editor-bar-header">
<div class="editor-section-bar-header">
<div class="param-editor">
<label for="fileInput">
<svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g
id="SVGRepo_tracerCarrier"
stroke-linecap="round"
stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M1 5C1 3.34315 2.34315 2 4 2H8.55848C9.84977 2 10.9962 2.82629 11.4045 4.05132L11.7208 5H20C21.1046 5 22 5.89543 22 7V9.00961C23.1475 9.12163 23.9808 10.196 23.7695 11.3578L22.1332 20.3578C21.9603 21.3087 21.132 22 20.1654 22H3C1.89543 22 1 21.1046 1 20V5ZM20 9V7H11.7208C10.8599 7 10.0956 6.44914 9.82339 5.63246L9.50716 4.68377C9.37105 4.27543 8.98891 4 8.55848 4H4C3.44772 4 3 4.44772 3 5V12.2709L3.35429 10.588C3.54913 9.66249 4.36562 9 5.31139 9H20ZM3.36634 20C3.41777 19.9109 3.4562 19.8122 3.47855 19.706L5.31139 11L21 11H21.8018L20.1654 20L3.36634 20Z"
fill="#000000"></path>
</g>
</svg>
</label>
<input
style="display: none"
type="file"
id="fileInput"
(change)="loadFromFile($event)" />
</div>
<div class="param-editor">
<button class="button-icon" type="button" (click)="saveToFile()">
<svg
width="800px"
height="800px"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M3 15C3 17.8284 3 19.2426 3.87868 20.1213C4.75736 21 6.17157 21 9 21H15C17.8284 21 19.2426 21 20.1213 20.1213C21 19.2426 21 17.8284 21 15"
stroke="#1C274C"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M12 3V16M12 16L16 11.625M12 16L8 11.625"
stroke="#1C274C"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</button>
</div>
</div>
<div class="editor-section-bar-header">
@if (errorMessage) {
<div class="param-editor">
<p style="color: red">{{ errorMessage }}</p>
</div>
}
<button
class="button-join"
type="button"
(click)="onRunButtonClicked()"
[disabled]="isLoaded">
Créer une salle
</button>
<select id="language" [(ngModel)]="selectedLanguage">
@for (language of languages; track language.name) {
<option [ngValue]="language">{{ language.name }}</option>
}
</select>
<div class="param-editor">
<button
class="button-icon button-run"
type="button"
(click)="onRunButtonClicked()"
[disabled]="isLoaded">
<div>RUN</div>
<svg
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
fill="#000000">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g
id="SVGRepo_tracerCarrier"
stroke-linecap="round"
stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path
d="M2.78 2L2 2.41v12l.78.42 9-6V8l-9-6zM3 13.48V3.35l7.6 5.07L3 13.48z"></path>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6 14.683l8.78-5.853V8L6 2.147V3.35l7.6 5.07L6 13.48v1.203z"></path>
</g>
</svg>
</button>
</div>
</div>
</div>
<div class="editor-center">
<div class="editor-child-element">
<codemirror6-editor [(ngModel)]="editorContent" [extensions]="extensions">
</codemirror6-editor>
</div>
<div class="editor-child-element">
<pre id="resultDiv" [innerHTML]="resultContent | safeHTML"></pre>
</div>
</div>
</div>

@ -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%;
}

@ -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<EditorLiveComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditorLiveComponent],
});
fixture = TestBed.createComponent(EditorLiveComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<number, (value: any) => 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<string, unknown>): Promise<any> {
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<boolean> {
// 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<readonly Update[]> {
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)));

@ -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({

Loading…
Cancel
Save