Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
mmacfadden committed Mar 4, 2019
0 parents commit c743f8d
Show file tree
Hide file tree
Showing 28 changed files with 1,868 additions and 0 deletions.
17 changes: 17 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"presets": [
["@babel/env", {
"targets": {
"browsers": ["last 2 versions"]
}
}],
"@babel/typescript"
],
"plugins": [
"transform-class-properties",
["module-resolver", {
"extensions": [".js", ".ts"],
"root": ["./src/js"]
}]
]
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.idea
node_modules
dist
6 changes: 6 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
language: node_js

node_js:
- "10.10"

script: npm run dist
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Change Log

## [v0.1.0](https://github.com/convergencelabs/monaco-collab-ext/tree/0.1.0) (2019-03-03)

- Initial release.



18 changes: 18 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Copyright (c) 2019 Convergence Labs, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
116 changes: 116 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
## Monaco Collaborative Extensions
[![Build Status](https://travis-ci.org/convergencelabs/monaco-collab-ext.svg?branch=master)](https://travis-ci.org/convergencelabs/monaco-collab-ext)

Enhances the [Monaco Editor](https://github.com/Microsoft/monaco-editor) by adding the ability to render cues about what remote users are doing in the system.

![demo graphic](./docs/demo.gif "Shared Cursors and Selections")

## Installation

Install package with NPM and add it to your development dependencies:

```npm install --save-dev @convergence/ace-collab-ext```

## Demo
Go [here](https://examples.convergence.io/monaco/index.html) to see a live demo of multiple cursors, multiple selections, and remote scrollbars (Visit on multiple browsers, or even better, point a friend to it too). This uses [Convergence](https://convergence.io) to handle the synchronization of data and user actions.

## Usage

### RemoteCursorManager
The RemoteCursorManager allows you to easily render the cursors of other users
working in the same document. The cursor position can be represented as either
a single linear index or as a 2-dimensional position in the form of
```{lineNumber: 0, column: 10}```.

```JavaScript
const editor = monaco.editor.create(document.getElementById("editor"), {
value: "function helloWolrd = () => { console.log('hello world!')",
theme: "vs-dark'",
language: 'javascript'
});

const remoteCursorManager = new MonacoCollabExt.RemoteCursorManager({
editor: editor,
tooltips: true,
tooltipDuration: 2
});

const cursor = remoteCursorManager.addCursor("jDoe", "blue", "John Doe");

// Set the position of the cursor.
cursor.setOffset(4);

// Hide the cursor
cursor.hide();

// Show the cursor
cursor.show();

// Remove the cursor.
cursor.dispose();
```

### RemoteSelectionManager
The RemoteSelectionManager allows you to easily render the selection of other
users working in the same document.

```JavaScript
const editor = monaco.editor.create(document.getElementById("editor"), {
value: "function helloWolrd = () => { console.log('hello world!')",
theme: "vs-dark'",
language: 'javascript'
});

const remoteSelectionManager = new MonacoCollabExt.RemoteSelectionManager({editor: editor});

const selection = remoteSelectionManager.addSelection("jDoe", "blue");

// Set the range of the selection using zero-based offsets.
selection.setOffsets(45, 55);

// Hide the selection
selection.hide();

// Show the selection
selection.show();

// Remove the selection.
selection.dispose();
```

### EditorContentManager
The EditorContentManager simplifies dealing with local and remote changes
to the editor.

```JavaScript
const editor = monaco.editor.create(document.getElementById("editor"), {
value: "function helloWolrd = () => { console.log('hello world!')",
theme: "vs-dark'",
language: 'javascript'
});

const contentManager = new MonacoCollabExt.EditorContentManager({
editor: editor,
onInsert(index, text) {
console.log("Insert", index, text);
},
onReplace(index, length, text) {
console.log("Replace", index, length, text);
},
onDelete(index, length) {
console.log("Delete", index, length);
}
});

// Insert text into the editor at offset 5.
contentManager.insert(5, "some text");

// Replace the text in the editor at range 5 - 10.
contentManager.replace(5, 10, "some text");

// Delete the text in the editor at range 5 - 10.
contentManager.delete(5, 10);

// Release resources when done
contentManager.dispose();
```
5 changes: 5 additions & 0 deletions copyright-header.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**!
© 2019 Convergence Labs, Inc.
@version <%= package.version %>
@license MIT
*/
Binary file added docs/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Monaco Collaborative Extensions Example
This directory contains a vanilla javascript example of using this library. Simply run `npm run dist` and then open the index.html in a web browser.
84 changes: 84 additions & 0 deletions example/editor_contents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
var editorContents = `var observableProto;
/**
* Represents a push-style collection.
*/
var Observable = Rx.Observable = (function () {
function makeSubscribe(self, subscribe) {
return function (o) {
var oldOnError = o.onError;
o.onError = function (e) {
makeStackTraceLong(e, self);
oldOnError.call(o, e);
};
return subscribe.call(self, o);
};
}
function Observable() {
if (Rx.config.longStackSupport && hasStacks) {
var oldSubscribe = this._subscribe;
var e = tryCatch(thrower)(new Error()).e;
this.stack = e.stack.substring(e.stack.indexOf('\\n') + 1);
this._subscribe = makeSubscribe(this, oldSubscribe);
}
}
observableProto = Observable.prototype;
/**
* Determines whether the given object is an Observable
* @param {Any} An object to determine whether it is an Observable
* @returns {Boolean} true if an Observable, else false.
*/
Observable.isObservable = function (o) {
return o && isFunction(o.subscribe);
};
/**
* Subscribes an o to the observable sequence.
* @param {Mixed} [oOrOnNext] The object that is to receive notifications or an action to invoke for each element in the observable sequence.
* @param {Function} [onError] Action to invoke upon exceptional termination of the observable sequence.
* @param {Function} [onCompleted] Action to invoke upon graceful termination of the observable sequence.
* @returns {Diposable} A disposable handling the subscriptions and unsubscriptions.
*/
observableProto.subscribe = observableProto.forEach = function (oOrOnNext, onError, onCompleted) {
return this._subscribe(typeof oOrOnNext === 'object' ?
oOrOnNext :
observerCreate(oOrOnNext, onError, onCompleted));
};
/**
* Subscribes to the next value in the sequence with an optional "this" argument.
* @param {Function} onNext The function to invoke on each element in the observable sequence.
* @param {Any} [thisArg] Object to use as this when executing callback.
* @returns {Disposable} A disposable handling the subscriptions and unsubscriptions.
*/
observableProto.subscribeOnNext = function (onNext, thisArg) {
return this._subscribe(observerCreate(typeof thisArg !== 'undefined' ? function(x) { onNext.call(thisArg, x); } : onNext));
};
/**
* Subscribes to an exceptional condition in the sequence with an optional "this" argument.
* @param {Function} onError The function to invoke upon exceptional termination of the observable sequence.
* @param {Any} [thisArg] Object to use as this when executing callback.
* @returns {Disposable} A disposable handling the subscriptions and unsubscriptions.
*/
observableProto.subscribeOnError = function (onError, thisArg) {
return this._subscribe(observerCreate(null, typeof thisArg !== 'undefined' ? function(e) { onError.call(thisArg, e); } : onError));
};
/**
* Subscribes to the next value in the sequence with an optional "this" argument.
* @param {Function} onCompleted The function to invoke upon graceful termination of the observable sequence.
* @param {Any} [thisArg] Object to use as this when executing callback.
* @returns {Disposable} A disposable handling the subscriptions and unsubscriptions.
*/
observableProto.subscribeOnCompleted = function (onCompleted, thisArg) {
return this._subscribe(observerCreate(null, null, typeof thisArg !== 'undefined' ? function() { onCompleted.call(thisArg); } : onCompleted));
};
return Observable;
})();`;
19 changes: 19 additions & 0 deletions example/example.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.body {
margin: 0;
}

.editors {
display: flex;
flex-direction: row;
flex: 1;
}

.editor-column {
flex: 1;
}

.editor {
height: 500px;
border: 1px solid grey;
margin-right: 20px;
}
80 changes: 80 additions & 0 deletions example/example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
const sourceUser = {
id: "source",
label: "Source User",
color: "orange"
};

const staticUser = {
id: "static",
label: "Static User",
color: "blue"
};

require.config({ paths: { 'vs': '../node_modules/monaco-editor/min/vs' }});
require(['vs/editor/editor.main', 'MonacoCollabExt'], function(m, MonacoCollabExt) {

//
// Create the target editor where events will be played into.
//
const target = monaco.editor.create(document.getElementById("target-editor"), {
value: editorContents,
theme: "vs-dark'",
language: 'javascript'
});

const remoteCursorManager = new MonacoCollabExt.RemoteCursorManager({
editor: target,
tooltips: true,
tooltipDuration: 2
});
const sourceUserCursor = remoteCursorManager.addCursor(sourceUser.id, sourceUser.color, sourceUser.label);
const staticUserCursor = remoteCursorManager.addCursor(staticUser.id, staticUser.color, staticUser.label);

const remoteSelectionManager = new MonacoCollabExt.RemoteSelectionManager({editor: target});
remoteSelectionManager.addSelection(sourceUser.id, sourceUser.color);
remoteSelectionManager.addSelection(staticUser.id, staticUser.color);

const targetContentManager = new MonacoCollabExt.EditorContentManager({
editor: target
});

//
// Faked other user.
//
staticUserCursor.setOffset(50);
remoteSelectionManager.setSelectionOffsets(staticUser.id, 40, 50);


//
// Create the source editor were events will be generated.
//
const source = monaco.editor.create(document.getElementById("source-editor"), {
value: editorContents,
theme: "vs-dark'",
language: 'javascript'
});

source.onDidChangeCursorPosition(e => {
const offset = source.getModel().getOffsetAt(e.position);
sourceUserCursor.setOffset(offset);
});

source.onDidChangeCursorSelection(e => {
const startOffset = source.getModel().getOffsetAt(e.selection.getStartPosition());
const endOffset = source.getModel().getOffsetAt(e.selection.getEndPosition());
remoteSelectionManager.setSelectionOffsets(sourceUser.id, startOffset, endOffset);
});

const sourceContentManager = new MonacoCollabExt.EditorContentManager({
editor: source,
onInsert(index, text) {
targetContentManager.insert(index, text);
},
onReplace(index, length, text) {
targetContentManager.replace(index, length, text);
},
onDelete(index, length) {
targetContentManager.delete(index, length);
}
});
});
Loading

0 comments on commit c743f8d

Please sign in to comment.