You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
468 lines
15 KiB
468 lines
15 KiB
import {Component, ElementRef, Input, OnDestroy, OnInit, ViewChild} from '@angular/core';
|
|
import {v4} from 'uuid';
|
|
import {ApiService, ResourceNotAvailableOfflineError} from '../../../service/api.service';
|
|
import {EditorNodeContract} from '../../nodes/EditorNode.contract';
|
|
import {EditorService} from '../../../service/editor.service';
|
|
import {EditorComponent} from 'ngx-monaco-editor';
|
|
import * as MonacoCollabExt from '@convergencelabs/monaco-collab-ext';
|
|
import {FlitterSocketConnection, FlitterSocketServerClientTransaction} from '../../../flitter-socket';
|
|
import {debug} from '../../../utility';
|
|
import {environment} from "../../../../environments/environment";
|
|
|
|
export interface RemoteUser {
|
|
uuid: string;
|
|
uid: string;
|
|
display: string;
|
|
color: string;
|
|
cursor?: any;
|
|
selection?: any;
|
|
}
|
|
|
|
export interface RemoteInsertOperation {
|
|
type: 'insert';
|
|
index: number;
|
|
text: string;
|
|
}
|
|
|
|
export interface RemoteReplaceOperation {
|
|
type: 'replace';
|
|
index: number;
|
|
text: string;
|
|
length: number;
|
|
}
|
|
|
|
export interface RemoteDeleteOperation {
|
|
type: 'delete';
|
|
index: number;
|
|
length: number;
|
|
}
|
|
|
|
export type RemoteOperation = RemoteInsertOperation | RemoteReplaceOperation | RemoteDeleteOperation;
|
|
|
|
@Component({
|
|
selector: 'editor-code',
|
|
templateUrl: './code.component.html',
|
|
styleUrls: ['./code.component.scss'],
|
|
})
|
|
export class CodeComponent extends EditorNodeContract implements OnInit, OnDestroy {
|
|
@Input() nodeId: string;
|
|
@Input() editorUUID?: string;
|
|
@ViewChild('theEditor') theEditor: EditorComponent;
|
|
@ViewChild('editorContainer') editorContainer: ElementRef;
|
|
public dirty = false;
|
|
protected dbRecord: any = {};
|
|
protected codeRefId!: string;
|
|
public notAvailableOffline = false;
|
|
public containerHeight = 540;
|
|
|
|
protected cursorManager?: MonacoCollabExt.RemoteCursorManager;
|
|
protected selectionManager?: MonacoCollabExt.RemoteSelectionManager;
|
|
protected contentManager?: MonacoCollabExt.EditorContentManager;
|
|
|
|
protected remoteUsers: RemoteUser[] = [];
|
|
protected localUser?: RemoteUser;
|
|
protected socket?: FlitterSocketConnection;
|
|
protected editorGroupID!: string;
|
|
|
|
public editorOptions = {
|
|
theme: this.isDark() ? 'vs-dark' : 'vs',
|
|
language: 'javascript',
|
|
uri: v4(),
|
|
readOnly: false,
|
|
automaticLayout: true,
|
|
scrollBeyondLastLine: false,
|
|
scrollbar: {
|
|
alwaysConsumeMouseWheel: false,
|
|
},
|
|
};
|
|
|
|
public editorValue = '';
|
|
public get readonly() {
|
|
return !this.node || !this.editorService.canEdit();
|
|
}
|
|
|
|
public languageOptions: Array<string> = [
|
|
'ABAP',
|
|
'AES',
|
|
'Apex',
|
|
'AZCLI',
|
|
'Bat',
|
|
'C',
|
|
'Cameligo',
|
|
'Clojure',
|
|
'CoffeeScript',
|
|
'Cpp',
|
|
'Csharp',
|
|
'CSP',
|
|
'CSS',
|
|
'Dockerfile',
|
|
'Fsharp',
|
|
'Go',
|
|
'GraphQL',
|
|
'Handlebars',
|
|
'HTML',
|
|
'INI',
|
|
'Java',
|
|
'JavaScript',
|
|
'JSON',
|
|
'Kotlin',
|
|
'LeSS',
|
|
'Lua',
|
|
'Markdown',
|
|
'MiPS',
|
|
'MSDAX',
|
|
'MySQL',
|
|
'Objective-C',
|
|
'Pascal',
|
|
'Pascaligo',
|
|
'Perl',
|
|
'pgSQL',
|
|
'PHP',
|
|
'Plaintext',
|
|
'Postiats',
|
|
'PowerQuery',
|
|
'PowerShell',
|
|
'Pug',
|
|
'Python',
|
|
'R',
|
|
'Razor',
|
|
'Redis',
|
|
'RedShift',
|
|
'RestructuredText',
|
|
'Ruby',
|
|
'Rust',
|
|
'SB',
|
|
'Scheme',
|
|
'SCSS',
|
|
'Shell',
|
|
'SOL',
|
|
'SQL',
|
|
'St',
|
|
'Swift',
|
|
'TCL',
|
|
'Twig',
|
|
'TypeScript',
|
|
'VB',
|
|
'XML',
|
|
'YAML',
|
|
];
|
|
protected hadLoad = false;
|
|
|
|
constructor(
|
|
public editorService: EditorService,
|
|
public readonly api: ApiService,
|
|
) { super(); }
|
|
|
|
public isDark() {
|
|
return document.body.classList.contains('dark');
|
|
}
|
|
|
|
public isDirty(): boolean | Promise<boolean> {
|
|
return this.dirty;
|
|
}
|
|
|
|
public needsSave(): boolean | Promise<boolean> {
|
|
return this.dirty;
|
|
}
|
|
|
|
public writeChangesToNode(): void | Promise<void> {
|
|
this.node.Value.Mode = 'code';
|
|
this.node.Value.Value = this.codeRefId;
|
|
this.node.value = this.codeRefId;
|
|
}
|
|
|
|
public needsLoad(): boolean | Promise<boolean> {
|
|
return this.node && !this.hadLoad;
|
|
}
|
|
|
|
public performLoad(): void | Promise<void> {
|
|
return new Promise((res, rej) => {
|
|
if ( !this.node.Value ) {
|
|
this.node.Value = {};
|
|
}
|
|
|
|
if ( !this.node.Value.Value && this.editorService.canEdit() ) {
|
|
this.api.createCodium(this.page.UUID, this.node.UUID).then(data => {
|
|
this.dbRecord = data;
|
|
this.node.Value.Mode = 'code';
|
|
this.node.Value.Value = data.UUID;
|
|
this.node.value = data.UUID;
|
|
this.codeRefId = data.UUID;
|
|
this.editorOptions.readOnly = this.readonly;
|
|
this.onSelectChange(false);
|
|
this.hadLoad = true;
|
|
this.notAvailableOffline = false;
|
|
res();
|
|
}).catch(rej);
|
|
} else {
|
|
this.api.getCodium(this.page.UUID, this.node.UUID, this.node.Value.Value, this.node.associatedTypeVersionNum).then(data => {
|
|
this.dbRecord = data;
|
|
this.initialValue = this.dbRecord.code;
|
|
this.editorValue = this.dbRecord.code;
|
|
this.editorOptions.language = this.dbRecord.Language;
|
|
this.codeRefId = this.node.Value.Value;
|
|
this.editorOptions.readOnly = this.readonly;
|
|
this.onSelectChange(false);
|
|
this.hadLoad = true;
|
|
this.notAvailableOffline = false;
|
|
res();
|
|
}).catch(e => {
|
|
if ( e instanceof ResourceNotAvailableOfflineError ) {
|
|
this.notAvailableOffline = true;
|
|
} else {
|
|
rej(e);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
public performSave(): void | Promise<void> {
|
|
if ( !this.editorService.canEdit() ) {
|
|
return;
|
|
}
|
|
|
|
return new Promise((res, rej) => {
|
|
this.dbRecord.code = this.editorValue;
|
|
this.dbRecord.Language = this.editorOptions.language;
|
|
|
|
this.api.saveCodium(this.page.UUID, this.node.UUID, this.node.Value.Value, this.dbRecord).then(data => {
|
|
this.dbRecord = data;
|
|
this.editorOptions.language = this.dbRecord.Language;
|
|
this.editorValue = this.dbRecord.code;
|
|
this.dirty = false;
|
|
res();
|
|
}).catch(rej);
|
|
});
|
|
}
|
|
|
|
public performDelete(): void | Promise<void> {
|
|
return this.api.deleteCodium(this.page.UUID, this.node.UUID, this.node.Value.Value);
|
|
}
|
|
|
|
ngOnInit() {
|
|
this.editorService = this.editorService.getEditor(this.editorUUID);
|
|
|
|
this.editorService.registerNodeEditor(this.nodeId, this).then(() => {
|
|
this.editorOptions.readOnly = !this.editorService.canEdit();
|
|
});
|
|
|
|
const url = `${environment.websocketBase}/api/v1/socket/code/.websocket`;
|
|
debug(`Editor socket URL: ${url}`);
|
|
|
|
if ( !this.editorService.isVersion() ) {
|
|
const socket = new FlitterSocketConnection(url);
|
|
socket.controller(this);
|
|
|
|
socket.on_open().then(() => {
|
|
debug('Connected to code editor socket', socket);
|
|
socket.asyncRequest('subscribe', { resource_id: this.node.Value.Value }).then(([transaction, _, data]) => {
|
|
debug('Subscribed to editor group:', data);
|
|
if ( data.editor_group_id ) {
|
|
this.editorGroupID = data.editor_group_id;
|
|
this.socket = socket;
|
|
this.localUser = data.local_user;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
if ( this.socket ) {
|
|
this.socket.socket.close();
|
|
}
|
|
}
|
|
|
|
onMonacoEditorInit(editor) {
|
|
let ignoreEvent = false;
|
|
const updateHeight = () => {
|
|
const contentHeight = Math.max(540, editor.getContentHeight());
|
|
this.containerHeight = contentHeight;
|
|
|
|
try {
|
|
ignoreEvent = true;
|
|
editor.layout({ width: this.editorContainer.nativeElement.offsetWidth, height: contentHeight });
|
|
} finally {
|
|
ignoreEvent = false;
|
|
}
|
|
};
|
|
|
|
editor.onDidChangeCursorPosition(event => {
|
|
this.socket?.asyncRequest('update_cursor', {
|
|
position: event.position,
|
|
uuid: this.localUser?.uuid,
|
|
editor_group_id: this.editorGroupID,
|
|
});
|
|
});
|
|
|
|
editor.onDidChangeCursorSelection(event => {
|
|
this.socket?.asyncRequest('update_selection', {
|
|
startPosition: {
|
|
lineNumber: event.selection.startLineNumber,
|
|
column: event.selection.startColumn,
|
|
},
|
|
endPosition: {
|
|
lineNumber: event.selection.endLineNumber,
|
|
column: event.selection.endColumn,
|
|
},
|
|
uuid: this.localUser?.uuid,
|
|
editor_group_id: this.editorGroupID,
|
|
});
|
|
});
|
|
|
|
editor.onDidContentSizeChange(updateHeight);
|
|
updateHeight();
|
|
|
|
this.cursorManager = new MonacoCollabExt.RemoteCursorManager({
|
|
editor,
|
|
tooltips: true,
|
|
tooltipDuration: 2,
|
|
});
|
|
|
|
this.selectionManager = new MonacoCollabExt.RemoteSelectionManager({ editor });
|
|
|
|
this.contentManager = new MonacoCollabExt.EditorContentManager({
|
|
editor,
|
|
onInsert: (index, text) => {
|
|
if ( this.readonly ) {
|
|
return;
|
|
}
|
|
|
|
this.socket?.asyncRequest('apply', {
|
|
editor_group_id: this.editorGroupID,
|
|
operations: [
|
|
{
|
|
type: 'insert',
|
|
index,
|
|
text,
|
|
},
|
|
],
|
|
});
|
|
},
|
|
onReplace: (index, length, text) => {
|
|
if ( this.readonly ) {
|
|
return;
|
|
}
|
|
|
|
this.socket?.asyncRequest('apply', {
|
|
editor_group_id: this.editorGroupID,
|
|
operations: [
|
|
{
|
|
type: 'replace',
|
|
index,
|
|
text,
|
|
length,
|
|
},
|
|
],
|
|
});
|
|
},
|
|
onDelete: (index, length) => {
|
|
if ( this.readonly ) {
|
|
return;
|
|
}
|
|
|
|
this.socket?.asyncRequest('apply', {
|
|
editor_group_id: this.editorGroupID,
|
|
operations: [
|
|
{
|
|
type: 'delete',
|
|
index,
|
|
length,
|
|
},
|
|
],
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
applyOperation(op: RemoteOperation) {
|
|
if ( op.type === 'insert' ) {
|
|
this.contentManager?.insert(op.index, op.text);
|
|
} else if ( op.type === 'replace' ) {
|
|
this.contentManager?.replace(op.index, op.length, op.text);
|
|
} else if ( op.type === 'delete' ) {
|
|
this.contentManager?.delete(op.index, op.length);
|
|
}
|
|
}
|
|
|
|
async applyRemoteOperation(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) {
|
|
const ops: RemoteOperation[] = transaction.incoming.operations || [];
|
|
for ( const op of ops ) {
|
|
this.applyOperation(op);
|
|
}
|
|
}
|
|
|
|
async updateCursorPosition(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) {
|
|
const position = transaction.incoming.position;
|
|
const uuid = transaction.incoming.uuid;
|
|
|
|
for ( const user of this.remoteUsers ) {
|
|
if ( user.uuid === uuid ) {
|
|
user.cursor?.setPosition(position);
|
|
}
|
|
}
|
|
}
|
|
|
|
async updateSelection(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) {
|
|
const startPosition = transaction.incoming.startPosition;
|
|
const endPosition = transaction.incoming.endPosition;
|
|
const uuid = transaction.incoming.uuid;
|
|
|
|
for ( const user of this.remoteUsers ) {
|
|
if ( user.uuid === uuid ) {
|
|
if ( startPosition && endPosition ) {
|
|
user.selection?.setPositions(startPosition, endPosition);
|
|
user.selection?.show();
|
|
} else {
|
|
user.selection?.hide();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async setEditorGroupUsers(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) {
|
|
const remoteUsers: RemoteUser[] = Array.isArray(transaction.incoming?.users) ? transaction.incoming.users : [];
|
|
console.log('set editor group users', remoteUsers, transaction);
|
|
for ( const user of this.remoteUsers ) {
|
|
this.cursorManager?.removeCursor(user.uuid);
|
|
user.cursor?.dispose();
|
|
delete user.cursor;
|
|
|
|
this.selectionManager?.removeSelection(user.uuid);
|
|
user.selection?.dispose();
|
|
delete user.selection;
|
|
}
|
|
|
|
while ( !this.cursorManager ) {
|
|
await new Promise(r => setTimeout(r, 500));
|
|
}
|
|
|
|
this.remoteUsers = remoteUsers;
|
|
for ( const user of this.remoteUsers ) {
|
|
user.cursor = this.cursorManager?.addCursor(user.uuid, user.color, user.display);
|
|
user.cursor?.setOffset(0);
|
|
user.cursor?.show();
|
|
|
|
user.selection = this.selectionManager?.addSelection(user.uuid, user.color);
|
|
}
|
|
}
|
|
|
|
public onEditorModelChange($event) {
|
|
if ( this.editorValue !== this.dbRecord.code ) {
|
|
this.dirty = true;
|
|
this.editorService.triggerSave();
|
|
}
|
|
}
|
|
|
|
public onSelectChange(updateDbRecord = true) {
|
|
if ( updateDbRecord ) {
|
|
this.dbRecord.Language = this.editorOptions.language;
|
|
this.editorService.triggerSave();
|
|
this.dirty = true;
|
|
}
|
|
|
|
this.editorOptions = {...this.editorOptions};
|
|
}
|
|
}
|