Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support collaboration feature #13309

Merged
merged 13 commits into from
Aug 28, 2024
Merged
1 change: 1 addition & 0 deletions examples/browser-only/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@theia/api-samples": "1.52.0",
"@theia/bulk-edit": "1.52.0",
"@theia/callhierarchy": "1.52.0",
"@theia/collaboration": "1.52.0",
"@theia/console": "1.52.0",
"@theia/core": "1.52.0",
"@theia/debug": "1.52.0",
Expand Down
3 changes: 3 additions & 0 deletions examples/browser-only/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
{
"path": "../../packages/callhierarchy"
},
{
"path": "../../packages/collaboration"
},
{
"path": "../../packages/console"
},
Expand Down
1 change: 1 addition & 0 deletions examples/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@theia/api-samples": "1.52.0",
"@theia/bulk-edit": "1.52.0",
"@theia/callhierarchy": "1.52.0",
"@theia/collaboration": "1.52.0",
"@theia/console": "1.52.0",
"@theia/core": "1.52.0",
"@theia/debug": "1.52.0",
Expand Down
3 changes: 3 additions & 0 deletions examples/browser/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
{
"path": "../../packages/callhierarchy"
},
{
"path": "../../packages/collaboration"
},
{
"path": "../../packages/console"
},
Expand Down
1 change: 1 addition & 0 deletions examples/electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@theia/api-samples": "1.52.0",
"@theia/bulk-edit": "1.52.0",
"@theia/callhierarchy": "1.52.0",
"@theia/collaboration": "1.52.0",
"@theia/console": "1.52.0",
"@theia/core": "1.52.0",
"@theia/debug": "1.52.0",
Expand Down
3 changes: 3 additions & 0 deletions examples/electron/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
{
"path": "../../packages/callhierarchy"
},
{
"path": "../../packages/collaboration"
},
{
"path": "../../packages/console"
},
Expand Down
10 changes: 10 additions & 0 deletions packages/collaboration/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};
33 changes: 33 additions & 0 deletions packages/collaboration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<div align='center'>

<br />

<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />

<h2>ECLIPSE THEIA - COLLABORATION EXTENSION</h2>

<hr />

</div>

## Description

The `@theia/collaboration` extension features to enable collaboration between multiple peers using Theia.
This is built on top of the [Open Collaboration Tools](https://www.open-collab.tools/) ([GitHub](https://github.com/TypeFox/open-collaboration-tools)) project.

Note that the project is still in a beta phase and can be subject to unexpected breaking changes. This package is therefore in a beta phase as well.

## Additional Information

- [API documentation for `@theia/collaboration`](https://eclipse-theia.github.io/theia/docs/next/modules/collaboration.html)
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)

## License

- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)

## Trademark
"Theia" is a trademark of the Eclipse Foundation
https://www.eclipse.org/theia
57 changes: 57 additions & 0 deletions packages/collaboration/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "@theia/collaboration",
"version": "1.52.0",
"description": "Theia - Collaboration Extension",
"dependencies": {
"@theia/core": "1.52.0",
"@theia/editor": "1.52.0",
"@theia/filesystem": "1.52.0",
"@theia/monaco": "1.52.0",
"@theia/monaco-editor-core": "1.83.101",
"@theia/workspace": "1.52.0",
"open-collaboration-protocol": "0.2.0-next.16e7c68",
"open-collaboration-yjs": "0.2.0-next.16e7c68",
"socket.io-client": "^4.5.3",
"yjs": "^13.6.7",
"lib0": "^0.2.52",
"y-protocols": "^1.0.6"
},
"publishConfig": {
"access": "public"
},
"theiaExtensions": [
{
"frontend": "lib/browser/collaboration-frontend-module"
}
],
"keywords": [
"theia-extension"
],
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
"repository": {
"type": "git",
"url": "https://github.com/eclipse-theia/theia.git"
},
"bugs": {
"url": "https://github.com/eclipse-theia/theia/issues"
},
"homepage": "https://github.com/eclipse-theia/theia",
"files": [
"lib",
"src"
],
"scripts": {
"build": "theiaext build",
"clean": "theiaext clean",
"compile": "theiaext compile",
"lint": "theiaext lint",
"test": "theiaext test",
"watch": "theiaext watch"
},
"devDependencies": {
"@theia/ext-scripts": "1.52.0"
},
"nyc": {
"extends": "../../configs/nyc.json"
}
}
77 changes: 77 additions & 0 deletions packages/collaboration/src/browser/collaboration-color-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// *****************************************************************************
// Copyright (C) 2024 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { injectable } from '@theia/core/shared/inversify';

export interface CollaborationColor {
r: number;
g: number;
b: number;
}

export namespace CollaborationColor {
export function fromString(code: string): CollaborationColor {
if (code.startsWith('#')) {
code = code.substring(1);
}
const r = parseInt(code.substring(0, 2), 16);
const g = parseInt(code.substring(2, 4), 16);
const b = parseInt(code.substring(4, 6), 16);
return { r, g, b };
}

export const Gold = fromString('#FFD700');
export const Tomato = fromString('#FF6347');
export const Aquamarine = fromString('#7FFFD4');
export const Beige = fromString('#F5F5DC');
export const Coral = fromString('#FF7F50');
export const DarkOrange = fromString('#FF8C00');
export const VioletRed = fromString('#C71585');
export const DodgerBlue = fromString('#1E90FF');
export const Chocolate = fromString('#D2691E');
export const LightGreen = fromString('#90EE90');
export const MediumOrchid = fromString('#BA55D3');
export const Orange = fromString('#FFA500');
}

@injectable()
export class CollaborationColorService {

light = 'white';
dark = 'black';

getColors(): CollaborationColor[] {
return [
CollaborationColor.Gold,
CollaborationColor.Aquamarine,
CollaborationColor.Tomato,
CollaborationColor.MediumOrchid,
CollaborationColor.LightGreen,
CollaborationColor.Orange,
CollaborationColor.Beige,
CollaborationColor.Chocolate,
CollaborationColor.VioletRed,
CollaborationColor.Coral,
CollaborationColor.DodgerBlue,
CollaborationColor.DarkOrange
];
}

requiresDarkFont(color: CollaborationColor): boolean {
// From https://stackoverflow.com/a/3943023
return ((color.r * 0.299) + (color.g * 0.587) + (color.b * 0.114)) > 186;
tsmaeder marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// *****************************************************************************
// Copyright (C) 2024 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import * as Y from 'yjs';
import { Disposable, Emitter, Event, URI } from '@theia/core';
import { injectable } from '@theia/core/shared/inversify';
import {
FileChange, FileDeleteOptions,
FileOverwriteOptions, FileSystemProviderCapabilities, FileType, Stat, WatchOptions, FileSystemProviderWithFileReadWriteCapability, FileWriteOptions
} from '@theia/filesystem/lib/common/files';
import { ProtocolBroadcastConnection, Workspace, Peer } from 'open-collaboration-protocol';

export namespace CollaborationURI {

export const scheme = 'collaboration';
tsmaeder marked this conversation as resolved.
Show resolved Hide resolved

export function create(workspace: Workspace, path?: string): URI {
return new URI(`${scheme}:///${workspace.name}${path ? '/' + path : ''}`);
}
}

@injectable()
export class CollaborationFileSystemProvider implements FileSystemProviderWithFileReadWriteCapability {

capabilities = FileSystemProviderCapabilities.FileReadWrite;

protected _readonly: boolean;

get readonly(): boolean {
return this._readonly;
}

set readonly(value: boolean) {
if (this._readonly !== value) {
this._readonly = value;
if (value) {
this.capabilities |= FileSystemProviderCapabilities.Readonly;
} else {
this.capabilities &= ~FileSystemProviderCapabilities.Readonly;
}
this.onDidChangeCapabilitiesEmitter.fire();
}
}

constructor(readonly connection: ProtocolBroadcastConnection, readonly host: Peer, readonly yjs: Y.Doc) {
}

protected encoder = new TextEncoder();
protected decoder = new TextDecoder();
protected onDidChangeCapabilitiesEmitter = new Emitter<void>();
protected onDidChangeFileEmitter = new Emitter<readonly FileChange[]>();
protected onFileWatchErrorEmitter = new Emitter<void>();

get onDidChangeCapabilities(): Event<void> {
return this.onDidChangeCapabilitiesEmitter.event;
}
get onDidChangeFile(): Event<readonly FileChange[]> {
return this.onDidChangeFileEmitter.event;
}
get onFileWatchError(): Event<void> {
return this.onFileWatchErrorEmitter.event;
}
async readFile(resource: URI): Promise<Uint8Array> {
const path = this.getHostPath(resource);
if (this.yjs.share.has(path)) {
const stringValue = this.yjs.getText(path);
return this.encoder.encode(stringValue.toString());
tsmaeder marked this conversation as resolved.
Show resolved Hide resolved
} else {
const data = await this.connection.fs.readFile(this.host.id, path);
return data.content;
}
}
async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
const path = this.getHostPath(resource);
await this.connection.fs.writeFile(this.host.id, path, { content });
}
watch(resource: URI, opts: WatchOptions): Disposable {
return Disposable.NULL;
}
stat(resource: URI): Promise<Stat> {
return this.connection.fs.stat(this.host.id, this.getHostPath(resource));
}
mkdir(resource: URI): Promise<void> {
return this.connection.fs.mkdir(this.host.id, this.getHostPath(resource));
}
async readdir(resource: URI): Promise<[string, FileType][]> {
const record = await this.connection.fs.readdir(this.host.id, this.getHostPath(resource));
return Object.entries(record);
}
delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
return this.connection.fs.delete(this.host.id, this.getHostPath(resource));
}
rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
return this.connection.fs.rename(this.host.id, this.getHostPath(from), this.getHostPath(to));
}

protected getHostPath(uri: URI): string {
const path = uri.path.toString().substring(1).split('/');
tsmaeder marked this conversation as resolved.
Show resolved Hide resolved
return path.slice(1).join('/');
}

triggerEvent(changes: FileChange[]): void {
this.onDidChangeFileEmitter.fire(changes);
}

}
Loading
Loading