'use strict';

Object.defineProperty(exports, '__esModule', {
  value: true
});

var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }

var _lodash = require('lodash');

var _lodash2 = _interopRequireDefault(_lodash);

var _reactLibInvariant = require('react/lib/invariant');

var _reactLibInvariant2 = _interopRequireDefault(_reactLibInvariant);

var _coreAlt = require('../core/alt');

var _coreAlt2 = _interopRequireDefault(_coreAlt);

var _coreAttributes = require('../core/attributes');

var _EditorActions = require('./EditorActions');

var _EditorActions2 = _interopRequireDefault(_EditorActions);

var _coreRichText = require('../core/RichText');

var _coreUtils = require( /*, logInGroup*/'../core/utils');

var _coreTokenizer = require('../core/tokenizer');

var _coreTokenizer2 = _interopRequireDefault(_coreTokenizer);

var _coreHtmlwriter = require('../core/htmlwriter');

var _coreHtmlwriter2 = _interopRequireDefault(_coreHtmlwriter);

var _coreTextwriter = require('../core/textwriter');

var _coreTextwriter2 = _interopRequireDefault(_coreTextwriter);

var _coreTextFontMetrics = require('../core/TextFontMetrics');

var _coreTextFontMetrics2 = _interopRequireDefault(_coreTextFontMetrics);

var _coreEditorCommon = require('../core/EditorCommon');

var FLOW_DEBUG = false;
var ACTION_DELETE = 'delete';
var ACTION_INSERT = 'insert';
var ACTION_ATTRIBUTES = 'attributes';

/**
 * The EditorStore is the editor's single source of truth for application state and logic related to the editor.
 * View state updates are provided to the view components in setState callbacks. This uses the Facebook Flux
 * unidirectional flow idea: actions (from the view and elsewhere) -> dispatcher -> store -> view.
 *
 * See https://facebook.github.io/flux/docs/overview.html
 */

var EditorStore = (function () {
  function EditorStore() {
    var _this = this;

    _classCallCheck(this, EditorStore);

    this.exportPublicMethods({
      getSelection: function getSelection() {
        return _this.getSelection();
      },
      getSelectionRich: function getSelectionRich() {
        return _this.getSelectionRich();
      },
      getSelectionHtml: function getSelectionHtml() {
        return _this.getSelectionHtml();
      },
      getSelectionText: function getSelectionText() {
        return _this.getSelectionText();
      }
    });

    this.bindActions(_EditorActions2['default']);

    this.config = null;
    this.replica = null;

    this.state = {
      position: _coreRichText.BASE_CHAR,
      positionEolStart: true,
      cursorMotion: false,
      selectionActive: false,
      remoteCursors: {},
      remoteNameReveal: [],
      focus: false,
      loaded: false
    };
  }

  _createClass(EditorStore, [{
    key: 'initialize',
    value: function initialize(_ref) {
      var config = _ref.config;
      var replica = _ref.replica;

      _coreTextFontMetrics2['default'].setConfig(config);
      this.config = config;
      this.replica = replica;

      this.setState({ focus: config.initialFocus });
    }
  }, {
    key: 'initializeCursorModel',
    value: function initializeCursorModel(_ref2) {
      var cursorSet = _ref2.cursorSet;
      var cursorModel = _ref2.cursorModel;

      this.cursorSet = cursorSet;
      this.cursorModel = cursorModel;
      this._setRemoteCursorModel();
    }
  }, {
    key: 'replicaInitialized',
    value: function replicaInitialized() {
      this.setState({ loaded: true });
      this._flow();
    }
  }, {
    key: 'replicaUpdated',
    value: function replicaUpdated() {
      // TODO can we determine what changed and do a more focused flow via the modifications parameter?
      this._flow();
    }
  }, {
    key: 'focusInput',
    value: function focusInput() {
      this._delayedCursorBlink(0);
      this.setState({ focus: true });
    }
  }, {
    key: 'inputFocusLost',
    value: function inputFocusLost() {
      this.setState({ focus: false });
    }
  }, {
    key: 'setRemoteCursorPosition',
    value: function setRemoteCursorPosition(remoteCursor) {
      var id = remoteCursor.model._id;
      // some bug in Swarm.js: sometimes the id is blank if a lot of cursor events have happened quickly in succession
      if (!id) return;
      var remoteCursors = this.state.remoteCursors;
      remoteCursors[id] = remoteCursor;
      this.setState({ remoteCursors: remoteCursors });
      this._delayedRemoteCursorNameReveal(id);
    }
  }, {
    key: 'unsetRemoteCursorPosition',
    value: function unsetRemoteCursorPosition(remoteCursor) {
      var id = remoteCursor.model._id;
      // some bug in Swarm.js: sometimes the id is blank if a lot of cursor events have happened quickly in succession
      if (!id) return;
      var remoteCursors = this.state.remoteCursors;
      if (id in remoteCursors) {
        delete remoteCursors[id];
        this.setState({ remoteCursors: remoteCursors });
      }
    }
  }, {
    key: 'updateRemoteCursorModel',
    value: function updateRemoteCursorModel() {
      this._setRemoteCursorModel();
    }
  }, {
    key: 'revealRemoteCursorName',
    value: function revealRemoteCursorName(remoteCursor) {
      this._delayedRemoteCursorNameReveal(remoteCursor.model._id);
    }
  }, {
    key: 'navigateLeft',
    value: function navigateLeft() {
      this._navigateLeftRight(-1);
    }
  }, {
    key: 'navigateRight',
    value: function navigateRight() {
      this._navigateLeftRight(1);
    }
  }, {
    key: 'navigateUp',
    value: function navigateUp() {
      this._navigateUpDown(-1);
    }
  }, {
    key: 'navigateDown',
    value: function navigateDown() {
      this._navigateUpDown(1);
    }
  }, {
    key: 'navigatePageUp',
    value: function navigatePageUp() {
      // assume for now a page is 10 lines
      this._navigateUpDown(-10);
    }
  }, {
    key: 'navigatePageDown',
    value: function navigatePageDown() {
      // assume for now a page is 10 lines
      this._navigateUpDown(10);
    }
  }, {
    key: 'navigateStart',
    value: function navigateStart() {
      this._resetPosition();
    }
  }, {
    key: 'navigateStartLine',
    value: function navigateStartLine() {
      if (this._emptyEditor()) {
        this._resetPosition();
        return;
      }

      var _lineContainingChar = (0, _coreEditorCommon.lineContainingChar)(this.state.lines, this.state.position, this.state.positionEolStart);

      var line = _lineContainingChar.line;

      this._setPosition(line.start, true);
    }
  }, {
    key: 'navigateEnd',
    value: function navigateEnd() {
      if (this._emptyEditor()) {
        this._resetPosition();
        return;
      }

      var positionEolStart = false;
      if (this._lastLine().isEof()) {
        positionEolStart = true;
      }
      this._setPosition(this._relativeChar(_coreRichText.BASE_CHAR, -1), positionEolStart);
    }
  }, {
    key: 'navigateEndLine',
    value: function navigateEndLine() {
      if (this._emptyEditor()) {
        this._resetPosition();
        return;
      }

      var _lineContainingChar2 = (0, _coreEditorCommon.lineContainingChar)(this.state.lines, this.state.position, this.state.positionEolStart);

      var line = _lineContainingChar2.line;

      var position = undefined;
      var positionEolStart = false;
      if (line.isEof() || line.chunks.length === 0) {
        position = this.state.position;
        positionEolStart = true;
      } else if (line.isHard()) {
        position = this._relativeChar(line.end, -1, 'limit');
      } else {
        position = line.end;
      }
      this._setPosition(position, positionEolStart);
    }
  }, {
    key: 'navigateWordLeft',
    value: function navigateWordLeft() {
      this._navigateWordLeftRight(-1);
    }
  }, {
    key: 'navigateWordRight',
    value: function navigateWordRight() {
      this._navigateWordLeftRight(1);
    }
  }, {
    key: 'navigateToCoordinates',
    value: function navigateToCoordinates(coordinates) {
      var _coordinatesToPosition2 = this._coordinatesToPosition(coordinates);

      var position = _coordinatesToPosition2.position;
      var positionEolStart = _coordinatesToPosition2.positionEolStart;

      // set the position and selection anchor if the user continues the selection later
      position = position ? position : _coreRichText.BASE_CHAR;

      this._setPosition(position, positionEolStart);
      this.setState({
        selectionAnchorChar: position
      });
    }
  }, {
    key: 'selectionLeft',
    value: function selectionLeft() {
      this._selectionLeftRight(-1);
    }
  }, {
    key: 'selectionRight',
    value: function selectionRight() {
      this._selectionLeftRight(1);
    }
  }, {
    key: 'selectionUp',
    value: function selectionUp() {
      this._selectionUpDown(-1);
    }
  }, {
    key: 'selectionDown',
    value: function selectionDown() {
      this._selectionUpDown(1);
    }
  }, {
    key: 'selectionPageUp',
    value: function selectionPageUp() {
      // assume for now a page is 10 lines
      this._selectionUpDown(-10);
    }
  }, {
    key: 'selectionPageDown',
    value: function selectionPageDown() {
      // assume for now a page is 10 lines
      this._selectionUpDown(10);
    }
  }, {
    key: 'selectionStart',
    value: function selectionStart() {
      this._modifySelection(_coreRichText.BASE_CHAR, true);
    }
  }, {
    key: 'selectionStartLine',
    value: function selectionStartLine() {
      var _lineContainingChar3 = (0, _coreEditorCommon.lineContainingChar)(this.state.lines, this.state.position, this.state.positionEolStart);

      var line = _lineContainingChar3.line;

      this._modifySelection(line.start, true);
    }
  }, {
    key: 'selectionEnd',
    value: function selectionEnd() {
      var toChar = this._lastLine() && this._lastLine().isEof() ? _coreRichText.EOF : this._relativeChar(_coreRichText.BASE_CHAR, -1);
      this._modifySelection(toChar, toChar === _coreRichText.EOF);
    }

    /**
     * Google docs behavior (as of 2015-06-22) is:
     * - line with soft return, select line with space at end
     * - line with hard return, select line without selecting the hard return (no action on empty lines)
     * - EOF line, no action
     *
     * Word 2010 behavior is:
     * - line with soft return, select line with space at end
     * - line with hard return, select line including the hard return (same on empty lines)
     * - EOF line, show "space" selection at EOF containing a newline
     *
     * We implement the Google docs behavior here, which seems a bit more intuitive.
     */
  }, {
    key: 'selectionEndLine',
    value: function selectionEndLine() {
      var _lineContainingChar4 = (0, _coreEditorCommon.lineContainingChar)(this.state.lines, this.state.position, this.state.positionEolStart);

      var line = _lineContainingChar4.line;

      var toChar = undefined;
      var positionEolStart = false;
      if (line.isEof()) {
        toChar = this.state.position;
        positionEolStart = true;
      } else if (line.isHard()) {
        toChar = this._relativeChar(line.end, -1, 'limit');
      } else {
        toChar = line.end;
      }
      this._modifySelection(toChar, positionEolStart);
    }
  }, {
    key: 'selectionWordLeft',
    value: function selectionWordLeft() {
      var position = undefined;
      var positionEolStart = undefined;
      if (this.state.position === _coreRichText.EOF) {
        position = this._lastLine().start;
        positionEolStart = true;
      } else {
        position = this._wordStartRelativeTo(this.state.position);
        positionEolStart = !(0, _coreEditorCommon.lineContainingChar)(this.state.lines, this.state.position).endOfLine;
      }
      this._modifySelection(position, positionEolStart);
    }
  }, {
    key: 'selectionWordRight',
    value: function selectionWordRight() {
      var position = this._wordEndRelativeTo(this.state.position);
      var endOfLine = (0, _coreEditorCommon.lineContainingChar)(this.state.lines, this.state.position).endOfLine;

      this._modifySelection(position, !endOfLine);
    }
  }, {
    key: 'selectionAll',
    value: function selectionAll() {
      this._setPosition(_coreRichText.BASE_CHAR);
      if (this.state.lines.length === 0) {
        this._modifySelection(_coreRichText.EOF, true);
      } else {
        var lastChar = this._lastLine() && this._lastLine().isEof() ? _coreRichText.EOF : this._lastLine().end;
        this._modifySelection(lastChar, false);
      }
    }
  }, {
    key: 'selectToCoordinates',
    value: function selectToCoordinates(coordinates) {
      var _coordinatesToPosition3 = this._coordinatesToPosition(coordinates);

      var position = _coordinatesToPosition3.position;
      var positionEolStart = _coordinatesToPosition3.positionEolStart;

      this._modifySelection(position, positionEolStart);
    }

    /**
     * Word selection follows Google Docs and Microsoft Word 2010 behavior of:
     * - selecting the word that was clicked, and one or more spaces following it
     * - if clicking at the end of a line with a soft return, the first word on the next line is selected
     * - if clicking at the end of a line with a hard return, the hard return is selected (shown as a "space")
     * - if clicking at the end of the last (empty) line, a (non-existent) hard return at EOF is selected (shown as
     *   a "space")
     */
  }, {
    key: 'selectWordAtCurrentPosition',
    value: function selectWordAtCurrentPosition() {
      if (this.state.lines.length === 0 || (0, _coreEditorCommon.lineContainingChar)(this.state.lines, this.state.position, true).line.isEof()) {
        this._setPosition(this.state.position);
        this._modifySelection(_coreRichText.EOF, true);
      } else {
        var word = this._wordRelativeTo(this.state.position);
        this._setPosition(word.start);
        this._modifySelection(word.end, false);
      }
    }
  }, {
    key: 'copySelection',
    value: function copySelection(copyHandler) {
      var selectionChunks = this._getSelectionRich();
      if (selectionChunks && selectionChunks.length > 0) {
        var copiedText = (0, _coreTextwriter2['default'])(selectionChunks);
        var copiedHtml = (0, _coreHtmlwriter2['default'])(selectionChunks);
        copyHandler(copiedText, copiedHtml, selectionChunks);
      }
    }
  }, {
    key: 'getSelection',
    value: function getSelection() {
      return this.replica.getTextRange(this.state.selectionLeftChar, this.state.selectionRightChar);
    }
  }, {
    key: 'getSelectionRich',
    value: function getSelectionRich() {
      return this._getSelectionRich();
    }
  }, {
    key: 'getSelectionHtml',
    value: function getSelectionHtml() {
      return (0, _coreHtmlwriter2['default'])(this._getSelectionRich());
    }
  }, {
    key: 'getSelectionText',
    value: function getSelectionText() {
      return (0, _coreTextwriter2['default'])(this._getSelectionRich());
    }
  }, {
    key: 'insertChars',
    value: function insertChars(_ref3) {
      var value = _ref3.value;
      var attributes = _ref3.attributes;
      var atPosition = _ref3.atPosition;
      var reflow = _ref3.reflow;

      return this._insertChars(value, attributes, atPosition, reflow);
    }
  }, {
    key: 'insertCharsBatch',
    value: function insertCharsBatch(chunks) {
      var _this2 = this;

      var firstInsertPosition = this.state.position;
      var insertPosition = null;
      chunks.forEach(function (c) {
        insertPosition = _this2._insertChars(c.text, c.attrs || {}, insertPosition, false);
      });
      this._flow({ start: firstInsertPosition, end: insertPosition, action: 'insert' });
    }
  }, {
    key: 'eraseCharBack',
    value: function eraseCharBack() {
      if (this.state.selectionActive) {
        this._eraseSelection();
      } else if (!(0, _coreEditorCommon.charEq)(this.state.position, _coreRichText.BASE_CHAR)) {
        var position = this._relativeChar(this.state.position, -1);
        this.replica.rmChars(this.state.position);
        this._flow({ start: position, end: this.state.position, action: ACTION_DELETE });

        var endOfLine = (0, _coreEditorCommon.lineContainingChar)(this.state.lines, position).endOfLine;
        this._setPosition(position, endOfLine);
      }
    }
  }, {
    key: 'eraseCharForward',
    value: function eraseCharForward() {
      if (this.state.selectionActive) {
        this._eraseSelection();
      } else if (!this._cursorAtEnd()) {
        var next = this._relativeChar(this.state.position, 1, 'limit');
        this.replica.rmChars(next);
        this._flow({ start: this.state.position, end: next, action: ACTION_DELETE });

        var endOfLine = (0, _coreEditorCommon.lineContainingChar)(this.state.lines, this.state.position).endOfLine;
        this._setPosition(this.state.position, endOfLine);
      }
    }
  }, {
    key: 'eraseWordBack',
    value: function eraseWordBack() {
      if (this.state.selectionActive) {
        this._eraseSelection();
      } else {
        var position = this.state.position;
        var start = this._wordStartRelativeTo(position);
        var end = position;
        if ((0, _coreEditorCommon.charEq)(start, position)) {
          // beginning of word, move to the previous word
          var previousStart = this._wordStartRelativeTo(this._relativeChar(position, -1, 'limit'));
          // no previous word, nothing to delete
          if (start === previousStart) return;
          end = this._wordEndRelativeTo(start);
        }

        // TODO delete at beginning of line deletes last word on previous line or last word of previous paragraph

        var wordChars = this.replica.getTextRange(start, end);
        this.replica.rmChars(wordChars);
        this._flow({ start: start, end: end, action: ACTION_DELETE });

        var lineContainingStart = (0, _coreEditorCommon.lineContainingChar)(this.state.lines, start);
        var endOfLine = lineContainingStart ? lineContainingStart.endOfLine : true;
        this._setPosition(start, endOfLine);
      }
    }
  }, {
    key: 'eraseWordForward',
    value: function eraseWordForward() {
      if (this.state.selectionActive) {
        this._eraseSelection();
      } else {
        var position = this.state.position;
        var options = undefined;
        if ((0, _coreTokenizer.isWhitespace)(this._relativeChar(position, 1, 'limit').char)) {
          options = { includeLeadingSpace: true };
        }
        var start = position;
        var end = this._wordEndRelativeTo(start, options);
        if ((0, _coreEditorCommon.charEq)(end, position)) {
          // ending of word, move to the next word
          var nextEnd = this._wordEndRelativeTo(this._relativeChar(position, 1, 'limit'), options);
          // no next word, nothing to delete
          if (end === nextEnd) return;
          start = this._wordStartRelativeTo(end, options);
        }

        // TODO delete at end of line deletes first word on next line or first word of next paragraph

        var wordChars = this.replica.getTextRange(start, end);
        this.replica.rmChars(wordChars);
        this._flow({ start: start, end: end, action: ACTION_DELETE });

        var lineContainingStart = (0, _coreEditorCommon.lineContainingChar)(this.state.lines, start);
        var endOfLine = lineContainingStart ? lineContainingStart.endOfLine : true;
        this._setPosition(start, endOfLine);
      }
    }
  }, {
    key: 'eraseSelection',
    value: function eraseSelection() {
      if (this.state.selectionActive) {
        this._eraseSelection();
      }
    }
  }, {
    key: 'toggleBold',
    value: function toggleBold() {
      this._toggleAttribute(_coreAttributes.ATTR.BOLD);
    }
  }, {
    key: 'toggleItalics',
    value: function toggleItalics() {
      this._toggleAttribute(_coreAttributes.ATTR.ITALIC);
    }
  }, {
    key: 'toggleUnderline',
    value: function toggleUnderline() {
      this._toggleAttribute(_coreAttributes.ATTR.UNDERLINE);
    }
  }, {
    key: 'toggleStrikethrough',
    value: function toggleStrikethrough() {
      this._toggleAttribute(_coreAttributes.ATTR.STRIKETHROUGH);
    }
  }, {
    key: 'toggleSuperscript',
    value: function toggleSuperscript() {
      this._toggleAttribute(_coreAttributes.ATTR.SUPERSCRIPT, _coreAttributes.ATTR.SUBSCRIPT);
    }
  }, {
    key: 'toggleSubscript',
    value: function toggleSubscript() {
      this._toggleAttribute(_coreAttributes.ATTR.SUBSCRIPT, _coreAttributes.ATTR.SUPERSCRIPT);
    }
  }, {
    key: '_insertChars',
    value: function _insertChars(value, attributes, atPosition, reflow) {
      if (_lodash2['default'].isUndefined(reflow)) reflow = true;
      var position = undefined;

      if (this.state.selectionActive) {
        position = this.state.selectionLeftChar;
        this._eraseSelection();
      } else {
        if (atPosition) {
          position = atPosition;
        } else {
          position = this.state.position;
        }

        if (position === _coreRichText.EOF) {
          position = this._relativeChar(_coreRichText.EOF, -1, 'limit');
        }
      }

      if (!attributes) {
        if (this.state.selectionActive) {
          position = this.state.selectionLeftChar;
          // if selection, then activeAttributes (set by command or toolbar) are set by the first selected char
          attributes = this._relativeChar(position, 1, 'limit').attributes;
        } else {
          attributes = this.state.activeAttributes ? this.state.activeAttributes : this._relativeChar(position, 0).attributes; // reload attributes from the replica in case they have changed
        }
      }

      this.replica.insertCharsAt(position, value, attributes);

      var relativeMove = value.length;
      var newPosition = this._relativeChar(position, relativeMove);
      // if the last char is a newline, then we want to position on the start of the next line
      var newPositionEolStart = value.slice(-1) === '\n';

      if (reflow) this._flow({ start: position, end: newPosition, action: ACTION_INSERT });

      this._setPosition(newPosition, newPositionEolStart);
      this.activeAttributes = attributes;

      // return the new position so that multiple insertChars calls can be made in sequence
      return newPosition;
    }
  }, {
    key: '_relativeChar',
    value: function _relativeChar(charOrId, relative, wrap) {
      return this.replica.getCharRelativeTo(charOrId, relative, wrap);
    }

    /**
     * Flows the content i.e. wraps the text into multiple lines, splits each line into chunks with the same
     * attributes, and sets the component state based on the result. This state is then used for rendering the
     * editor surface during the next render cycle. This should be called after any operation that may change
     * the content flow, such as inserting or deleting text.
     *
     * One or more spaces at the end of a word are not "counted" for wrapping purposes because that would cause the
     * space to show up at the beginning of the next line. Instead, these spaces are included on the prior line
     * even though strictly it causes the line length to exceed the margin. This behavior is consistent with
     * common word processors such as Microsoft Word and Google Docs.
     *
     * Even though all of the state here can be calculated from the replica, this is not done at render time
     * because the line state must be available when user input such as clicks or selections are made.
     *
     * Because the algorithm to shortcut the flow before and after the changed area is complex, and can fail
     * without causing an obvious problem other than performance, debug statements are included and can be
     * enabled by setting FLOW_DEBUG to true.
     */
  }, {
    key: '_flow',
    value: function _flow(modification) {
      var _this3 = this;

      var lines = [];

      var modStartAtLine = undefined;
      var startChar = undefined;
      if (modification) {
        var startLineIndex = 0;
        modStartAtLine = (0, _coreEditorCommon.lineContainingChar)(this.state.lines, modification.start, true);

        // shortcut beginning of flow, start at the line prior to the one containing the modification position
        if (modStartAtLine && modStartAtLine.index > 0) {
          startLineIndex = modStartAtLine.index - 1;
          // if the previous line is a hard return, we can ignore it --> no changes on this line could affect it
          if (this.state.lines[startLineIndex].isHard()) {
            startLineIndex = modStartAtLine.index;
          }
          (0, _coreUtils.pushArray)(lines, this.state.lines.slice(0, startLineIndex));
          startChar = this.state.lines[startLineIndex].start;
        } else {
          startChar = _coreRichText.BASE_CHAR;
        }
      } else {
        startChar = _coreRichText.BASE_CHAR;
      }

      if (FLOW_DEBUG) {
        var modificationSummary = undefined;
        if (modification) {
          modificationSummary = modification.action + ' ' + modification.start.toString() + ' … ' + modification.end.toString();
        } else {
          modificationSummary = 'n/a';
        }
        console.group('FLOW: modification=' + modificationSummary + ' startChar=' + startChar.toString());
      }

      var newLine = false;

      var CurrentWord = (function () {
        function CurrentWord() {
          _classCallCheck(this, CurrentWord);

          this.reset();
        }

        _createClass(CurrentWord, [{
          key: 'reset',
          value: function reset() {
            this.chars = [];
            this.length = 0;
            this.pendingChunks = [];
            this.advance = 0;
            this.lineAdvance = 0;
            this.attributes = null;
            this.lastCharSpace = false;
          }
        }, {
          key: 'pushChar',
          value: function pushChar(c, fontSize) {
            if (!this.attributes) {
              this.attributes = c.attributes;
            }
            var charAdvance = _coreTextFontMetrics2['default'].advanceXForChars(fontSize, c);

            // don't count spaces in the word in the advance, but include it in the line advance
            if (c.char === ' ') {
              this.lastCharSpace = true;
            } else {
              this.advance += charAdvance;
            }
            this.lineAdvance += charAdvance;
            this.chars.push(c);
            this.length++;
          }
        }, {
          key: 'popChar',
          value: function popChar() {
            this.length--;
            return this.chars.pop();
          }
        }, {
          key: 'pushChunks',
          value: function pushChunks() {
            if (this.length > 0) {
              this.pendingChunks.push({
                length: this.length,
                attributes: this.attributes
              });
            }
            this.attributes = null;
            this.length = 0;
          }
        }]);

        return CurrentWord;
      })();

      var CurrentLine = (function () {
        function CurrentLine() {
          _classCallCheck(this, CurrentLine);

          this.reset();
        }

        _createClass(CurrentLine, [{
          key: 'reset',
          value: function reset() {
            this.chars = [];
            this.chunks = [];
            this.advance = 0;
            this.start = null;
            this.end = null;
          }
        }, {
          key: 'pushWord',
          value: function pushWord(word) {
            (0, _coreUtils.pushArray)(this.chars, word.chars);

            var lastChunk = undefined;
            if (this.chunks.length > 0) {
              lastChunk = this.chunks[this.chunks.length - 1];
              // if the last chunk in the line matches attributes with the first word chunk, join them to minimize chunks
              if ((0, _coreAttributes.attributesEqual)(lastChunk.attributes, word.pendingChunks[0].attributes)) {
                lastChunk.end += word.pendingChunks[0].length;
                word.pendingChunks.shift();
              }
            }

            // set the start and end of each chunk, start and end are suitable for feeding to chars.slice i.e. end is exclusive
            var start = lastChunk ? lastChunk.end : 0;
            word.pendingChunks.map(function (c) {
              c.start = start;
              c.end = start + c.length;
              start = c.end;
              delete c.length;
              return c;
            });

            (0, _coreUtils.pushArray)(this.chunks, word.pendingChunks);
            this.end = this.chars[this.chars.length - 1];
            this.advance += word.lineAdvance;
            word.reset();
          }
        }, {
          key: 'pushNewline',
          value: function pushNewline(c) {
            (0, _reactLibInvariant2['default'])(c.char === '\n', 'pushNewline can only be called with a newline char.');
            this.chars.push(c);
            this.end = c;
          }
        }, {
          key: 'pushEof',
          value: function pushEof() {
            this.end = _coreRichText.EOF;
          }
        }]);

        return CurrentLine;
      })();

      var currentWord = new CurrentWord();
      var currentLine = new CurrentLine();

      var pushLine = function pushLine(line) {
        if (line.end) {
          var l = new _coreEditorCommon.Line(line.chars, line.chunks, line.start, line.end, line.advance);
          lines.push(l);
          if (FLOW_DEBUG) {
            console.debug('PUSHED LINE (index=' + (lines.length - 1) + '): ' + l.toString());
          }
          newLine = true;
        }
        line.reset();
        line.start = lines.length > 0 ? lines[lines.length - 1].end : _coreRichText.BASE_CHAR;
      };

      var modLineOffset = undefined;

      var processChar = function processChar(_x) {
        var _again = true;

        _function: while (_again) {
          var c = _x;
          modEndAtLine = modEndAtLine = startIndex = matchChars = iterations = matched = i = existingLines = lastChar = undefined;
          _again = false;

          if (newLine && modification && _this3.state.lines) {
            // Shortcut the flow algorithm if we know and are past the line after the modifications, and
            // any existing lines past that point match up with our current buffer (word + char). Once modLineOffset
            // is defined, we'll keep doing the search for each pushed line
            if (_lodash2['default'].isUndefined(modLineOffset)) {
              if (modification.action === ACTION_INSERT) {
                if (lines[lines.length - 1].hasChar(modification.end)) {
                  // the number of lines inserted
                  modLineOffset = lines.length - modStartAtLine.index;
                  if (modification.end.char === '\n') modLineOffset++;
                }
              } else if (modification.action === ACTION_DELETE) {
                if (lines.length > modStartAtLine.index) {
                  var modEndAtLine = (0, _coreEditorCommon.lineContainingChar)(_this3.state.lines, modification.end);
                  modLineOffset = modStartAtLine.index - modEndAtLine.index;
                }
              } else if (modification.action === ACTION_ATTRIBUTES) {
                if (lines[lines.length - 1].hasChar(modification.end)) {
                  // the number of lines less or more based on the attributes change
                  var modEndAtLine = (0, _coreEditorCommon.lineContainingChar)(_this3.state.lines, modification.end);
                  modLineOffset = lines.length - modEndAtLine.index;
                }
              }
            }
            if (!_lodash2['default'].isUndefined(modLineOffset)) {
              var startIndex = lines.length - modLineOffset;
              var matchChars = _lodash2['default'].clone(currentWord.chars);
              matchChars.push(c);

              if (FLOW_DEBUG) {
                console.group('MATCH ATTEMPT: chars=[' + matchChars.map(function (ch) {
                  return ch.char === '\n' ? '↵' : ch.char;
                }).join('') + '] modLineOffset=' + modLineOffset);
              }
              var iterations = 0;
              var matched = false;
              for (var i = startIndex; i < _this3.state.lines.length && !matched; i++) {
                iterations++;
                if (FLOW_DEBUG) {
                  console.debug(_this3.state.lines[i].toString());
                }
                if ((0, _coreEditorCommon.charArrayEq)(matchChars, _this3.state.lines[i].chars.slice(0, matchChars.length))) {
                  // line matches, so just grab the rest of the lines from the current state, setting start to match end
                  var existingLines = _this3.state.lines.slice(i);
                  existingLines[0].start = lines[lines.length - 1].end;
                  (0, _coreUtils.pushArray)(lines, existingLines);
                  matched = true;
                }
                // No point in checking more if past a hard return
                if (i > startIndex && _this3.state.lines[i - 1].isHard()) break;
              }
              if (FLOW_DEBUG) {
                console.debug('Match ' + (matched ? 'succeeded' : 'failed') + ' in ' + iterations + ' iterations.');
                console.groupEnd();
              }
              if (matched) {
                return false;
              }
            }
            newLine = false;
          }

          if (!(0, _coreAttributes.attributesEqual)(currentWord.attributes, c.attributes)) {
            currentWord.pushChunks();
          }

          if (c.char !== '\n' && c.char !== ' ' && currentWord.lastCharSpace) {
            // new word
            currentWord.pushChunks();
            currentLine.pushWord(currentWord);
            currentWord.pushChar(c, _this3.config.fontSize);
          } else if (c.char === '\n') {
            // new line
            currentWord.pushChunks();
            currentLine.pushWord(currentWord);
            currentLine.pushNewline(c);
            pushLine(currentLine);
          } else {
            currentWord.pushChar(c, _this3.config.fontSize);
          }

          // check for line wrap
          if (currentLine.advance === 0 && currentWord.advance > _this3.config.width) {
            // word longer than a line, here we need to remove the last char to get us back under the line width
            var lastChar = currentWord.popChar();
            currentWord.pushChunks();
            currentLine.pushWord(currentWord);
            pushLine(currentLine);
            _x = lastChar;
            _again = true;
            continue _function;
          } else if (currentLine.advance + currentWord.advance > _this3.config.width) {
            pushLine(currentLine);
          }
          return true;
        }
      };

      currentLine.start = startChar;
      var contentIterator = this.replica.getTextRange(startChar)[Symbol.iterator]();
      var e = undefined;
      var completed = false;
      while (!(e = contentIterator.next()).done && !completed) {
        if (!processChar(e.value)) {
          completed = true;
        }
      }

      if (!completed) {
        currentWord.pushChunks();
        currentLine.pushWord(currentWord);
        pushLine(currentLine);

        // add an empty last line if the last line ended with a newline
        if (lines.length > 0 && lines[lines.length - 1].isHard()) {
          currentLine.pushEof();
          pushLine(currentLine);
        }
      }

      this.setState({ lines: lines });

      if (FLOW_DEBUG) {
        console.groupEnd();
      }
    }
  }, {
    key: '_setRemoteCursorModel',
    value: function _setRemoteCursorModel() {
      var _this4 = this;

      if (!this.cursorModel) return;

      var updatedModel = {
        position: this.state.position.id,
        positionEolStart: this.state.positionEolStart,
        selectionActive: this.state.selectionActive,
        selectionLeftChar: this.state.selectionActive ? (0, _coreEditorCommon.charId)(this.state.selectionLeftChar) : null,
        selectionRightChar: this.state.selectionActive ? (0, _coreEditorCommon.charId)(this.state.selectionRightChar) : null,
        ms: Date.now()
      };

      // do the remote work at the end of the event queue to avoid UI latency
      setTimeout(function () {
        // add the cursor model back into the set if it is not there (reaped due to idleness?)
        if (_this4.cursorSet.list().findIndex(function (e) {
          return e._id === _this4.cursorModel._id;
        }) < 0) {
          _this4.cursorSet.addObject(_this4.cursorModel);
        }
        _this4.cursorModel.set(updatedModel);
      }, 0);
    }

    /**
     * Sets the character and cursor position within the text. The position is relative to an existing
     * character given by its replica id. The cursor position is calculated based on the character
     * position. This is generally straightforward except when the character position is the last
     * character of a line. In this situation, there are two possible cursor positions: at the end of
     * the line, or at the beginning of the next line. The desired cursor location depends on how one
     * got there e.g. hitting "end" on a line should keep you on the same line, hitting "home" on the
     * next line takes you to the same character position, except at the beginning of that line.
     *
     * @param {object} position The character position to set.
     * @param {boolean} [positionEolStart = true] positionEolStart When rendering the cursor, this state
     *   determines the cursor position when the character position is at a line end: whether to place
     *   the cursor at the start of the next line (positionEolStart = true), or at the end of the
     *   current one (positionEolStart = false). If the cursor position is not at a line end, this state
     *   is ignored by the renderer. Since this state is often "left over" from previous calls to _setPosition
     *   it should not be trusted other than for rendering.
     * @param {boolean} [resetUpDown = true] resetUpDown Whether to reset the up/down advance
     *   and position values.
     */
  }, {
    key: '_setPosition',
    value: function _setPosition(position, positionEolStart, resetUpDown) {
      if (_lodash2['default'].isUndefined(positionEolStart)) positionEolStart = this.state.positionEolStart;
      if (_lodash2['default'].isUndefined(resetUpDown)) resetUpDown = true;

      //console.debug('position', position, 'positionEolStart', positionEolStart, 'resetUpDown', resetUpDown)

      this.setState({
        position: position,
        positionEolStart: positionEolStart,
        selectionActive: false,
        activeAttributes: null
      });

      // todo set the line information in the state, line index, advanceX

      if (resetUpDown) {
        this.upDownAdvanceX = null;
        this.upDownPositionEolStart = null;
      }

      this._delayedCursorBlink();
      this._setRemoteCursorModel();
    }
  }, {
    key: '_resetPosition',
    value: function _resetPosition() {
      this._setPosition(_coreRichText.BASE_CHAR);
    }
  }, {
    key: '_delayedCursorBlink',
    value: function _delayedCursorBlink(timeout) {
      var _this5 = this;

      if (_lodash2['default'].isUndefined(timeout)) timeout = 1000;

      this.setState({ cursorMotion: true });

      // after timeout, reset the cursor blink, clear any previous resets to avoid unnecessary state changes
      if (this.cursorMotionTimeout) {
        clearTimeout(this.cursorMotionTimeout);
      }
      this.cursorMotionTimeout = setTimeout(function () {
        _this5.setState({ cursorMotion: false });
        _this5.cursorMotionTimeout = null;
      }, timeout);
    }
  }, {
    key: '_delayedRemoteCursorNameReveal',
    value: function _delayedRemoteCursorNameReveal(id, timeout) {
      var _this6 = this;

      if (_lodash2['default'].isUndefined(timeout)) timeout = 2000;

      this.state.remoteNameReveal.push(id);
      this.setState({ remoteNameReveal: this.state.remoteNameReveal });

      setTimeout(function () {
        // this should never be false
        if (_this6.state.remoteNameReveal.length > 0) _this6.state.remoteNameReveal.shift();
        _this6.setState({ remoteNameReveal: _this6.state.remoteNameReveal });
      }, timeout);
    }
  }, {
    key: '_lastLine',
    value: function _lastLine() {
      return this.state.lines[this.state.lines.length - 1];
    }
  }, {
    key: '_emptyEditor',
    value: function _emptyEditor() {
      return this.state.lines.length === 0 && (this.state.position === _coreRichText.EOF || (0, _coreEditorCommon.charEq)(this.state.position, _coreRichText.BASE_CHAR));
    }
  }, {
    key: '_cursorAtEnd',
    value: function _cursorAtEnd() {
      return this._emptyEditor() || (0, _coreEditorCommon.charEq)(this.state.position, this._lastLine().end) || this._lastLine().isEof() && (0, _coreEditorCommon.charEq)(this.state.position, this._lastLine().start);
    }
  }, {
    key: '_getSelectionRich',
    value: function _getSelectionRich() {
      var _this7 = this;

      var selectionChunks = [];

      if (!this.state.selectionActive) {
        return selectionChunks;
      }

      var currentChunk = {
        chars: [],
        attributes: null,

        reset: function reset() {
          this.chars = [];
          this.attributes = null;
        },

        pushChar: function pushChar(c) {
          if (!this.attributes) {
            this.attributes = c.attributes;
          }
          // push newlines as separate chunks for ease of parsing paragraphs and breaks from chunks
          if (c.char === '\n') {
            // previous chunk
            this.pushChunk();
          }
          this.chars.push(c.char);
          if (c.char === '\n') {
            // newline chunk
            this.pushChunk();
          }
        },

        pushChunk: function pushChunk() {
          if (this.chars.length > 0) {
            selectionChunks.push({
              text: this.chars.join(''),
              attrs: this.attributes
            });
          }
          this.reset();
        }
      };

      var processChar = function processChar(c) {
        if (!(0, _coreAttributes.attributesEqual)(currentChunk.attributes, c.attributes)) {
          currentChunk.pushChunk();
        }
        currentChunk.pushChar(c, _this7);
      };

      var selectionChars = this.replica.getTextRange(this.state.selectionLeftChar, this.state.selectionRightChar);
      var contentIterator = selectionChars[Symbol.iterator]();
      var e = undefined;
      while (!(e = contentIterator.next()).done) {
        processChar(e.value);
      }
      // last chunk
      currentChunk.pushChunk();

      return selectionChunks;
    }
  }, {
    key: '_modifySelection',
    value: function _modifySelection(toChar, positionEolStart, resetUpDown) {
      var _this8 = this;

      if (!toChar) return;
      if (_lodash2['default'].isUndefined(resetUpDown)) resetUpDown = true;

      if (this.cursorMotionTimeout) {
        clearTimeout(this.cursorMotionTimeout);
      }

      this.setState(function (previousState) {
        if (previousState.selectionActive) {
          if ((0, _coreEditorCommon.charEq)(previousState.selectionAnchorChar, previousState.selectionLeftChar)) {
            var compareAnchorPos = _this8.replica.compareCharPos(toChar, previousState.selectionAnchorChar);
            if (compareAnchorPos < 0) {
              return {
                selectionRightChar: previousState.selectionAnchorChar,
                selectionLeftChar: toChar,
                position: toChar,
                positionEolStart: positionEolStart
              };
            } else if (compareAnchorPos > 0) {
              return {
                selectionRightChar: toChar,
                position: toChar,
                positionEolStart: positionEolStart
              };
            } else {
              _this8._setPosition(previousState.selectionAnchorChar, positionEolStart);
            }
          } else {
            var compareAnchorPos = _this8.replica.compareCharPos(previousState.selectionAnchorChar, toChar);
            if (compareAnchorPos < 0) {
              return {
                selectionRightChar: toChar,
                selectionLeftChar: previousState.selectionAnchorChar,
                position: toChar,
                positionEolStart: positionEolStart
              };
            } else if (compareAnchorPos > 0) {
              return {
                selectionLeftChar: toChar,
                position: toChar,
                positionEolStart: positionEolStart
              };
            } else {
              _this8._setPosition(previousState.selectionAnchorChar, positionEolStart);
            }
          }
        } else {
          var comparePos = _this8.replica.compareCharPos(previousState.position, toChar);
          if (comparePos === 0) return null;
          return {
            selectionActive: true,
            selectionAnchorChar: previousState.position,
            selectionLeftChar: comparePos < 0 ? previousState.position : toChar,
            selectionRightChar: comparePos > 0 ? previousState.position : toChar,
            position: toChar,
            positionEolStart: positionEolStart
          };
        }
        // TODO toolbar state based on common rich text attributes of selection
      });
      if (resetUpDown) {
        this.upDownAdvanceX = null;
        this.upDownPositionEolStart = null;
      }
      this._setRemoteCursorModel();
    }
  }, {
    key: '_charPositionRelativeToIndex',
    value: function _charPositionRelativeToIndex(charIndex, textChars) {
      if (charIndex === 0) {
        return this._relativeChar(textChars[0], -1);
      } else {
        return textChars[charIndex - 1];
      }
    }
  }, {
    key: '_wordRelativeTo',
    value: function _wordRelativeTo(char, options) {
      var textChars = this.replica.getTextRange(_coreRichText.BASE_CHAR);
      var charIndex = textChars.findIndex(function (e) {
        return (0, _coreEditorCommon.charEq)(e, char);
      }) + 1;
      var tokenRanges = (0, _coreTokenizer2['default'])(textChars.map(function (c) {
        return c.char;
      }), options);
      for (var i = 0; i < tokenRanges.length; i++) {
        if (charIndex >= tokenRanges[i].start && charIndex < tokenRanges[i].end) {
          return {
            start: tokenRanges[i].start === 0 ? _coreRichText.BASE_CHAR : textChars[tokenRanges[i].start - 1],
            end: textChars[tokenRanges[i].end - 1]
          };
        }
      }
      // if charIndex == last token range end
      var last = tokenRanges.length - 1;
      return {
        start: tokenRanges[last].start === 0 ? _coreRichText.BASE_CHAR : textChars[tokenRanges[last].start - 1],
        end: textChars[tokenRanges[last].end - 1]
      };
    }
  }, {
    key: '_wordStartRelativeTo',
    value: function _wordStartRelativeTo(char, options) {
      var textChars = this.replica.getTextRange(_coreRichText.BASE_CHAR, char);
      var tokenRanges = (0, _coreTokenizer2['default'])(textChars.map(function (c) {
        return c.char;
      }), options);
      if (tokenRanges.length > 0) {
        var start = tokenRanges[tokenRanges.length - 1].start;
        return start < 1 ? _coreRichText.BASE_CHAR : textChars[start - 1];
      } else {
        return _coreRichText.BASE_CHAR;
      }
    }
  }, {
    key: '_wordEndRelativeTo',
    value: function _wordEndRelativeTo(char, options) {
      var textChars = this.replica.getTextRange(char);
      var tokenRanges = (0, _coreTokenizer2['default'])(textChars.map(function (c) {
        return c.char;
      }), options);
      if (tokenRanges.length > 0) {
        var end = tokenRanges[0].end;
        return textChars[end - 1];
      } else {
        return this._cursorAtEnd() ? _coreRichText.EOF : this._relativeChar(_coreRichText.BASE_CHAR, -1);
      }
    }
  }, {
    key: '_eraseSelection',
    value: function _eraseSelection() {
      (0, _reactLibInvariant2['default'])(this.state.selectionActive, 'Selection must be active to erase it.');
      var position = this.state.selectionLeftChar;

      var selectionChars = this.replica.getTextRange(this.state.selectionLeftChar, this.state.selectionRightChar);
      this.replica.rmChars(selectionChars);
      this._flow({ start: this.state.selectionLeftChar, end: this.state.selectionRightChar, action: ACTION_DELETE });

      var endOfLine = (0, _coreEditorCommon.lineContainingChar)(this.state.lines, position).endOfLine;
      this._setPosition(position, endOfLine);
    }
  }, {
    key: '_navigateLeftRight',
    value: function _navigateLeftRight(charCount) {
      var position = undefined;
      if (this._emptyEditor()) {
        this._resetPosition();
        return;
      }

      if (this.state.selectionActive && charCount < 0) {
        // left from left char
        position = this.state.selectionLeftChar;
      } else if (this.state.selectionActive) {
        // right from right char
        position = this.state.selectionRightChar;
      } else {
        position = this._relativeChar(this.state.position, charCount, 'limit');
      }
      var endOfLine = (0, _coreEditorCommon.lineContainingChar)(this.state.lines, position).endOfLine;
      this._setPosition(position, endOfLine);
    }
  }, {
    key: '_navigateUpDown',
    value: function _navigateUpDown(lineCount) {
      if (this._emptyEditor()) {
        this._resetPosition();
        return;
      }

      if (this.state.selectionActive) {
        // collapse the selection and position the cursor relative to the left (if up) or right (if down)
        var position = undefined;
        var _positionEolStart = undefined;
        if (lineCount < 0) {
          position = this.state.selectionLeftChar;
          _positionEolStart = true;
        } else if (this.state.selectionActive) {
          position = this.state.selectionRightChar;
          _positionEolStart = false;
        }
        this._setPosition(position, _positionEolStart);
      }

      var upDownAdvanceX = this.upDownAdvanceX;
      var positionEolStart = this.upDownPositionEolStart;
      var currentLineAndAdvance = this._lineAndAdvanceAtPosition(this.state.position, this.state.positionEolStart);
      var index = currentLineAndAdvance.index;

      if (this.upDownAdvanceX == null || this.upDownPositionEolStart == null) {
        upDownAdvanceX = currentLineAndAdvance.advanceX;
        positionEolStart = this.state.positionEolStart;

        // save the advance and positionEolStart in case the user navigates up or down again
        this.upDownAdvanceX = upDownAdvanceX;
        this.upDownPositionEolStart = positionEolStart;
      }

      var targetIndex = index + lineCount;
      var targetLine = undefined;
      if (targetIndex < 0 || targetIndex > this.state.lines.length - 1) {
        if (targetIndex < 0 && index !== 0) {
          targetLine = this.state.lines[0];
        } else if (targetIndex > this.state.lines.length - 1 && index !== this.state.lines.length - 1) {
          targetLine = this.state.lines[this.state.lines.length - 1];
        } else {
          // nowhere to go, just unblink for a second to indicate to the user input was received
          this._delayedCursorBlink();
        }
      } else {
        targetLine = this.state.lines[targetIndex];
      }

      if (targetLine) {
        var newPosition = undefined;
        if (targetLine.isEof()) {
          newPosition = targetLine.start;
          positionEolStart = true;
        } else {
          var chars = targetLine.chars;
          var indexAndCursor = _coreTextFontMetrics2['default'].indexAndCursorForXValue(this.config.fontSize, upDownAdvanceX, chars);
          newPosition = this._charPositionRelativeToIndex(indexAndCursor.index, chars);

          // if the new position is the start of the line, position the cursor at the start of the line
          positionEolStart = (0, _coreEditorCommon.charEq)(newPosition, targetLine.start);
        }
        this._setPosition(newPosition, positionEolStart, false);
      }
    }
  }, {
    key: '_navigateWordLeftRight',
    value: function _navigateWordLeftRight(wordCount) {
      var position = undefined;
      if (this._emptyEditor()) {
        this._resetPosition();
        return;
      }

      if (this.state.selectionActive && wordCount < 0) {
        // start from one character into the selection left char so that relative to the left selected word
        position = this._relativeChar(this.state.selectionLeftChar, 1, 'limit');
      } else if (this.state.selectionActive) {
        // start from one character before the selection right char so that relative to the right selected word
        position = this._relativeChar(this.state.selectionRightChar, -1, 'limit');
      } else {
        position = this.state.position;
      }
      var relativeTo = _lodash2['default'].bind(wordCount < 0 ? this._wordStartRelativeTo : this._wordEndRelativeTo, this);
      position = relativeTo(position);
      var endOfLine = (0, _coreEditorCommon.lineContainingChar)(this.state.lines, position).endOfLine;
      this._setPosition(position, endOfLine);
    }
  }, {
    key: '_selectionLeftRight',
    value: function _selectionLeftRight(charCount) {
      var endOfLine = (0, _coreEditorCommon.lineContainingChar)(this.state.lines, this.state.position).endOfLine;
      var toChar = this._relativeChar(this.state.position, charCount, 'eof');
      if (toChar === _coreRichText.EOF && this._lastLine() && !this._lastLine().isEof()) {
        toChar = this._lastLine().end;
      }
      this._modifySelection(toChar, this.state.position === _coreRichText.EOF && charCount === -1 || !endOfLine);
    }
  }, {
    key: '_selectionUpDown',
    value: function _selectionUpDown(lineCount) {
      var upDownAdvanceX = this.upDownAdvanceX;
      var positionEolStart = this.upDownPositionEolStart;
      var currentLineAndAdvance = this._lineAndAdvanceAtPosition(this.state.position, this.state.positionEolStart);
      var line = currentLineAndAdvance.line;
      var index = currentLineAndAdvance.index;

      if (this.upDownAdvanceX == null || this.upDownPositionEolStart == null) {
        upDownAdvanceX = currentLineAndAdvance.advanceX;
        positionEolStart = this.state.positionEolStart;

        // save the advance and positionEolStart in case the user navigates up or down again
        this.upDownAdvanceX = upDownAdvanceX;
        this.upDownPositionEolStart = positionEolStart;
      }

      var targetIndex = index + lineCount;
      if (targetIndex < 0) {
        this._modifySelection(_coreRichText.BASE_CHAR, true);
        // at start of first line, reset the advanceX, and positionEolStart is now true
        this.upDownAdvanceX = 0;
        this.upDownPositionEolStart = true;
      } else if (targetIndex > this.state.lines.length - 1 && !this._lastLine().isEof()) {
        // trying to navigate past the last line (and last line does not have an EOF), position at end of line
        var toChar = this._relativeChar(_coreRichText.BASE_CHAR, -1);
        this._modifySelection(toChar, false);
        // at end of last line, reset the advanceX to the end of the line, and positionEolStart is now false
        var chars = line.chars;
        this.upDownAdvanceX = _coreTextFontMetrics2['default'].advanceXForChars(this.config.fontSize, chars);
        this.upDownPositionEolStart = false;
      } else if (targetIndex >= this.state.lines.length - 1 && this._lastLine().isEof()) {
        this._modifySelection(_coreRichText.EOF, true, false);
      } else if (this._emptyEditor()) {
        this._resetPosition();
      } else {
        var targetLine = this.state.lines[targetIndex];
        var newPosition = undefined;
        if (targetLine.isEof()) {
          newPosition = targetLine.start;
          positionEolStart = true;
        } else {
          var chars = targetLine.chars;
          var indexAndCursor = _coreTextFontMetrics2['default'].indexAndCursorForXValue(this.config.fontSize, upDownAdvanceX, chars);
          newPosition = this._charPositionRelativeToIndex(indexAndCursor.index, chars);

          // if the new position is the start of the line, position the cursor at the start of the line
          positionEolStart = (0, _coreEditorCommon.charEq)(newPosition, targetLine.start);
        }
        this._modifySelection(newPosition, positionEolStart, false);
      }
    }
  }, {
    key: '_toggleAttribute',
    value: function _toggleAttribute(attribute, exclusiveWith) {
      if (this.state.selectionActive) {
        var selectionChars = this.replica.getTextRange(this.state.selectionLeftChar, this.state.selectionRightChar);
        var charsWithAttrNotSet = selectionChars.filter(function (c) {
          return !c.attributes || !c.attributes[attribute];
        });

        var setAttr = {};

        if (charsWithAttrNotSet && charsWithAttrNotSet.length > 0) {
          var attr = {};
          attr[attribute] = true;

          for (var i = 0; i < charsWithAttrNotSet.length; i++) {
            var currentAttrs = charsWithAttrNotSet[i].copyOfAttributes();
            if (exclusiveWith && currentAttrs && currentAttrs[exclusiveWith]) delete currentAttrs[exclusiveWith];
            setAttr[charsWithAttrNotSet[i].id] = currentAttrs ? _lodash2['default'].merge(currentAttrs, attr) : attr;
          }
        } else {
          for (var i = 0; i < selectionChars.length; i++) {
            var currentAttrs = selectionChars[i].copyOfAttributes();
            delete currentAttrs[attribute];
            setAttr[selectionChars[i].id] = currentAttrs;
          }
        }

        this.replica.setAttributes(setAttr);
        this._flow({ start: this.state.selectionLeftChar, end: this.state.selectionRightChar, action: ACTION_ATTRIBUTES });
      } else {
        // TODO set the state of the toolbar so the toolbar button can be rendered accordingly
        // no selection so we are either toggling the explicitly set state, or setting the state explicitly
        var activeAttributes = this.state.activeAttributes;
        if (activeAttributes) {
          activeAttributes[attribute] = !activeAttributes[attribute];
          if (activeAttributes[attribute] && exclusiveWith && activeAttributes[exclusiveWith]) {
            activeAttributes[exclusiveWith] = false;
          }
        } else if (this.state.position) {
          var currentAttrs = this._relativeChar(this.state.position, 0, 'limit').copyOfAttributes();
          if (currentAttrs) {
            currentAttrs[attribute] = !currentAttrs[attribute];
            activeAttributes = currentAttrs;
            if (activeAttributes[attribute] && exclusiveWith && activeAttributes[exclusiveWith]) {
              activeAttributes[exclusiveWith] = false;
            }
          } else {
            activeAttributes = {};
            activeAttributes[attribute] = true;
          }
        }
        this.setState({ activeAttributes: activeAttributes });
      }
    }
  }, {
    key: '_lineAndAdvanceAtPosition',
    value: function _lineAndAdvanceAtPosition(position, positionEolStart) {
      var _lineContainingChar5 = (0, _coreEditorCommon.lineContainingChar)(this.state.lines, position, positionEolStart);

      var line = _lineContainingChar5.line;
      var index = _lineContainingChar5.index;
      var endOfLine = _lineContainingChar5.endOfLine;

      var advanceX = undefined;

      if ((0, _coreEditorCommon.charEq)(_coreRichText.BASE_CHAR, position) || endOfLine && positionEolStart) {
        advanceX = 0;
      } else {
        var chars = line.charsTo(position);
        advanceX = _coreTextFontMetrics2['default'].advanceXForChars(this.config.fontSize, chars);
      }

      return {
        advanceX: advanceX,
        line: line,
        index: index,
        endOfLine: endOfLine
      };
    }
  }, {
    key: '_coordinatesToPosition',
    value: function _coordinatesToPosition(_ref4) {
      var x = _ref4.x;
      var y = _ref4.y;

      // TODO this works for now since all line heights are the same, but get the heights of each line dynamically
      var lineHeight = _coreTextFontMetrics2['default'].lineHeight(this.config.fontSize);
      var lineIndex = Math.floor(y / lineHeight);

      if (lineIndex < 0) {
        // clicked before the first line, set cursor on the first line
        lineIndex = 0;
      } else if (lineIndex > this.state.lines.length - 1) {
        // clicked after the last line, set cursor on the last line
        lineIndex = this.state.lines.length - 1;
      }

      var line = this.state.lines[lineIndex];
      var position = undefined;
      var positionEolStart = undefined;

      if (!line) {
        position = _coreRichText.BASE_CHAR;
        positionEolStart = false;
      } else if (line.isEof()) {
        position = line.start;
        positionEolStart = true;
      } else {
        var indexAndCursor = _coreTextFontMetrics2['default'].indexAndCursorForXValue(this.config.fontSize, x, line.chars);
        position = this._charPositionRelativeToIndex(indexAndCursor.index, line.chars);

        // if clicked a line beginning (char position is end of last line) then position beginning of clicked line
        // note that the cursorX is relative to the beginning of the line
        var cursorX = indexAndCursor.cursorX;
        positionEolStart = cursorX === 0 || line.isEof();
      }

      return {
        position: position,
        positionEolStart: positionEolStart
      };
    }
  }]);

  return EditorStore;
})();

exports['default'] = _coreAlt2['default'].createStore(EditorStore);
module.exports = exports['default'];