open-collaboration-monaco
Version:
Connect a single Monaco Editor to an Open Collaboration Tools session
134 lines (119 loc) • 4.63 kB
text/typescript
// ******************************************************************************
// Copyright 2024 TypeFox GmbH
// This program and the accompanying materials are made available under the
// terms of the MIT License, which is available in the project root.
// ******************************************************************************
import * as types from 'open-collaboration-protocol';
import * as awarenessProtocol from 'y-protocols/awareness';
type PeerDecorationOptions = {
selectionClassName: string;
cursorClassName: string;
cursorInvertedClassName: string;
};
export class DisposablePeer {
readonly peer: types.Peer;
color: string | undefined;
private yjsAwareness: awarenessProtocol.Awareness;
readonly decoration: PeerDecorationOptions;
get clientId(): number | undefined {
const states = this.yjsAwareness.getStates() as Map<number, types.ClientAwareness>;
for (const [clientID, state] of states.entries()) {
if (state.peer === this.peer.id) {
return clientID;
}
}
return undefined;
}
get lastUpdated(): number | undefined {
const clientId = this.clientId;
if (clientId !== undefined) {
const meta = this.yjsAwareness.meta.get(clientId);
if (meta) {
return meta.lastUpdated;
}
}
return undefined;
}
constructor(yAwareness: awarenessProtocol.Awareness, peer: types.Peer) {
this.peer = peer;
this.yjsAwareness = yAwareness;
this.decoration = this.createDecorations();
}
private createDecorations(): PeerDecorationOptions {
const color = createColor();
const colorCss = typeof color === 'string' ? color : `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
this.color = colorCss;
const className = `peer-${this.peer.id}`;
const cursorClassName = `${className}-cursor`;
const cursorInvertedClassName = `${className}-cursor-inverted`;
const selectionClassName = `${className}-selection`;
const cursorCss = `.${cursorClassName} {
background-color: ${colorCss} !important;
border-color: ${colorCss} !important;
position: absolute;
border-right: solid 2px;
border-top: solid 2px;
border-bottom: solid 2px;
height: 100%;
box-sizing: border-box;
}`;
generateCSS(cursorCss);
const cursorAfterCss = `.${cursorClassName}::after {
content: "${this.peer.name}";
position: absolute;
transform: translateY(-100%);
padding: 0 4px;
border-radius: 4px 4px 4px 0px;
background-color: ${colorCss};
}`;
generateCSS(cursorAfterCss);
const cursorAfterInvertedCss = `.${cursorClassName}.${cursorInvertedClassName}::after {
transform: translateY(100%);
margin-top: -2px;
border-radius: 0px 4px 4px 4px;
z-index: 1;
}`;
generateCSS(cursorAfterInvertedCss);
const selectionCss = `.${selectionClassName} {
background: ${colorCss} !important;
opacity: 0.25;
}`;
generateCSS(selectionCss);
return {
cursorClassName,
cursorInvertedClassName,
selectionClassName
};
}
}
let colorIndex = 0;
const defaultColors: Array<[number, number, number] | string> = [
'yellow', // Yellow
'green', // Green
'magenta', // Magenta
'lightGreen', // Light green
[255, 178, 123], // Light orange
[255, 157, 242], // Light magenta
[92, 45, 145], // Purple
[0, 178, 148], // Light teal
[255, 241, 0], // Light yellow
[180, 160, 255] // Light purple
];
const knownColors = new Set<string>();
function createColor(): [number, number, number] | string {
if (colorIndex < defaultColors.length) {
return defaultColors[colorIndex++];
}
const o = Math.round, r = Math.random, s = 255;
let color: [number, number, number];
do {
color = [o(r() * s), o(r() * s), o(r() * s)];
} while (knownColors.has(JSON.stringify(color)));
knownColors.add(JSON.stringify(color));
return color;
}
function generateCSS(cssText: string) {
const style: HTMLStyleElement = document.createElement('style');
style.textContent = cssText;
document.head.appendChild(style);
}