Sfoglia il codice sorgente

Add rendering for collaborator cursors.

Ian Rose 8 anni fa
parent
commit
e1cb26bb19

+ 1 - 2
packages/cells/src/widget.ts

@@ -22,7 +22,6 @@ import {
 } from '@phosphor/widgets';
 
 import {
-<<<<<<< HEAD
   IClientSession
 } from '@jupyterlab/apputils';
 
@@ -681,7 +680,7 @@ class MarkdownCell extends Cell {
       timeout: RENDER_TIMEOUT
     });
     this._monitor.activityStopped.connect(()=>{
-      if(this._rendered) {
+      if (this._rendered) {
         this.update();
       }
     }, this);

+ 10 - 0
packages/codeeditor/src/editor.ts

@@ -105,6 +105,16 @@ namespace CodeEditor {
      * A display name added to a selection.
      */
     displayName?: string;
+
+    /**
+     * A CSS string to apply to the selection.
+     */
+    css?: string;
+
+    /**
+     * A color for cursors.
+     */
+    color?: string;
   }
 
   /**

+ 42 - 7
packages/codemirror/src/editor.ts

@@ -4,6 +4,10 @@
 import * as CodeMirror
   from 'codemirror';
 
+import {
+  JSONExt
+} from '@phosphor/coreutils';
+
 import {
   ArrayExt
 } from '@phosphor/algorithm';
@@ -514,7 +518,9 @@ class CodeMirrorEditor implements CodeEditor.IEditor {
     const uuid = args.key;
     if (uuid !== this.uuid) {
       this._cleanSelections(uuid);
-      this._markSelections(uuid, args.newValue);
+      if (args.type !== 'remove') {
+        this._markSelections(uuid, args.newValue);
+      }
     }
   }
 
@@ -535,9 +541,17 @@ class CodeMirrorEditor implements CodeEditor.IEditor {
   private _markSelections(uuid: string, selections: CodeEditor.ITextSelection[]) {
     const markers: CodeMirror.TextMarker[] = [];
     selections.forEach(selection => {
-      const { anchor, head } = this._toCodeMirrorSelection(selection);
-      const markerOptions = this._toTextMarkerOptions(selection);
-      this.doc.markText(anchor, head, markerOptions);
+      // Only render selections if the start is not equal to the end.
+      // In that case, we don't need to render the cursor.
+      if (!JSONExt.deepEqual(selection.start, selection.end)) {
+        const { anchor, head } = this._toCodeMirrorSelection(selection);
+        const markerOptions = this._toTextMarkerOptions(selection.style);
+        markers.push(this.doc.markText(anchor, head, markerOptions));
+      } else {
+        let caret = this._getCaret(selection.uuid);
+        markers.push(this.doc.setBookmark(
+          this._toCodeMirrorPosition(selection.end), {widget: caret}));
+      }
     });
     this.selectionMarkers[uuid] = markers;
   }
@@ -569,7 +583,8 @@ class CodeMirrorEditor implements CodeEditor.IEditor {
     if (style) {
       return {
         className: style.className,
-        title: style.displayName
+        title: style.displayName,
+        css: style.css
       };
     }
     return undefined;
@@ -579,9 +594,17 @@ class CodeMirrorEditor implements CodeEditor.IEditor {
    * Converts an editor selection to a code mirror selection.
    */
   private _toCodeMirrorSelection(selection: CodeEditor.IRange): CodeMirror.Selection {
+    //Selections only appear to render correctly if the anchor
+    //is before the head in the document. That is, reverse selections
+    //do not appear as intended.
+    let forward: boolean = (selection.start.line < selection.end.line) ||
+                           (selection.start.line === selection.end.line &&
+                            selection.start.column <= selection.end.column);
+    let anchor = forward ? selection.start : selection.end;
+    let head = forward ? selection.end : selection.start;
     return {
-      anchor: this._toCodeMirrorPosition(selection.start),
-      head: this._toCodeMirrorPosition(selection.end)
+      anchor: this._toCodeMirrorPosition(anchor),
+      head: this._toCodeMirrorPosition(head)
     };
   }
 
@@ -694,6 +717,18 @@ class CodeMirrorEditor implements CodeEditor.IEditor {
     }
   }
 
+  /**
+   * Construct a caret element representing the position
+   * of a collaborator's cursor.
+   */
+  private _getCaret(uuid: string): HTMLElement {
+    let caret: HTMLElement = document.createElement('span');
+    caret.className = 'jp-CollaboratorCursor';
+    caret.style.borderBottomColor=`${this._selectionStyle.color}`
+    caret.appendChild(document.createTextNode('\u00a0'));
+    return caret;
+  }
+
   private _model: CodeEditor.IModel;
   private _editor: CodeMirror.Editor;
   protected selectionMarkers: { [key: string]: CodeMirror.TextMarker[] | undefined } = {};

+ 9 - 0
packages/codemirror/style/index.css

@@ -66,6 +66,15 @@
   margin-left: calc(-1 * var(--jp-code-padding));
 }
 
+.jp-CollaboratorCursor {
+  border-left: 5px solid transparent;
+  border-right: 5px solid transparent;
+  border-top: none;
+  border-bottom: 3px solid;
+  position: absolute;
+  width: 0;
+}
+
 
 /*
   Here is our jupyter theme for CodeMirror syntax highlighting

+ 31 - 0
packages/fileeditor/src/widget.ts

@@ -48,6 +48,27 @@ class FileEditor extends CodeEditorWrapper {
     context.pathChanged.connect(this._onPathChanged, this);
     context.ready.then(() => { this._onContextReady(); });
     this._onPathChanged();
+
+    if (context.model.modelDB.isCollaborative) {
+      let modelDB = context.model.modelDB;
+      modelDB.connected.then(() => {
+        //Setup the selection style for collaborators
+        let localCollaborator = modelDB.collaborators.localCollaborator;
+        this.editor.uuid = localCollaborator.sessionId;
+        let color = localCollaborator.color;
+        let r = parseInt(color.slice(1,3), 16);
+        let g  = parseInt(color.slice(3,5), 16);
+        let b  = parseInt(color.slice(5,7), 16);
+        this.editor.selectionStyle = {
+          css: `background-color: rgba( ${r}, ${g}, ${b}, 0.1)`,
+          color: localCollaborator.color
+        };
+
+        modelDB.collaborators.changed.connect(this._onCollaboratorsChanged, this);
+        //Trigger an initial onCollaboratorsChanged event.
+        this._onCollaboratorsChanged();
+      });
+    }
   }
 
   /**
@@ -124,6 +145,16 @@ class FileEditor extends CodeEditorWrapper {
     this.title.label = path.split('/').pop();
   }
 
+  private _onCollaboratorsChanged(): void {
+    //if there are selections corresponding to non-collaborators,
+    //they are stale and should be removed.
+    for (let key of this.editor.model.selections.keys()) {
+      if (!this._context.model.modelDB.collaborators.has(key)) {
+        this.editor.model.selections.delete(key);
+      }
+    }
+  }
+
   protected _context: DocumentRegistry.Context;
   private _mimeTypeService: IEditorMimeTypeService;
 }

+ 64 - 0
packages/notebook/src/widget.ts

@@ -203,6 +203,20 @@ class StaticNotebook extends Widget {
     }
     let oldValue = this._model;
     this._model = newValue;
+
+    if (oldValue && oldValue.modelDB.isCollaborative) {
+      oldValue.modelDB.connected.then(() => {
+        oldValue.modelDB.collaborators.changed.disconnect(
+          this._onCollaboratorsChanged, this);
+      });
+    }
+    if (newValue.modelDB.isCollaborative) {
+      newValue.modelDB.connected.then(() => {
+        newValue.modelDB.collaborators.changed.connect(
+          this._onCollaboratorsChanged, this);
+      });
+    }
+
     // Trigger private, protected, and public changes.
     this._onModelChanged(oldValue, newValue);
     this.onModelChanged(oldValue, newValue);
@@ -453,6 +467,23 @@ class StaticNotebook extends Widget {
     });
   }
 
+  /**
+   * Handle an update to the collaborators.
+   */
+  private _onCollaboratorsChanged(): void {
+    // If there are selections corresponding to non-collaborators,
+    // they are stale and should be removed.
+    for (let i = 0; i < this.widgets.length; i++) {
+      let cell = this.widgets[i];
+      for (let key of cell.model.selections.keys()) {
+        if (!this._model.modelDB.collaborators.has(key)) {
+          cell.model.selections.delete(key);
+        }
+      }
+    }
+  }
+
+
   private _mimetype = 'text/plain';
   private _model: INotebookModel = null;
   private _mimetypeService: IEditorMimeTypeService;
@@ -695,6 +726,7 @@ class Notebook extends StaticNotebook {
     if (newValue === oldValue) {
       return;
     }
+    this._trimSelections();
     this._stateChanged.emit({ name: 'activeCellIndex', oldValue, newValue });
   }
 
@@ -928,6 +960,24 @@ class Notebook extends StaticNotebook {
    * Handle a cell being inserted.
    */
   protected onCellInserted(index: number, cell: Cell): void {
+    if (this.model.modelDB.isCollaborative) {
+      let modelDB = this.model.modelDB;
+      modelDB.connected.then(() => {
+        if (!cell.isDisposed) {
+          //Setup the selection style for collaborators
+          let localCollaborator = modelDB.collaborators.localCollaborator;
+          cell.editor.uuid = localCollaborator.sessionId;
+          let color = localCollaborator.color;
+          let r = parseInt(color.slice(1,3), 16);
+          let g  = parseInt(color.slice(3,5), 16);
+          let b  = parseInt(color.slice(5,7), 16);
+          cell.editor.selectionStyle = {
+            css: `background-color: rgba( ${r}, ${g}, ${b}, 0.1)`,
+            color: localCollaborator.color
+          };
+        }
+      });
+    }
     cell.editor.edgeRequested.connect(this._onEdgeRequest, this);
     // If the insertion happened above, increment the active cell
     // index, otherwise it stays the same.
@@ -1384,6 +1434,20 @@ class Notebook extends StaticNotebook {
     this._selectionChanged.emit(void 0);
   }
 
+  /**
+   * Remove selections from inactive cells to avoid
+   * spurious cursors.
+   */
+  private _trimSelections(): void {
+    for (let i = 0; i < this.widgets.length; i++) {
+      if (i !== this._activeCellIndex) {
+        let cell = this.widgets[i];
+        cell.model.selections.delete(cell.editor.uuid);
+      }
+    }
+  }
+
+
   private _activeCellIndex = -1;
   private _activeCell: Cell = null;
   private _mode: NotebookMode = 'command';