@jupyter/collaboration 1.0.0-alpha.4 → 1.0.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/collaboratorspanel.js +8 -2
- package/lib/components.d.ts +1 -1
- package/lib/cursors.d.ts +23 -0
- package/lib/cursors.js +278 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/tokens.d.ts +1 -1
- package/package.json +12 -10
- package/style/sidepanel.css +4 -0
|
@@ -4,6 +4,10 @@ import * as React from 'react';
|
|
|
4
4
|
import { Panel } from '@lumino/widgets';
|
|
5
5
|
import { ReactWidget } from '@jupyterlab/apputils';
|
|
6
6
|
import { PathExt } from '@jupyterlab/coreutils';
|
|
7
|
+
/**
|
|
8
|
+
* The CSS class added to collaborators panel.
|
|
9
|
+
*/
|
|
10
|
+
const COLLABORATORS_PANEL_CLASS = 'jp-CollaboratorsPanel';
|
|
7
11
|
/**
|
|
8
12
|
* The CSS class added to collaborators list container.
|
|
9
13
|
*/
|
|
@@ -39,6 +43,7 @@ export class CollaboratorsPanel extends Panel {
|
|
|
39
43
|
};
|
|
40
44
|
this._awareness = awareness;
|
|
41
45
|
this._currentUser = currentUser;
|
|
46
|
+
this.addClass(COLLABORATORS_PANEL_CLASS);
|
|
42
47
|
this._body = new CollaboratorsBody(fileopener);
|
|
43
48
|
this.addWidget(this._body);
|
|
44
49
|
this.update();
|
|
@@ -70,8 +75,9 @@ export class CollaboratorsBody extends ReactWidget {
|
|
|
70
75
|
let currentFileLocation = '';
|
|
71
76
|
if (value.current) {
|
|
72
77
|
canOpenCurrent = true;
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
const path = value.current.split(':');
|
|
79
|
+
currentFileLocation = `${path[1]}:${path[2]}`;
|
|
80
|
+
current = PathExt.basename(path[2]);
|
|
75
81
|
current =
|
|
76
82
|
current.length > 25 ? current.slice(0, 12).concat('…') : current;
|
|
77
83
|
separator = '•';
|
package/lib/components.d.ts
CHANGED
package/lib/cursors.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Extension } from '@codemirror/state';
|
|
2
|
+
import { Awareness } from 'y-protocols/awareness';
|
|
3
|
+
import { Text } from 'yjs';
|
|
4
|
+
/**
|
|
5
|
+
* Yjs document objects
|
|
6
|
+
*/
|
|
7
|
+
export type EditorAwareness = {
|
|
8
|
+
/**
|
|
9
|
+
* User related information
|
|
10
|
+
*/
|
|
11
|
+
awareness: Awareness;
|
|
12
|
+
/**
|
|
13
|
+
* Shared editor source
|
|
14
|
+
*/
|
|
15
|
+
ytext: Text;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* CodeMirror extension to display remote users cursors
|
|
19
|
+
*
|
|
20
|
+
* @param config Editor source and awareness
|
|
21
|
+
* @returns CodeMirror extension
|
|
22
|
+
*/
|
|
23
|
+
export declare function remoteUserCursors(config: EditorAwareness): Extension;
|
package/lib/cursors.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
import { Annotation, EditorSelection, Facet } from '@codemirror/state';
|
|
4
|
+
import { EditorView, hoverTooltip, layer, RectangleMarker, tooltips, ViewPlugin } from '@codemirror/view';
|
|
5
|
+
import { JSONExt } from '@lumino/coreutils';
|
|
6
|
+
import { createAbsolutePositionFromRelativePosition, createRelativePositionFromJSON, createRelativePositionFromTypeIndex } from 'yjs';
|
|
7
|
+
/**
|
|
8
|
+
* Facet storing the Yjs document objects
|
|
9
|
+
*/
|
|
10
|
+
const editorAwarenessFacet = Facet.define({
|
|
11
|
+
combine(configs) {
|
|
12
|
+
return configs[configs.length - 1];
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Remote selection theme
|
|
17
|
+
*/
|
|
18
|
+
const remoteSelectionTheme = EditorView.baseTheme({
|
|
19
|
+
'.jp-remote-cursor': {
|
|
20
|
+
borderLeft: '1px solid black',
|
|
21
|
+
marginLeft: '-1px'
|
|
22
|
+
},
|
|
23
|
+
'.jp-remote-cursor.jp-mod-primary': {
|
|
24
|
+
borderLeftWidth: '2px'
|
|
25
|
+
},
|
|
26
|
+
'.jp-remote-selection': {
|
|
27
|
+
opacity: 0.5
|
|
28
|
+
},
|
|
29
|
+
'.cm-tooltip': {
|
|
30
|
+
border: 'none'
|
|
31
|
+
},
|
|
32
|
+
'.cm-tooltip .jp-remote-userInfo': {
|
|
33
|
+
color: 'var(--jp-ui-inverse-font-color0)',
|
|
34
|
+
padding: '0px 2px'
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
// TODO fix which user needs update
|
|
38
|
+
const remoteSelectionsAnnotation = Annotation.define();
|
|
39
|
+
/**
|
|
40
|
+
* Wrapper around RectangleMarker to be able to set the user color for the remote cursor and selection ranges.
|
|
41
|
+
*/
|
|
42
|
+
class RemoteMarker {
|
|
43
|
+
/**
|
|
44
|
+
* Constructor
|
|
45
|
+
*
|
|
46
|
+
* @param style Specific user style to be applied on the marker element
|
|
47
|
+
* @param marker {@link RectangleMarker} to wrap
|
|
48
|
+
*/
|
|
49
|
+
constructor(style, marker) {
|
|
50
|
+
this.style = style;
|
|
51
|
+
this.marker = marker;
|
|
52
|
+
}
|
|
53
|
+
draw() {
|
|
54
|
+
const elt = this.marker.draw();
|
|
55
|
+
for (const [key, value] of Object.entries(this.style)) {
|
|
56
|
+
// @ts-expect-error Unknown key
|
|
57
|
+
elt.style[key] = value;
|
|
58
|
+
}
|
|
59
|
+
return elt;
|
|
60
|
+
}
|
|
61
|
+
eq(other) {
|
|
62
|
+
return (this.marker.eq(other.marker) && JSONExt.deepEqual(this.style, other.style));
|
|
63
|
+
}
|
|
64
|
+
update(dom, oldMarker) {
|
|
65
|
+
for (const [key, value] of Object.entries(this.style)) {
|
|
66
|
+
// @ts-expect-error Unknown key
|
|
67
|
+
dom.style[key] = value;
|
|
68
|
+
}
|
|
69
|
+
return this.marker.update(dom, oldMarker.marker);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Extension defining a new editor layer storing the remote user cursors
|
|
74
|
+
*/
|
|
75
|
+
const remoteCursorsLayer = layer({
|
|
76
|
+
above: true,
|
|
77
|
+
markers(view) {
|
|
78
|
+
const { awareness, ytext } = view.state.facet(editorAwarenessFacet);
|
|
79
|
+
const ydoc = ytext.doc;
|
|
80
|
+
const cursors = [];
|
|
81
|
+
awareness.getStates().forEach((state, clientID) => {
|
|
82
|
+
var _a, _b, _c;
|
|
83
|
+
if (clientID === awareness.doc.clientID) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const cursors_ = state.cursors;
|
|
87
|
+
for (const cursor of cursors_ !== null && cursors_ !== void 0 ? cursors_ : []) {
|
|
88
|
+
if (!(cursor === null || cursor === void 0 ? void 0 : cursor.anchor) || !(cursor === null || cursor === void 0 ? void 0 : cursor.head)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const anchor = createAbsolutePositionFromRelativePosition(cursor.anchor, ydoc);
|
|
92
|
+
const head = createAbsolutePositionFromRelativePosition(cursor.head, ydoc);
|
|
93
|
+
if ((anchor === null || anchor === void 0 ? void 0 : anchor.type) !== ytext || (head === null || head === void 0 ? void 0 : head.type) !== ytext) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const className = ((_a = cursor.primary) !== null && _a !== void 0 ? _a : true)
|
|
97
|
+
? 'jp-remote-cursor jp-mod-primary'
|
|
98
|
+
: 'jp-remote-cursor';
|
|
99
|
+
const cursor_ = EditorSelection.cursor(head.index, head.index > anchor.index ? -1 : 1);
|
|
100
|
+
for (const piece of RectangleMarker.forRange(view, className, cursor_)) {
|
|
101
|
+
// Wrap the rectangle marker to set the user color
|
|
102
|
+
cursors.push(new RemoteMarker({ borderLeftColor: (_c = (_b = state.user) === null || _b === void 0 ? void 0 : _b.color) !== null && _c !== void 0 ? _c : 'black' }, piece));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
return cursors;
|
|
107
|
+
},
|
|
108
|
+
update(update, layer) {
|
|
109
|
+
return !!update.transactions.find(t => t.annotation(remoteSelectionsAnnotation));
|
|
110
|
+
},
|
|
111
|
+
class: 'jp-remote-cursors'
|
|
112
|
+
});
|
|
113
|
+
/**
|
|
114
|
+
* Tooltip extension to display user display name at cursor position
|
|
115
|
+
*/
|
|
116
|
+
const userHover = hoverTooltip((view, pos) => {
|
|
117
|
+
var _a;
|
|
118
|
+
const { awareness, ytext } = view.state.facet(editorAwarenessFacet);
|
|
119
|
+
const ydoc = ytext.doc;
|
|
120
|
+
for (const [clientID, state] of awareness.getStates()) {
|
|
121
|
+
if (clientID === awareness.doc.clientID) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
for (const cursor of (_a = state.cursors) !== null && _a !== void 0 ? _a : []) {
|
|
125
|
+
if (!(cursor === null || cursor === void 0 ? void 0 : cursor.head)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const head = createAbsolutePositionFromRelativePosition(cursor.head, ydoc);
|
|
129
|
+
if ((head === null || head === void 0 ? void 0 : head.type) !== ytext) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// Use some margin around the cursor to display the user.
|
|
133
|
+
if (head.index - 1 <= pos && pos <= head.index + 1) {
|
|
134
|
+
return {
|
|
135
|
+
pos: head.index,
|
|
136
|
+
above: true,
|
|
137
|
+
create: () => {
|
|
138
|
+
var _a, _b, _c, _d;
|
|
139
|
+
const dom = document.createElement('div');
|
|
140
|
+
dom.classList.add('jp-remote-userInfo');
|
|
141
|
+
dom.style.backgroundColor = (_b = (_a = state.user) === null || _a === void 0 ? void 0 : _a.color) !== null && _b !== void 0 ? _b : 'darkgrey';
|
|
142
|
+
dom.textContent =
|
|
143
|
+
(_d = (_c = state.user) === null || _c === void 0 ? void 0 : _c.display_name) !== null && _d !== void 0 ? _d : 'Anonymous';
|
|
144
|
+
return { dom };
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}, {
|
|
152
|
+
hideOn: (tr, tooltip) => !!tr.annotation(remoteSelectionsAnnotation)
|
|
153
|
+
});
|
|
154
|
+
/**
|
|
155
|
+
* Extension defining a new editor layer storing the remote selections
|
|
156
|
+
*/
|
|
157
|
+
const remoteSelectionLayer = layer({
|
|
158
|
+
above: false,
|
|
159
|
+
markers(view) {
|
|
160
|
+
const { awareness, ytext } = view.state.facet(editorAwarenessFacet);
|
|
161
|
+
const ydoc = ytext.doc;
|
|
162
|
+
const cursors = [];
|
|
163
|
+
awareness.getStates().forEach((state, clientID) => {
|
|
164
|
+
var _a, _b, _c;
|
|
165
|
+
if (clientID === awareness.doc.clientID) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const cursors_ = state.cursors;
|
|
169
|
+
for (const cursor of cursors_ !== null && cursors_ !== void 0 ? cursors_ : []) {
|
|
170
|
+
if (((_a = cursor.empty) !== null && _a !== void 0 ? _a : true) || !(cursor === null || cursor === void 0 ? void 0 : cursor.anchor) || !(cursor === null || cursor === void 0 ? void 0 : cursor.head)) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const anchor = createAbsolutePositionFromRelativePosition(cursor.anchor, ydoc);
|
|
174
|
+
const head = createAbsolutePositionFromRelativePosition(cursor.head, ydoc);
|
|
175
|
+
if ((anchor === null || anchor === void 0 ? void 0 : anchor.type) !== ytext || (head === null || head === void 0 ? void 0 : head.type) !== ytext) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const className = 'jp-remote-selection';
|
|
179
|
+
for (const piece of RectangleMarker.forRange(view, className, EditorSelection.range(anchor.index, head.index))) {
|
|
180
|
+
// Wrap the rectangle marker to set the user color
|
|
181
|
+
cursors.push(new RemoteMarker({ backgroundColor: (_c = (_b = state.user) === null || _b === void 0 ? void 0 : _b.color) !== null && _c !== void 0 ? _c : 'black' }, piece));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
return cursors;
|
|
186
|
+
},
|
|
187
|
+
update(update, layer) {
|
|
188
|
+
return !!update.transactions.find(t => t.annotation(remoteSelectionsAnnotation));
|
|
189
|
+
},
|
|
190
|
+
class: 'jp-remote-selections'
|
|
191
|
+
});
|
|
192
|
+
/**
|
|
193
|
+
* CodeMirror extension exchanging and displaying remote user selection ranges (including cursors)
|
|
194
|
+
*/
|
|
195
|
+
const showCollaborators = ViewPlugin.fromClass(class {
|
|
196
|
+
constructor(view) {
|
|
197
|
+
this.editorAwareness = view.state.facet(editorAwarenessFacet);
|
|
198
|
+
this._listener = ({ added, updated, removed }) => {
|
|
199
|
+
const clients = added.concat(updated).concat(removed);
|
|
200
|
+
if (clients.findIndex(id => id !== this.editorAwareness.awareness.doc.clientID) >= 0) {
|
|
201
|
+
// Trick to get the remoteCursorLayers to be updated
|
|
202
|
+
view.dispatch({ annotations: [remoteSelectionsAnnotation.of([])] });
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
this.editorAwareness.awareness.on('change', this._listener);
|
|
206
|
+
}
|
|
207
|
+
destroy() {
|
|
208
|
+
this.editorAwareness.awareness.off('change', this._listener);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Communicate the current user cursor position to all remotes
|
|
212
|
+
*/
|
|
213
|
+
update(update) {
|
|
214
|
+
var _a;
|
|
215
|
+
if (!update.docChanged && !update.selectionSet) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const { awareness, ytext } = this.editorAwareness;
|
|
219
|
+
const localAwarenessState = awareness.getLocalState();
|
|
220
|
+
// set local awareness state (update cursors)
|
|
221
|
+
if (localAwarenessState) {
|
|
222
|
+
const hasFocus = update.view.hasFocus && update.view.dom.ownerDocument.hasFocus();
|
|
223
|
+
const selection = update.state.selection;
|
|
224
|
+
const cursors = new Array();
|
|
225
|
+
if (hasFocus && selection) {
|
|
226
|
+
for (const r of selection.ranges) {
|
|
227
|
+
const primary = r === selection.main;
|
|
228
|
+
const anchor = createRelativePositionFromTypeIndex(ytext, r.anchor);
|
|
229
|
+
const head = createRelativePositionFromTypeIndex(ytext, r.head);
|
|
230
|
+
cursors.push({
|
|
231
|
+
anchor,
|
|
232
|
+
head,
|
|
233
|
+
primary,
|
|
234
|
+
empty: r.empty
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (!localAwarenessState.cursors || cursors.length > 0) {
|
|
238
|
+
const oldCursors = (_a = localAwarenessState.cursors) === null || _a === void 0 ? void 0 : _a.map(cursor => {
|
|
239
|
+
return {
|
|
240
|
+
...cursor,
|
|
241
|
+
anchor: (cursor === null || cursor === void 0 ? void 0 : cursor.anchor)
|
|
242
|
+
? createRelativePositionFromJSON(cursor.anchor)
|
|
243
|
+
: null,
|
|
244
|
+
head: (cursor === null || cursor === void 0 ? void 0 : cursor.head)
|
|
245
|
+
? createRelativePositionFromJSON(cursor.head)
|
|
246
|
+
: null
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
if (!JSONExt.deepEqual(cursors, oldCursors)) {
|
|
250
|
+
// Update cursors
|
|
251
|
+
awareness.setLocalStateField('cursors', cursors);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}, {
|
|
258
|
+
provide: () => {
|
|
259
|
+
return [
|
|
260
|
+
remoteSelectionTheme,
|
|
261
|
+
remoteCursorsLayer,
|
|
262
|
+
remoteSelectionLayer,
|
|
263
|
+
userHover,
|
|
264
|
+
// As we use relative positioning of widget, the tooltip must be positioned absolutely
|
|
265
|
+
// And we attach the tooltip to the body to avoid overflow rules
|
|
266
|
+
tooltips({ position: 'absolute', parent: document.body })
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
/**
|
|
271
|
+
* CodeMirror extension to display remote users cursors
|
|
272
|
+
*
|
|
273
|
+
* @param config Editor source and awareness
|
|
274
|
+
* @returns CodeMirror extension
|
|
275
|
+
*/
|
|
276
|
+
export function remoteUserCursors(config) {
|
|
277
|
+
return [editorAwarenessFacet.of(config), showCollaborators];
|
|
278
|
+
}
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
package/lib/tokens.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyter/collaboration",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.6",
|
|
4
4
|
"description": "JupyterLab - Real-Time Collaboration Widgets",
|
|
5
5
|
"homepage": "https://github.com/jupyterlab/jupyter_collaboration",
|
|
6
6
|
"bugs": {
|
|
@@ -39,25 +39,27 @@
|
|
|
39
39
|
"watch": "tsc -b --watch"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@
|
|
43
|
-
"@
|
|
44
|
-
"@jupyterlab/
|
|
45
|
-
"@jupyterlab/
|
|
46
|
-
"@
|
|
47
|
-
"@
|
|
48
|
-
"@lumino/
|
|
42
|
+
"@codemirror/state": "^6.2.0",
|
|
43
|
+
"@codemirror/view": "^6.7.0",
|
|
44
|
+
"@jupyterlab/apputils": "^4.0.0-alpha.22",
|
|
45
|
+
"@jupyterlab/coreutils": "^6.0.0-alpha.22",
|
|
46
|
+
"@jupyterlab/services": "^7.0.0-alpha.22",
|
|
47
|
+
"@jupyterlab/ui-components": "^4.0.0-alpha.37",
|
|
48
|
+
"@lumino/coreutils": "^2.0.0",
|
|
49
|
+
"@lumino/virtualdom": "^2.0.0",
|
|
50
|
+
"@lumino/widgets": "^2.0.0",
|
|
49
51
|
"react": "^18.2.0",
|
|
50
52
|
"y-protocols": "^1.0.5",
|
|
51
53
|
"yjs": "^13.5.40"
|
|
52
54
|
},
|
|
53
55
|
"devDependencies": {
|
|
56
|
+
"@types/react": "^18.0.27",
|
|
54
57
|
"rimraf": "^4.1.2",
|
|
55
|
-
"typescript": "~
|
|
58
|
+
"typescript": "~5.0.2"
|
|
56
59
|
},
|
|
57
60
|
"publishConfig": {
|
|
58
61
|
"access": "public"
|
|
59
62
|
},
|
|
60
|
-
"jupyterlab": {},
|
|
61
63
|
"typedoc": {
|
|
62
64
|
"entryPoint": "./src/index.ts",
|
|
63
65
|
"readmeFile": "./README.md",
|
package/style/sidepanel.css
CHANGED