Selaa lähdekoodia

[codeeditor] Added ISelection interface

akosyakov 8 vuotta sitten
vanhempi
commit
b6125956d3
1 muutettua tiedostoa jossa 292 lisäystä ja 56 poistoa
  1. 292 56
      src/codeeditor/editor.ts

+ 292 - 56
src/codeeditor/editor.ts

@@ -5,10 +5,6 @@ import {
   IDisposable
 } from 'phosphor/lib/core/disposable';
 
-import {
-  find
-} from 'phosphor/lib/algorithm/searching';
-
 import {
   Message
 } from 'phosphor/lib/core/messaging';
@@ -25,10 +21,6 @@ import {
   IChangedArgs
 } from '../common/interfaces';
 
-import {
-  ObservableVector
-} from '../common/observablevector';
-
 
 /**
  * A namespace for code editors.
@@ -99,22 +91,22 @@ namespace CodeEditor {
   export
   interface ITextSelection {
     /**
-     * The index to the first character in the current selection.
+     * The position of the first character in the current selection.
      *
      * #### Notes
      * If this position is greater than [end] then the selection is considered
      * to be backward.
      */
-    start: number;
+    start: IPosition;
 
     /**
-     * The index to the last character in the current selection.
+     * The position of the last character in the current selection.
      *
      * #### Notes
      * If this position is less than [start] then the selection is considered
      * to be backward.
      */
-    end: number;
+    end: IPosition;
 
     /**
      * The uuid of the text selection owner.
@@ -127,6 +119,206 @@ namespace CodeEditor {
     className?: string;
   }
 
+  /**
+   * An interface to manage selections by selection owners.
+   * 
+   * #### Definitions
+   * - a user code that has an associated uuid is called a selection owner, see `CodeEditor.ISelectionOwner`
+   * - a selection belongs to a selection owner only if it is associated with the owner by an uuid, see `CodeEditor.ITextSelection`
+   * 
+   * #### Read access
+   * - any user code can observe any selection
+   * 
+   * #### Write access
+   * - if a user code is a selection owner then:
+   *   - it can change selections beloging to it
+   *   - but it must not change selections beloging to other selection owners
+   * - otherwise it must not change any selection
+   */
+  export
+  interface ISelections {
+
+    /**
+     * A signal emitted when selections changes.
+     */
+    readonly changed: ISignal<Selections, ISelections.IChangedArgs>;
+
+    /**
+     * The uuids of selection owners.
+     */
+    readonly uuids: string[];
+
+    /**
+     * Returns the primary position of the cursor.
+     */
+    getCursorPosition(uuid: string): IPosition | null;
+
+    /**
+     * Set the primary position of the cursor. This will remove any secondary cursors.
+     */
+    setCursorPosition(uuid: string, position: IPosition | null): void;
+
+    /**
+     * Returns the primary selection.
+     */
+    getSelection(uuid: string): ITextSelection | null;
+
+    /**
+     * Set the primary selection. This will remove any secondary cursors.
+     */
+    setSelection(uuid: string, selection: ITextSelection | null): void;
+
+    /**
+     * Gets the selections for all the cursors in ascending order. 
+     */
+    getSelections(uuid: string): ITextSelection[];
+
+    /**
+     * Sets the selections for all the cursors.
+     * Cursors will be removed or added, as necessary.
+     */
+    setSelections(uuid: string, newSelections: ITextSelection[]): void;
+  }
+
+  /**
+   * A namespace for `ISelections`.
+   */
+  export
+  namespace ISelections {
+    /**
+     * An arguments for the selection changed signal.
+     */
+    export
+    interface IChangedArgs {
+      /**
+       * The uuid of a selection owner.
+       */
+      readonly uuid: string;
+      /**
+       * The old selections.
+       */
+      readonly oldSelections: ITextSelection[];
+      /**
+       * The new selections.
+       */
+      readonly newSelections: ITextSelection[];
+    }
+  }
+
+  /**
+   * Default implementation of `ISelections`.
+   */
+  export
+  class Selections implements ISelections {
+
+    /**
+     * A signal emitted when selections changes.
+     */
+    readonly changed: ISignal<Selections, ISelections.IChangedArgs>;
+
+    /**
+     * Uuids of all selection owners.
+     */
+    get uuids(): string[] {
+      return Object.keys(this._selections);
+    }
+
+    /**
+     * Returns the primary position of the cursor.
+     */
+    getCursorPosition(uuid: string): IPosition | null {
+      const selection = this.getSelection(uuid);
+      return this.toPosition(selection);
+    }
+
+    /**
+     * Set the primary position of the cursor. This will remove any secondary cursors.
+     */
+    setCursorPosition(uuid: string, position: IPosition | null) {
+      const selection = this.toSelection(uuid, position);
+      this.setSelection(uuid, selection);
+    }
+
+    /**
+     * Returns the primary selection.
+     */
+    getSelection(uuid: string): ITextSelection | null {
+      const selections = this.getSelections(uuid);
+      return selections.length > 0 ? selections[0] : null;
+    }
+
+    /**
+     * Set the primary selection. This will remove any secondary cursors.
+     */
+    setSelection(uuid: string, selection: ITextSelection | null) {
+      const selections: ITextSelection[] = [];
+      if (selection) {
+        selections.push(selection);
+      }
+      this.setSelections(uuid, selections);
+    }
+
+    /**
+     * Gets the selections for all the cursors in ascending order. 
+     */
+    getSelections(uuid: string): ITextSelection[] {
+      const selections = this._selections[uuid];
+      return selections ? selections : [];
+    }
+
+    /**
+     * Sets the selections for all the cursors.
+     * Cursors will be removed or added, as necessary.
+     */
+    setSelections(uuid: string, newSelections: ITextSelection[]): void {
+      const oldSelections = this.getSelections(uuid);
+      this.removeSelections(uuid);
+      this.sortSelections(newSelections);
+      this._selections[uuid] = newSelections;
+      this.changed.emit({ uuid, oldSelections, newSelections });
+    }
+
+    /**
+     * Sorts given selections in ascending order.
+     */
+    protected sortSelections(selections: ITextSelection[]) {
+      selections.sort((selection, selection2) => {
+        const result = selection.start.line - selection2.start.line;
+        if (result !== 0) {
+          return result;
+        }
+        return selection.start.column - selection2.start.column;
+      });
+    }
+
+    /**
+     * Converts the given position to a selection.
+     */
+    protected toSelection(uuid: string, position: IPosition | null): ITextSelection | null {
+      return position ? { uuid, start: position, end: position } : null;
+    }
+
+    /**
+     * Converts the given selection to a position.
+     */
+    protected toPosition(selection: ITextSelection | null): IPosition | null {
+      return selection ? selection.start : null;
+    }
+
+    /**
+     * Removes selections by the given uuid.
+     */
+    protected removeSelections(uuid: string) {
+      delete this._selections[uuid];
+    }
+
+    private _selections: {
+      [key: string]: ITextSelection[] | null
+    } = {};
+  }
+
+  defineSignal(Selections.prototype, 'changed');
+
   /**
    * An editor model.
    */
@@ -162,15 +354,8 @@ namespace CodeEditor {
 
     /**
      * The currently selected code.
-     *
-     * @returns A read-only copy of the text selections.
-     */
-    readonly selections: ObservableVector<ITextSelection>;
-
-    /**
-     * Returns the primary cursor position.
      */
-    getCursorPosition(): IPosition;
+    readonly selections: ISelections;
 
     /**
      * Returns the content for the given line number.
@@ -203,6 +388,42 @@ namespace CodeEditor {
     clearHistory(): void;
   }
 
+  /**
+   * A selection owner.
+   */
+  export
+  interface ISelectionOwner {
+    /**
+     * The uuid of this selection owner.
+     */
+    readonly uuid: string;
+    /**
+     * Set the primary position of the cursor. This will remove any secondary cursors.
+     */
+    setCursorPosition(position: IPosition): void;
+    /**
+     * Returns the primary position of the cursor.
+     */
+    getCursorPosition(): IPosition;
+    /**
+     * Set the primary selection. This will remove any secondary cursors.
+     */
+    setSelection(selection: ITextSelection): void;
+    /**
+     * Returns the primary selection.
+     */
+    getSelection(): ITextSelection;
+    /**
+     * Gets the selections for all the cursors.
+     */
+    getSelections(): ITextSelection[];
+    /**
+     * Sets the selections for all the cursors.
+     * Cursors will be removed or added, as necessary.
+     */
+    setSelections(selections: ITextSelection[]): void;
+  }
+
   /**
    * A keydown handler type.
    * 
@@ -217,7 +438,7 @@ namespace CodeEditor {
    * A widget that provides a code editor.
    */
   export
-  interface IEditor extends IDisposable {
+  interface IEditor extends ISelectionOwner, IDisposable {
     /**
      * Whether line numbers should be displayed. Defaults to false.
      */
@@ -333,6 +554,8 @@ namespace CodeEditor {
      */
     mimeTypeChanged: ISignal<this, IChangedArgs<string>>;
 
+    readonly selections = new Selections();
+
     /**
      * Whether the model is disposed.
      */
@@ -389,25 +612,6 @@ namespace CodeEditor {
       });
     }
 
-    /**
-     * Get the selections for the model.
-     */
-    get selections(): ObservableVector<ITextSelection> {
-      return this._selections;
-    }
-
-    /**
-     * Returns the primary cursor position.
-     */
-    getCursorPosition(): IPosition {
-      let selections = this.selections;
-      let cursor = find(selections, (selection) => { return selection.start === selection.end; });
-      if (cursor) {
-        return this.getPositionAt(cursor.start);
-      }
-      return null;
-    }
-
     /**
      * Get the number of lines in the model.
      */
@@ -458,7 +662,6 @@ namespace CodeEditor {
 
     private _mimetype = '';
     private _value = '';
-    private _selections = new ObservableVector<ITextSelection>();
     private _isDisposed = false;
   }
 }
@@ -471,6 +674,7 @@ defineSignal(CodeEditor.Model.prototype, 'mimeTypeChanged');
  * An implementation of an editor for an html text area.
  */
 class TextAreaEditor extends Widget implements CodeEditor.IEditor {
+  readonly uuid = '1';
   /**
    * Construct a new text editor.
    */
@@ -480,12 +684,9 @@ class TextAreaEditor extends Widget implements CodeEditor.IEditor {
     let node = this.node as HTMLTextAreaElement;
     node.readOnly = options.readOnly || false;
     node.wrap = options.wordWrap ? 'hard' : 'soft';
-    let selection = model.selections.at(0);
-    if (selection) {
-      node.setSelectionRange(selection.start, selection.end);
-    }
     model.selections.changed.connect(this.onModelSelectionsChanged, this);
     model.valueChanged.connect(this.onModelValueChanged, this);
+    this.updateSelections();
   }
 
   get lineNumbers(): boolean {
@@ -620,14 +821,50 @@ class TextAreaEditor extends Widget implements CodeEditor.IEditor {
     (this.node as HTMLTextAreaElement).value = args.newValue;
   }
 
-  protected onModelSelectionsChanged(sender: ObservableVector<CodeEditor.ITextSelection>, args: ObservableVector.IChangedArgs<CodeEditor.ITextSelection>) {
-    let node = this.node as HTMLTextAreaElement;
-    let selection = sender.at(0);
+  setCursorPosition(position: CodeEditor.IPosition): void {
+    this.setSelection({
+      uuid: this.uuid,
+      start: position,
+      end: position
+    });
+  }
+
+  getCursorPosition(): CodeEditor.IPosition {
+    return this.getSelection().start;
+  }
+
+  setSelection(selection: CodeEditor.ITextSelection | null): void {
+    const node = this.node as HTMLTextAreaElement;
     if (selection) {
-      node.setSelectionRange(selection.start, selection.end);
+      const start = this.model.getOffsetAt(selection.start);
+      const end = this.model.getOffsetAt(selection.end);
+      node.setSelectionRange(start, end);
+    } else {
+      node.setSelectionRange(0, 0);
     }
   }
 
+  getSelection(): CodeEditor.ITextSelection {
+    const node = this.node as HTMLTextAreaElement;
+    return {
+      uuid: this.uuid,
+      start: this.model.getPositionAt(node.selectionStart),
+      end: this.model.getPositionAt(node.selectionEnd)
+    };
+  }
+
+  getSelections(): CodeEditor.ITextSelection[] {
+    return [this.getSelection()];
+  }
+
+  setSelections(selections: CodeEditor.ITextSelection[]): void {
+    this.setSelection(selections.length > 0 ? selections[0] : null);
+  }
+
+  protected onModelSelectionsChanged(sender: CodeEditor.ISelections, args: CodeEditor.ISelections.IChangedArgs) {
+    // display foreign cursors
+  }
+
   private _evtKeydown(event: KeyboardEvent): void {
     let handler = this._handler;
     if (handler) {
@@ -636,14 +873,13 @@ class TextAreaEditor extends Widget implements CodeEditor.IEditor {
   }
 
   private _evtMouseUp(event: MouseEvent): void {
-    let node = this.node as HTMLTextAreaElement;
+    this.updateSelections();
+  }
+
+  protected updateSelections() {
     this._changeGuard = true;
-    this._model.selections.clear();
-    this._model.selections.pushBack({
-      start: node.selectionStart,
-      end: node.selectionEnd,
-      uuid: '1'
-    });
+    const selection = this.getSelection();
+    this._model.selections.setSelection(selection.uuid, selection);
     this._changeGuard = false;
   }