const _ = require('underscore-plus');
const path = require('path');
const fs = require('fs-plus');
const Grim = require('grim');
const dedent = require('dedent');
const { CompositeDisposable, Disposable, Emitter } = require('event-kit');
const TextBuffer = require('text-buffer');
const { Point, Range } = TextBuffer;
const DecorationManager = require('./decoration-manager');
const Cursor = require('./cursor');
const Selection = require('./selection');
const NullGrammar = require('./null-grammar');
const TextMateLanguageMode = require('./text-mate-language-mode');
const ScopeDescriptor = require('./scope-descriptor');

const TextMateScopeSelector = require('first-mate').ScopeSelector;
const GutterContainer = require('./gutter-container');
let TextEditorComponent = null;
let TextEditorElement = null;
const {
  isDoubleWidthCharacter,
  isHalfWidthCharacter,
  isKoreanCharacter,
  isWrapBoundary
} = require('./text-utils');

const SERIALIZATION_VERSION = 1;
const NON_WHITESPACE_REGEXP = /\S/;
const ZERO_WIDTH_NBSP = '\ufeff';
let nextId = 0;

const DEFAULT_NON_WORD_CHARACTERS = '/\\()"\':,.;<>~!@#$%^&*|+=[]{}`?-…';

// Essential: This class represents all essential editing state for a single
// {TextBuffer}, including cursor and selection positions, folds, and soft wraps.
// If you're manipulating the state of an editor, use this class.
//
// A single {TextBuffer} can belong to multiple editors. For example, if the
// same file is open in two different panes, Atom creates a separate editor for
// each pane. If the buffer is manipulated the changes are reflected in both
// editors, but each maintains its own cursor position, folded lines, etc.
//
// ## Accessing TextEditor Instances
//
// The easiest way to get hold of `TextEditor` objects is by registering a callback
// with `::observeTextEditors` on the `atom.workspace` global. Your callback will
// then be called with all current editor instances and also when any editor is
// created in the future.
//
// ```js
// atom.workspace.observeTextEditors(editor => {
//   editor.insertText('Hello World')
// })
// ```
//
// ## Buffer vs. Screen Coordinates
//
// Because editors support folds and soft-wrapping, the lines on screen don't
// always match the lines in the buffer. For example, a long line that soft wraps
// twice renders as three lines on screen, but only represents one line in the
// buffer. Similarly, if rows 5-10 are folded, then row 6 on screen corresponds
// to row 11 in the buffer.
//
// Your choice of coordinates systems will depend on what you're trying to
// achieve. For example, if you're writing a command that jumps the cursor up or
// down by 10 lines, you'll want to use screen coordinates because the user
// probably wants to skip lines *on screen*. However, if you're writing a package
// that jumps between method definitions, you'll want to work in buffer
// coordinates.
//
// **When in doubt, just default to buffer coordinates**, then experiment with
// soft wraps and folds to ensure your code interacts with them correctly.
module.exports = class TextEditor {
  static setClipboard(clipboard) {
    this.clipboard = clipboard;
  }

  static setScheduler(scheduler) {
    if (TextEditorComponent == null) {
      TextEditorComponent = require('./text-editor-component');
    }
    return TextEditorComponent.setScheduler(scheduler);
  }

  static didUpdateStyles() {
    if (TextEditorComponent == null) {
      TextEditorComponent = require('./text-editor-component');
    }
    return TextEditorComponent.didUpdateStyles();
  }

  static didUpdateScrollbarStyles() {
    if (TextEditorComponent == null) {
      TextEditorComponent = require('./text-editor-component');
    }
    return TextEditorComponent.didUpdateScrollbarStyles();
  }

  static viewForItem(item) {
    return item.element || item;
  }

  static deserialize(state, atomEnvironment) {
    if (state.version !== SERIALIZATION_VERSION) return null;

    let bufferId = state.tokenizedBuffer
      ? state.tokenizedBuffer.bufferId
      : state.bufferId;

    try {
      state.buffer = atomEnvironment.project.bufferForIdSync(bufferId);
      if (!state.buffer) return null;
    } catch (error) {
      if (error.syscall === 'read') {
        return; // Error reading the file, don't deserialize an editor for it
      } else {
        throw error;
      }
    }

    state.assert = atomEnvironment.assert.bind(atomEnvironment);

    // Semantics of the readOnly flag have changed since its introduction.
    // Only respect readOnly2, which has been set with the current readOnly semantics.
    delete state.readOnly;
    state.readOnly = state.readOnly2;
    delete state.readOnly2;

    const editor = new TextEditor(state);
    if (state.registered) {
      const disposable = atomEnvironment.textEditors.add(editor);
      editor.onDidDestroy(() => disposable.dispose());
    }
    return editor;
  }

  constructor(params = {}) {
    if (this.constructor.clipboard == null) {
      throw new Error(
        'Must call TextEditor.setClipboard at least once before creating TextEditor instances'
      );
    }

    this.id = params.id != null ? params.id : nextId++;
    if (this.id >= nextId) {
      // Ensure that new editors get unique ids:
      nextId = this.id + 1;
    }
    this.initialScrollTopRow = params.initialScrollTopRow;
    this.initialScrollLeftColumn = params.initialScrollLeftColumn;
    this.decorationManager = params.decorationManager;
    this.selectionsMarkerLayer = params.selectionsMarkerLayer;
    this.mini = params.mini != null ? params.mini : false;
    this.keyboardInputEnabled =
      params.keyboardInputEnabled != null ? params.keyboardInputEnabled : true;
    this.readOnly = params.readOnly != null ? params.readOnly : false;
    this.placeholderText = params.placeholderText;
    this.showLineNumbers = params.showLineNumbers;
    this.assert = params.assert || (condition => condition);
    this.showInvisibles =
      params.showInvisibles != null ? params.showInvisibles : true;
    this.autoHeight = params.autoHeight;
    this.autoWidth = params.autoWidth;
    this.scrollPastEnd =
      params.scrollPastEnd != null ? params.scrollPastEnd : false;
    this.scrollSensitivity =
      params.scrollSensitivity != null ? params.scrollSensitivity : 40;
    this.editorWidthInChars = params.editorWidthInChars;
    this.invisibles = params.invisibles;
    this.showIndentGuide = params.showIndentGuide;
    this.softWrapped = params.softWrapped;
    this.softWrapAtPreferredLineLength = params.softWrapAtPreferredLineLength;
    this.preferredLineLength = params.preferredLineLength;
    this.showCursorOnSelection =
      params.showCursorOnSelection != null
        ? params.showCursorOnSelection
        : true;
    this.maxScreenLineLength = params.maxScreenLineLength;
    this.softTabs = params.softTabs != null ? params.softTabs : true;
    this.autoIndent = params.autoIndent != null ? params.autoIndent : true;
    this.autoIndentOnPaste =
      params.autoIndentOnPaste != null ? params.autoIndentOnPaste : true;
    this.undoGroupingInterval =
      params.undoGroupingInterval != null ? params.undoGroupingInterval : 300;
    this.softWrapped = params.softWrapped != null ? params.softWrapped : false;
    this.softWrapAtPreferredLineLength =
      params.softWrapAtPreferredLineLength != null
        ? params.softWrapAtPreferredLineLength
        : false;
    this.preferredLineLength =
      params.preferredLineLength != null ? params.preferredLineLength : 80;
    this.maxScreenLineLength =
      params.maxScreenLineLength != null ? params.maxScreenLineLength : 500;
    this.showLineNumbers =
      params.showLineNumbers != null ? params.showLineNumbers : true;
    const { tabLength = 2 } = params;

    this.alive = true;
    this.doBackgroundWork = this.doBackgroundWork.bind(this);
    this.serializationVersion = 1;
    this.suppressSelectionMerging = false;
    this.selectionFlashDuration = 500;
    this.gutterContainer = null;
    this.verticalScrollMargin = 2;
    this.horizontalScrollMargin = 6;
    this.lineHeightInPixels = null;
    this.defaultCharWidth = null;
    this.height = null;
    this.width = null;
    this.registered = false;
    this.atomicSoftTabs = true;
    this.emitter = new Emitter();
    this.disposables = new CompositeDisposable();
    this.cursors = [];
    this.cursorsByMarkerId = new Map();
    this.selections = [];
    this.hasTerminatedPendingState = false;

    if (params.buffer) {
      this.buffer = params.buffer;
    } else {
      this.buffer = new TextBuffer({
        shouldDestroyOnFileDelete() {
          return atom.config.get('core.closeDeletedFileTabs');
        }
      });
      this.buffer.setLanguageMode(
        new TextMateLanguageMode({ buffer: this.buffer, config: atom.config })
      );
    }

    const languageMode = this.buffer.getLanguageMode();
    this.languageModeSubscription =
      languageMode.onDidTokenize &&
      languageMode.onDidTokenize(() => {
        this.emitter.emit('did-tokenize');
      });
    if (this.languageModeSubscription)
      this.disposables.add(this.languageModeSubscription);

    if (params.displayLayer) {
      this.displayLayer = params.displayLayer;
    } else {
      const displayLayerParams = {
        invisibles: this.getInvisibles(),
        softWrapColumn: this.getSoftWrapColumn(),
        showIndentGuides: this.doesShowIndentGuide(),
        atomicSoftTabs:
          params.atomicSoftTabs != null ? params.atomicSoftTabs : true,
        tabLength,
        ratioForCharacter: this.ratioForCharacter.bind(this),
        isWrapBoundary,
        foldCharacter: ZERO_WIDTH_NBSP,
        softWrapHangingIndent:
          params.softWrapHangingIndentLength != null
            ? params.softWrapHangingIndentLength
            : 0
      };

      this.displayLayer = this.buffer.getDisplayLayer(params.displayLayerId);
      if (this.displayLayer) {
        this.displayLayer.reset(displayLayerParams);
        this.selectionsMarkerLayer = this.displayLayer.getMarkerLayer(
          params.selectionsMarkerLayerId
        );
      } else {
        this.displayLayer = this.buffer.addDisplayLayer(displayLayerParams);
      }
    }

    this.backgroundWorkHandle = requestIdleCallback(this.doBackgroundWork);
    this.disposables.add(
      new Disposable(() => {
        if (this.backgroundWorkHandle != null)
          return cancelIdleCallback(this.backgroundWorkHandle);
      })
    );

    this.defaultMarkerLayer = this.displayLayer.addMarkerLayer();
    if (!this.selectionsMarkerLayer) {
      this.selectionsMarkerLayer = this.addMarkerLayer({
        maintainHistory: true,
        persistent: true,
        role: 'selections'
      });
    }

    this.decorationManager = new DecorationManager(this);
    this.decorateMarkerLayer(this.selectionsMarkerLayer, { type: 'cursor' });
    if (!this.isMini()) this.decorateCursorLine();

    this.decorateMarkerLayer(this.displayLayer.foldsMarkerLayer, {
      type: 'line-number',
      class: 'folded'
    });

    for (let marker of this.selectionsMarkerLayer.getMarkers()) {
      this.addSelection(marker);
    }

    this.subscribeToBuffer();
    this.subscribeToDisplayLayer();

    if (this.cursors.length === 0 && !params.suppressCursorCreation) {
      const initialLine = Math.max(parseInt(params.initialLine) || 0, 0);
      const initialColumn = Math.max(parseInt(params.initialColumn) || 0, 0);
      this.addCursorAtBufferPosition([initialLine, initialColumn]);
    }

    this.gutterContainer = new GutterContainer(this);
    this.lineNumberGutter = this.gutterContainer.addGutter({
      name: 'line-number',
      type: 'line-number',
      priority: 0,
      visible: params.lineNumberGutterVisible
    });
  }

  get element() {
    return this.getElement();
  }

  get editorElement() {
    Grim.deprecate(dedent`\
      \`TextEditor.prototype.editorElement\` has always been private, but now
      it is gone. Reading the \`editorElement\` property still returns a
      reference to the editor element but this field will be removed in a
      later version of Atom, so we recommend using the \`element\` property instead.\
    `);

    return this.getElement();
  }

  get displayBuffer() {
    Grim.deprecate(dedent`\
      \`TextEditor.prototype.displayBuffer\` has always been private, but now
      it is gone. Reading the \`displayBuffer\` property now returns a reference
      to the containing \`TextEditor\`, which now provides *some* of the API of
      the defunct \`DisplayBuffer\` class.\
    `);
    return this;
  }

  get languageMode() {
    return this.buffer.getLanguageMode();
  }

  get tokenizedBuffer() {
    return this.buffer.getLanguageMode();
  }

  get rowsPerPage() {
    return this.getRowsPerPage();
  }

  decorateCursorLine() {
    this.cursorLineDecorations = [
      this.decorateMarkerLayer(this.selectionsMarkerLayer, {
        type: 'line',
        class: 'cursor-line',
        onlyEmpty: true
      }),
      this.decorateMarkerLayer(this.selectionsMarkerLayer, {
        type: 'line-number',
        class: 'cursor-line'
      }),
      this.decorateMarkerLayer(this.selectionsMarkerLayer, {
        type: 'line-number',
        class: 'cursor-line-no-selection',
        onlyHead: true,
        onlyEmpty: true
      })
    ];
  }

  doBackgroundWork(deadline) {
    const previousLongestRow = this.getApproximateLongestScreenRow();
    if (this.displayLayer.doBackgroundWork(deadline)) {
      this.backgroundWorkHandle = requestIdleCallback(this.doBackgroundWork);
    } else {
      this.backgroundWorkHandle = null;
    }

    if (
      this.component &&
      this.getApproximateLongestScreenRow() !== previousLongestRow
    ) {
      this.component.scheduleUpdate();
    }
  }

  update(params) {
    const displayLayerParams = {};

    for (let param of Object.keys(params)) {
      const value = params[param];

      switch (param) {
        case 'autoIndent':
          this.updateAutoIndent(value, false);
          break;

        case 'autoIndentOnPaste':
          this.updateAutoIndentOnPaste(value, false);
          break;

        case 'undoGroupingInterval':
          this.updateUndoGroupingInterval(value, false);
          break;

        case 'scrollSensitivity':
          this.updateScrollSensitivity(value, false);
          break;

        case 'encoding':
          this.updateEncoding(value, false);
          break;

        case 'softTabs':
          this.updateSoftTabs(value, false);
          break;

        case 'atomicSoftTabs':
          this.updateAtomicSoftTabs(value, false, displayLayerParams);
          break;

        case 'tabLength':
          this.updateTabLength(value, false, displayLayerParams);
          break;

        case 'softWrapped':
          this.updateSoftWrapped(value, false, displayLayerParams);
          break;

        case 'softWrapHangingIndentLength':
          this.updateSoftWrapHangingIndentLength(
            value,
            false,
            displayLayerParams
          );
          break;

        case 'softWrapAtPreferredLineLength':
          this.updateSoftWrapAtPreferredLineLength(
            value,
            false,
            displayLayerParams
          );
          break;

        case 'preferredLineLength':
          this.updatePreferredLineLength(value, false, displayLayerParams);
          break;

        case 'maxScreenLineLength':
          this.updateMaxScreenLineLength(value, false, displayLayerParams);
          break;

        case 'mini':
          this.updateMini(value, false, displayLayerParams);
          break;

        case 'readOnly':
          this.updateReadOnly(value, false);
          break;

        case 'keyboardInputEnabled':
          this.updateKeyboardInputEnabled(value, false);
          break;

        case 'placeholderText':
          this.updatePlaceholderText(value, false);
          break;

        case 'lineNumberGutterVisible':
          this.updateLineNumberGutterVisible(value, false);
          break;

        case 'showIndentGuide':
          this.updateShowIndentGuide(value, false, displayLayerParams);
          break;

        case 'showLineNumbers':
          this.updateShowLineNumbers(value, false);
          break;

        case 'showInvisibles':
          this.updateShowInvisibles(value, false, displayLayerParams);
          break;

        case 'invisibles':
          this.updateInvisibles(value, false, displayLayerParams);
          break;

        case 'editorWidthInChars':
          this.updateEditorWidthInChars(value, false, displayLayerParams);
          break;

        case 'width':
          this.updateWidth(value, false, displayLayerParams);
          break;

        case 'scrollPastEnd':
          this.updateScrollPastEnd(value, false);
          break;

        case 'autoHeight':
          this.updateAutoHight(value, false);
          break;

        case 'autoWidth':
          this.updateAutoWidth(value, false);
          break;

        case 'showCursorOnSelection':
          this.updateShowCursorOnSelection(value, false);
          break;

        default:
          if (param !== 'ref' && param !== 'key') {
            throw new TypeError(`Invalid TextEditor parameter: '${param}'`);
          }
      }
    }

    return this.finishUpdate(displayLayerParams);
  }

  finishUpdate(displayLayerParams = {}) {
    this.displayLayer.reset(displayLayerParams);

    if (this.component) {
      return this.component.getNextUpdatePromise();
    } else {
      return Promise.resolve();
    }
  }

  updateAutoIndent(value, finish) {
    this.autoIndent = value;
    if (finish) this.finishUpdate();
  }

  updateAutoIndentOnPaste(value, finish) {
    this.autoIndentOnPaste = value;
    if (finish) this.finishUpdate();
  }

  updateUndoGroupingInterval(value, finish) {
    this.undoGroupingInterval = value;
    if (finish) this.finishUpdate();
  }

  updateScrollSensitivity(value, finish) {
    this.scrollSensitivity = value;
    if (finish) this.finishUpdate();
  }

  updateEncoding(value, finish) {
    this.buffer.setEncoding(value);
    if (finish) this.finishUpdate();
  }

  updateSoftTabs(value, finish) {
    if (value !== this.softTabs) {
      this.softTabs = value;
    }
    if (finish) this.finishUpdate();
  }

  updateAtomicSoftTabs(value, finish, displayLayerParams = {}) {
    if (value !== this.displayLayer.atomicSoftTabs) {
      displayLayerParams.atomicSoftTabs = value;
    }
    if (finish) this.finishUpdate(displayLayerParams);
  }

  updateTabLength(value, finish, displayLayerParams = {}) {
    if (value > 0 && value !== this.displayLayer.tabLength) {
      displayLayerParams.tabLength = value;
    }
    if (finish) this.finishUpdate(displayLayerParams);
  }

  updateSoftWrapped(value, finish, displayLayerParams = {}) {
    if (value !== this.softWrapped) {
      this.softWrapped = value;
      displayLayerParams.softWrapColumn = this.getSoftWrapColumn();
      this.emitter.emit('did-change-soft-wrapped', this.isSoftWrapped());
    }
    if (finish) this.finishUpdate(displayLayerParams);
  }

  updateSoftWrapHangingIndentLength(value, finish, displayLayerParams = {}) {
    if (value !== this.displayLayer.softWrapHangingIndent) {
      displayLayerParams.softWrapHangingIndent = value;
    }
    if (finish) this.finishUpdate(displayLayerParams);
  }

  updateSoftWrapAtPreferredLineLength(value, finish, displayLayerParams = {}) {
    if (value !== this.softWrapAtPreferredLineLength) {
      this.softWrapAtPreferredLineLength = value;
      displayLayerParams.softWrapColumn = this.getSoftWrapColumn();
    }
    if (finish) this.finishUpdate(displayLayerParams);
  }

  updatePreferredLineLength(value, finish, displayLayerParams = {}) {
    if (value !== this.preferredLineLength) {
      this.preferredLineLength = value;
      displayLayerParams.softWrapColumn = this.getSoftWrapColumn();
    }
    if (finish) this.finishUpdate(displayLayerParams);
  }

  updateMaxScreenLineLength(value, finish, displayLayerParams = {}) {
    if (value !== this.maxScreenLineLength) {
      this.maxScreenLineLength = value;
      displayLayerParams.softWrapColumn = this.getSoftWrapColumn();
    }
    if (finish) this.finishUpdate(displayLayerParams);
  }

  updateMini(value, finish, displayLayerParams = {}) {
    if (value !== this.mini) {
      this.mini = value;
      this.emitter.emit('did-change-mini', value);
      displayLayerParams.invisibles = this.getInvisibles();
      displayLayerParams.softWrapColumn = this.getSoftWrapColumn();
      displayLayerParams.showIndentGuides = this.doesShowIndentGuide();
      if (this.mini) {
        for (let decoration of this.cursorLineDecorations) {
          decoration.destroy();
        }
        this.cursorLineDecorations = null;
      } else {
        this.decorateCursorLine();
      }
      if (this.component != null) {
        this.component.scheduleUpdate();
      }
    }
    if (finish) this.finishUpdate(displayLayerParams);
  }

  updateReadOnly(value, finish) {
    if (value !== this.readOnly) {
      this.readOnly = value;
      if (this.component != null) {
        this.component.scheduleUpdate();
      }
    }
    if (finish) this.finishUpdate();
  }

  updateKeyboardInputEnabled(value, finish) {
    if (value !== this.keyboardInputEnabled) {
      this.keyboardInputEnabled = value;
      if (this.component != null) {
        this.component.scheduleUpdate();
      }
    }
    if (finish) this.finishUpdate();
  }

  updatePlaceholderText(value, finish) {
    if (value !== this.placeholderText) {
      this.placeholderText = value;
      this.emitter.emit('did-change-placeholder-text', value);
    }
    if (finish) this.finishUpdate();
  }

  updateLineNumberGutterVisible(value, finish) {
    if (value !== this.lineNumberGutterVisible) {
      if (value) {
        this.lineNumberGutter.show();
      } else {
        this.lineNumberGutter.hide();
      }
      this.emitter.emit(
        'did-change-line-number-gutter-visible',
        this.lineNumberGutter.isVisible()
      );
    }
    if (finish) this.finishUpdate();
  }

  updateShowIndentGuide(value, finish, displayLayerParams = {}) {
    if (value !== this.showIndentGuide) {
      this.showIndentGuide = value;
      displayLayerParams.showIndentGuides = this.doesShowIndentGuide();
    }
    if (finish) this.finishUpdate(displayLayerParams);
  }

  updateShowLineNumbers(value, finish) {
    if (value !== this.showLineNumbers) {
      this.showLineNumbers = value;
      if (this.component != null) {
        this.component.scheduleUpdate();
      }
    }
    if (finish) this.finishUpdate();
  }

  updateShowInvisibles(value, finish, displayLayerParams = {}) {
    if (value !== this.showInvisibles) {
      this.showInvisibles = value;
      displayLayerParams.invisibles = this.getInvisibles();
    }
    if (finish) this.finishUpdate(displayLayerParams);
  }

  updateInvisibles(value, finish, displayLayerParams = {}) {
    if (!_.isEqual(value, this.invisibles)) {
      this.invisibles = value;
      displayLayerParams.invisibles = this.getInvisibles();
    }
    if (finish) this.finishUpdate(displayLayerParams);
  }

  updateEditorWidthInChars(value, finish, displayLayerParams = {}) {
    if (value > 0 && value !== this.editorWidthInChars) {
      this.editorWidthInChars = value;
      displayLayerParams.softWrapColumn = this.getSoftWrapColumn();
    }
    if (finish) this.finishUpdate(displayLayerParams);
  }

  updateWidth(value, finish, displayLayerParams = {}) {
    if (value !== this.width) {
      this.width = value;
      displayLayerParams.softWrapColumn = this.getSoftWrapColumn();
    }
    if (finish) this.finishUpdate(displayLayerParams);
  }

  updateScrollPastEnd(value, finish) {
    if (value !== this.scrollPastEnd) {
      this.scrollPastEnd = value;
      if (this.component) this.component.scheduleUpdate();
    }
    if (finish) this.finishUpdate();
  }

  updateAutoHight(value, finish) {
    if (value !== this.autoHeight) {
      this.autoHeight = value;
    }
    if (finish) this.finishUpdate();
  }

  updateAutoWidth(value, finish) {
    if (value !== this.autoWidth) {
      this.autoWidth = value;
    }
    if (finish) this.finishUpdate();
  }

  updateShowCursorOnSelection(value, finish) {
    if (value !== this.showCursorOnSelection) {
      this.showCursorOnSelection = value;
      if (this.component) this.component.scheduleUpdate();
    }
    if (finish) this.finishUpdate();
  }

  scheduleComponentUpdate() {
    if (this.component) this.component.scheduleUpdate();
  }

  serialize() {
    return {
      deserializer: 'TextEditor',
      version: SERIALIZATION_VERSION,

      displayLayerId: this.displayLayer.id,
      selectionsMarkerLayerId: this.selectionsMarkerLayer.id,

      initialScrollTopRow: this.getScrollTopRow(),
      initialScrollLeftColumn: this.getScrollLeftColumn(),

      tabLength: this.displayLayer.tabLength,
      atomicSoftTabs: this.displayLayer.atomicSoftTabs,
      softWrapHangingIndentLength: this.displayLayer.softWrapHangingIndent,

      id: this.id,
      bufferId: this.buffer.id,
      softTabs: this.softTabs,
      softWrapped: this.softWrapped,
      softWrapAtPreferredLineLength: this.softWrapAtPreferredLineLength,
      preferredLineLength: this.preferredLineLength,
      mini: this.mini,
      readOnly2: this.readOnly, // readOnly encompassed both readOnly and keyboardInputEnabled
      keyboardInputEnabled: this.keyboardInputEnabled,
      editorWidthInChars: this.editorWidthInChars,
      width: this.width,
      maxScreenLineLength: this.maxScreenLineLength,
      registered: this.registered,
      invisibles: this.invisibles,
      showInvisibles: this.showInvisibles,
      showIndentGuide: this.showIndentGuide,
      autoHeight: this.autoHeight,
      autoWidth: this.autoWidth
    };
  }

  subscribeToBuffer() {
    this.buffer.retain();
    this.disposables.add(
      this.buffer.onDidChangeLanguageMode(
        this.handleLanguageModeChange.bind(this)
      )
    );
    this.disposables.add(
      this.buffer.onDidChangePath(() => {
        this.emitter.emit('did-change-title', this.getTitle());
        this.emitter.emit('did-change-path', this.getPath());
      })
    );
    this.disposables.add(
      this.buffer.onDidChangeEncoding(() => {
        this.emitter.emit('did-change-encoding', this.getEncoding());
      })
    );
    this.disposables.add(this.buffer.onDidDestroy(() => this.destroy()));
    this.disposables.add(
      this.buffer.onDidChangeModified(() => {
        if (!this.hasTerminatedPendingState && this.buffer.isModified())
          this.terminatePendingState();
      })
    );
  }

  terminatePendingState() {
    if (!this.hasTerminatedPendingState)
      this.emitter.emit('did-terminate-pending-state');
    this.hasTerminatedPendingState = true;
  }

  onDidTerminatePendingState(callback) {
    return this.emitter.on('did-terminate-pending-state', callback);
  }

  subscribeToDisplayLayer() {
    this.disposables.add(
      this.displayLayer.onDidChange(changes => {
        this.mergeIntersectingSelections();
        if (this.component) this.component.didChangeDisplayLayer(changes);
        this.emitter.emit(
          'did-change',
          changes.map(change => new ChangeEvent(change))
        );
      })
    );
    this.disposables.add(
      this.displayLayer.onDidReset(() => {
        this.mergeIntersectingSelections();
        if (this.component) this.component.didResetDisplayLayer();
        this.emitter.emit('did-change', {});
      })
    );
    this.disposables.add(
      this.selectionsMarkerLayer.onDidCreateMarker(this.addSelection.bind(this))
    );
    return this.disposables.add(
      this.selectionsMarkerLayer.onDidUpdate(() =>
        this.component != null
          ? this.component.didUpdateSelections()
          : undefined
      )
    );
  }

  destroy() {
    if (!this.alive) return;
    this.alive = false;
    this.disposables.dispose();
    this.displayLayer.destroy();
    for (let selection of this.selections.slice()) {
      selection.destroy();
    }
    this.buffer.release();
    this.gutterContainer.destroy();
    this.emitter.emit('did-destroy');
    this.emitter.clear();
    if (this.component) this.component.element.component = null;
    this.component = null;
    this.lineNumberGutter.element = null;
  }

  isAlive() {
    return this.alive;
  }

  isDestroyed() {
    return !this.alive;
  }

  /*
  Section: Event Subscription
  */

  // Essential: Calls your `callback` when the buffer's title has changed.
  //
  // * `callback` {Function}
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidChangeTitle(callback) {
    return this.emitter.on('did-change-title', callback);
  }

  // Essential: Calls your `callback` when the buffer's path, and therefore title, has changed.
  //
  // * `callback` {Function}
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidChangePath(callback) {
    return this.emitter.on('did-change-path', callback);
  }

  // Essential: Invoke the given callback synchronously when the content of the
  // buffer changes.
  //
  // Because observers are invoked synchronously, it's important not to perform
  // any expensive operations via this method. Consider {::onDidStopChanging} to
  // delay expensive operations until after changes stop occurring.
  //
  // * `callback` {Function}
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidChange(callback) {
    return this.emitter.on('did-change', callback);
  }

  // Essential: Invoke `callback` when the buffer's contents change. It is
  // emit asynchronously 300ms after the last buffer change. This is a good place
  // to handle changes to the buffer without compromising typing performance.
  //
  // * `callback` {Function}
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidStopChanging(callback) {
    return this.getBuffer().onDidStopChanging(callback);
  }

  // Essential: Calls your `callback` when a {Cursor} is moved. If there are
  // multiple cursors, your callback will be called for each cursor.
  //
  // * `callback` {Function}
  //   * `event` {Object}
  //     * `oldBufferPosition` {Point}
  //     * `oldScreenPosition` {Point}
  //     * `newBufferPosition` {Point}
  //     * `newScreenPosition` {Point}
  //     * `textChanged` {Boolean}
  //     * `cursor` {Cursor} that triggered the event
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidChangeCursorPosition(callback) {
    return this.emitter.on('did-change-cursor-position', callback);
  }

  // Essential: Calls your `callback` when a selection's screen range changes.
  //
  // * `callback` {Function}
  //   * `event` {Object}
  //     * `oldBufferRange` {Range}
  //     * `oldScreenRange` {Range}
  //     * `newBufferRange` {Range}
  //     * `newScreenRange` {Range}
  //     * `selection` {Selection} that triggered the event
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidChangeSelectionRange(callback) {
    return this.emitter.on('did-change-selection-range', callback);
  }

  // Extended: Calls your `callback` when soft wrap was enabled or disabled.
  //
  // * `callback` {Function}
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidChangeSoftWrapped(callback) {
    return this.emitter.on('did-change-soft-wrapped', callback);
  }

  // Extended: Calls your `callback` when the buffer's encoding has changed.
  //
  // * `callback` {Function}
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidChangeEncoding(callback) {
    return this.emitter.on('did-change-encoding', callback);
  }

  // Extended: Calls your `callback` when the grammar that interprets and
  // colorizes the text has been changed. Immediately calls your callback with
  // the current grammar.
  //
  // * `callback` {Function}
  //   * `grammar` {Grammar}
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  observeGrammar(callback) {
    callback(this.getGrammar());
    return this.onDidChangeGrammar(callback);
  }

  // Extended: Calls your `callback` when the grammar that interprets and
  // colorizes the text has been changed.
  //
  // * `callback` {Function}
  //   * `grammar` {Grammar}
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidChangeGrammar(callback) {
    return this.buffer.onDidChangeLanguageMode(() => {
      callback(this.buffer.getLanguageMode().grammar);
    });
  }

  // Extended: Calls your `callback` when the result of {::isModified} changes.
  //
  // * `callback` {Function}
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidChangeModified(callback) {
    return this.getBuffer().onDidChangeModified(callback);
  }

  // Extended: Calls your `callback` when the buffer's underlying file changes on
  // disk at a moment when the result of {::isModified} is true.
  //
  // * `callback` {Function}
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidConflict(callback) {
    return this.getBuffer().onDidConflict(callback);
  }

  // Extended: Calls your `callback` before text has been inserted.
  //
  // * `callback` {Function}
  //   * `event` event {Object}
  //     * `text` {String} text to be inserted
  //     * `cancel` {Function} Call to prevent the text from being inserted
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onWillInsertText(callback) {
    return this.emitter.on('will-insert-text', callback);
  }

  // Extended: Calls your `callback` after text has been inserted.
  //
  // * `callback` {Function}
  //   * `event` event {Object}
  //     * `text` {String} text to be inserted
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidInsertText(callback) {
    return this.emitter.on('did-insert-text', callback);
  }

  // Essential: Invoke the given callback after the buffer is saved to disk.
  //
  // * `callback` {Function} to be called after the buffer is saved.
  //   * `event` {Object} with the following keys:
  //     * `path` The path to which the buffer was saved.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidSave(callback) {
    return this.getBuffer().onDidSave(callback);
  }

  // Essential: Invoke the given callback when the editor is destroyed.
  //
  // * `callback` {Function} to be called when the editor is destroyed.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidDestroy(callback) {
    return this.emitter.once('did-destroy', callback);
  }

  // Extended: Calls your `callback` when a {Cursor} is added to the editor.
  // Immediately calls your callback for each existing cursor.
  //
  // * `callback` {Function}
  //   * `cursor` {Cursor} that was added
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  observeCursors(callback) {
    this.getCursors().forEach(callback);
    return this.onDidAddCursor(callback);
  }

  // Extended: Calls your `callback` when a {Cursor} is added to the editor.
  //
  // * `callback` {Function}
  //   * `cursor` {Cursor} that was added
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidAddCursor(callback) {
    return this.emitter.on('did-add-cursor', callback);
  }

  // Extended: Calls your `callback` when a {Cursor} is removed from the editor.
  //
  // * `callback` {Function}
  //   * `cursor` {Cursor} that was removed
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidRemoveCursor(callback) {
    return this.emitter.on('did-remove-cursor', callback);
  }

  // Extended: Calls your `callback` when a {Selection} is added to the editor.
  // Immediately calls your callback for each existing selection.
  //
  // * `callback` {Function}
  //   * `selection` {Selection} that was added
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  observeSelections(callback) {
    this.getSelections().forEach(callback);
    return this.onDidAddSelection(callback);
  }

  // Extended: Calls your `callback` when a {Selection} is added to the editor.
  //
  // * `callback` {Function}
  //   * `selection` {Selection} that was added
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidAddSelection(callback) {
    return this.emitter.on('did-add-selection', callback);
  }

  // Extended: Calls your `callback` when a {Selection} is removed from the editor.
  //
  // * `callback` {Function}
  //   * `selection` {Selection} that was removed
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidRemoveSelection(callback) {
    return this.emitter.on('did-remove-selection', callback);
  }

  // Extended: Calls your `callback` with each {Decoration} added to the editor.
  // Calls your `callback` immediately for any existing decorations.
  //
  // * `callback` {Function}
  //   * `decoration` {Decoration}
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  observeDecorations(callback) {
    return this.decorationManager.observeDecorations(callback);
  }

  // Extended: Calls your `callback` when a {Decoration} is added to the editor.
  //
  // * `callback` {Function}
  //   * `decoration` {Decoration} that was added
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidAddDecoration(callback) {
    return this.decorationManager.onDidAddDecoration(callback);
  }

  // Extended: Calls your `callback` when a {Decoration} is removed from the editor.
  //
  // * `callback` {Function}
  //   * `decoration` {Decoration} that was removed
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidRemoveDecoration(callback) {
    return this.decorationManager.onDidRemoveDecoration(callback);
  }

  // Called by DecorationManager when a decoration is added.
  didAddDecoration(decoration) {
    if (this.component && decoration.isType('block')) {
      this.component.addBlockDecoration(decoration);
    }
  }

  // Extended: Calls your `callback` when the placeholder text is changed.
  //
  // * `callback` {Function}
  //   * `placeholderText` {String} new text
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidChangePlaceholderText(callback) {
    return this.emitter.on('did-change-placeholder-text', callback);
  }

  onDidChangeScrollTop(callback) {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.'
    );
    return this.getElement().onDidChangeScrollTop(callback);
  }

  onDidChangeScrollLeft(callback) {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::onDidChangeScrollLeft instead.'
    );
    return this.getElement().onDidChangeScrollLeft(callback);
  }

  onDidRequestAutoscroll(callback) {
    return this.emitter.on('did-request-autoscroll', callback);
  }

  // TODO Remove once the tabs package no longer uses .on subscriptions
  onDidChangeIcon(callback) {
    return this.emitter.on('did-change-icon', callback);
  }

  onDidUpdateDecorations(callback) {
    return this.decorationManager.onDidUpdateDecorations(callback);
  }

  // Retrieves the current buffer's URI.
  getURI() {
    return this.buffer.getUri();
  }

  // Create an {TextEditor} with its initial state based on this object
  copy() {
    const displayLayer = this.displayLayer.copy();
    const selectionsMarkerLayer = displayLayer.getMarkerLayer(
      this.buffer.getMarkerLayer(this.selectionsMarkerLayer.id).copy().id
    );
    const softTabs = this.getSoftTabs();
    return new TextEditor({
      buffer: this.buffer,
      selectionsMarkerLayer,
      softTabs,
      suppressCursorCreation: true,
      tabLength: this.getTabLength(),
      initialScrollTopRow: this.getScrollTopRow(),
      initialScrollLeftColumn: this.getScrollLeftColumn(),
      assert: this.assert,
      displayLayer,
      grammar: this.getGrammar(),
      autoWidth: this.autoWidth,
      autoHeight: this.autoHeight,
      showCursorOnSelection: this.showCursorOnSelection
    });
  }

  // Controls visibility based on the given {Boolean}.
  setVisible(visible) {
    if (visible) {
      const languageMode = this.buffer.getLanguageMode();
      if (languageMode.startTokenizing) languageMode.startTokenizing();
    }
  }

  setMini(mini) {
    this.updateMini(mini, true);
  }

  isMini() {
    return this.mini;
  }

  setReadOnly(readOnly) {
    this.updateReadOnly(readOnly, true);
  }

  isReadOnly() {
    return this.readOnly;
  }

  enableKeyboardInput(enabled) {
    this.updateKeyboardInputEnabled(enabled, true);
  }

  isKeyboardInputEnabled() {
    return this.keyboardInputEnabled;
  }

  onDidChangeMini(callback) {
    return this.emitter.on('did-change-mini', callback);
  }

  setLineNumberGutterVisible(lineNumberGutterVisible) {
    this.updateLineNumberGutterVisible(lineNumberGutterVisible, true);
  }

  isLineNumberGutterVisible() {
    return this.lineNumberGutter.isVisible();
  }

  anyLineNumberGutterVisible() {
    return this.getGutters().some(
      gutter => gutter.type === 'line-number' && gutter.visible
    );
  }

  onDidChangeLineNumberGutterVisible(callback) {
    return this.emitter.on('did-change-line-number-gutter-visible', callback);
  }

  // Essential: Calls your `callback` when a {Gutter} is added to the editor.
  // Immediately calls your callback for each existing gutter.
  //
  // * `callback` {Function}
  //   * `gutter` {Gutter} that currently exists/was added.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  observeGutters(callback) {
    return this.gutterContainer.observeGutters(callback);
  }

  // Essential: Calls your `callback` when a {Gutter} is added to the editor.
  //
  // * `callback` {Function}
  //   * `gutter` {Gutter} that was added.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidAddGutter(callback) {
    return this.gutterContainer.onDidAddGutter(callback);
  }

  // Essential: Calls your `callback` when a {Gutter} is removed from the editor.
  //
  // * `callback` {Function}
  //   * `name` The name of the {Gutter} that was removed.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidRemoveGutter(callback) {
    return this.gutterContainer.onDidRemoveGutter(callback);
  }

  // Set the number of characters that can be displayed horizontally in the
  // editor.
  //
  // * `editorWidthInChars` A {Number} representing the width of the
  // {TextEditorElement} in characters.
  setEditorWidthInChars(editorWidthInChars) {
    this.updateEditorWidthInChars(editorWidthInChars, true);
  }

  // Returns the editor width in characters.
  getEditorWidthInChars() {
    if (this.width != null && this.defaultCharWidth > 0) {
      return Math.max(0, Math.floor(this.width / this.defaultCharWidth));
    } else {
      return this.editorWidthInChars;
    }
  }

  /*
  Section: Buffer
  */

  // Essential: Retrieves the current {TextBuffer}.
  getBuffer() {
    return this.buffer;
  }

  /*
  Section: File Details
  */

  // Essential: Get the editor's title for display in other parts of the
  // UI such as the tabs.
  //
  // If the editor's buffer is saved, its title is the file name. If it is
  // unsaved, its title is "untitled".
  //
  // Returns a {String}.
  getTitle() {
    return this.getFileName() || 'untitled';
  }

  // Essential: Get unique title for display in other parts of the UI, such as
  // the window title.
  //
  // If the editor's buffer is unsaved, its title is "untitled"
  // If the editor's buffer is saved, its unique title is formatted as one
  // of the following,
  // * "<filename>" when it is the only editing buffer with this file name.
  // * "<filename> — <unique-dir-prefix>" when other buffers have this file name.
  //
  // Returns a {String}
  getLongTitle() {
    if (this.getPath()) {
      const fileName = this.getFileName();

      let myPathSegments;
      const openEditorPathSegmentsWithSameFilename = [];
      for (const textEditor of atom.workspace.getTextEditors()) {
        if (textEditor.getFileName() === fileName) {
          const pathSegments = fs
            .tildify(textEditor.getDirectoryPath())
            .split(path.sep);
          openEditorPathSegmentsWithSameFilename.push(pathSegments);
          if (textEditor === this) myPathSegments = pathSegments;
        }
      }

      if (
        !myPathSegments ||
        openEditorPathSegmentsWithSameFilename.length === 1
      )
        return fileName;

      let commonPathSegmentCount;
      for (let i = 0, { length } = myPathSegments; i < length; i++) {
        const myPathSegment = myPathSegments[i];
        if (
          openEditorPathSegmentsWithSameFilename.some(
            segments =>
              segments.length === i + 1 || segments[i] !== myPathSegment
          )
        ) {
          commonPathSegmentCount = i;
          break;
        }
      }

      return `${fileName} \u2014 ${path.join(
        ...myPathSegments.slice(commonPathSegmentCount)
      )}`;
    } else {
      return 'untitled';
    }
  }

  // Essential: Returns the {String} path of this editor's text buffer.
  getPath() {
    return this.buffer.getPath();
  }

  getFileName() {
    const fullPath = this.getPath();
    if (fullPath) return path.basename(fullPath);
  }

  getDirectoryPath() {
    const fullPath = this.getPath();
    if (fullPath) return path.dirname(fullPath);
  }

  // Extended: Returns the {String} character set encoding of this editor's text
  // buffer.
  getEncoding() {
    return this.buffer.getEncoding();
  }

  // Extended: Set the character set encoding to use in this editor's text
  // buffer.
  //
  // * `encoding` The {String} character set encoding name such as 'utf8'
  setEncoding(encoding) {
    this.buffer.setEncoding(encoding);
  }

  // Essential: Returns {Boolean} `true` if this editor has been modified.
  isModified() {
    return this.buffer.isModified();
  }

  // Essential: Returns {Boolean} `true` if this editor has no content.
  isEmpty() {
    return this.buffer.isEmpty();
  }

  /*
  Section: File Operations
  */

  // Essential: Saves the editor's text buffer.
  //
  // See {TextBuffer::save} for more details.
  save() {
    return this.buffer.save();
  }

  // Essential: Saves the editor's text buffer as the given path.
  //
  // See {TextBuffer::saveAs} for more details.
  //
  // * `filePath` A {String} path.
  saveAs(filePath) {
    return this.buffer.saveAs(filePath);
  }

  // Determine whether the user should be prompted to save before closing
  // this editor.
  shouldPromptToSave({ windowCloseRequested, projectHasPaths } = {}) {
    if (
      windowCloseRequested &&
      projectHasPaths &&
      atom.stateStore.isConnected()
    ) {
      return this.buffer.isInConflict();
    } else {
      return this.isModified() && !this.buffer.hasMultipleEditors();
    }
  }

  // Returns an {Object} to configure dialog shown when this editor is saved
  // via {Pane::saveItemAs}.
  getSaveDialogOptions() {
    return {};
  }

  /*
  Section: Reading Text
  */

  // Essential: Returns a {String} representing the entire contents of the editor.
  getText() {
    return this.buffer.getText();
  }

  // Essential: Get the text in the given {Range} in buffer coordinates.
  //
  // * `range` A {Range} or range-compatible {Array}.
  //
  // Returns a {String}.
  getTextInBufferRange(range) {
    return this.buffer.getTextInRange(range);
  }

  // Essential: Returns a {Number} representing the number of lines in the buffer.
  getLineCount() {
    return this.buffer.getLineCount();
  }

  // Essential: Returns a {Number} representing the number of screen lines in the
  // editor. This accounts for folds.
  getScreenLineCount() {
    return this.displayLayer.getScreenLineCount();
  }

  getApproximateScreenLineCount() {
    return this.displayLayer.getApproximateScreenLineCount();
  }

  // Essential: Returns a {Number} representing the last zero-indexed buffer row
  // number of the editor.
  getLastBufferRow() {
    return this.buffer.getLastRow();
  }

  // Essential: Returns a {Number} representing the last zero-indexed screen row
  // number of the editor.
  getLastScreenRow() {
    return this.getScreenLineCount() - 1;
  }

  // Essential: Returns a {String} representing the contents of the line at the
  // given buffer row.
  //
  // * `bufferRow` A {Number} representing a zero-indexed buffer row.
  lineTextForBufferRow(bufferRow) {
    return this.buffer.lineForRow(bufferRow);
  }

  // Essential: Returns a {String} representing the contents of the line at the
  // given screen row.
  //
  // * `screenRow` A {Number} representing a zero-indexed screen row.
  lineTextForScreenRow(screenRow) {
    const screenLine = this.screenLineForScreenRow(screenRow);
    if (screenLine) return screenLine.lineText;
  }

  logScreenLines(start = 0, end = this.getLastScreenRow()) {
    for (let row = start; row <= end; row++) {
      const line = this.lineTextForScreenRow(row);
      console.log(row, this.bufferRowForScreenRow(row), line, line.length);
    }
  }

  tokensForScreenRow(screenRow) {
    const tokens = [];
    let lineTextIndex = 0;
    const currentTokenScopes = [];
    const { lineText, tags } = this.screenLineForScreenRow(screenRow);
    for (const tag of tags) {
      if (this.displayLayer.isOpenTag(tag)) {
        currentTokenScopes.push(this.displayLayer.classNameForTag(tag));
      } else if (this.displayLayer.isCloseTag(tag)) {
        currentTokenScopes.pop();
      } else {
        tokens.push({
          text: lineText.substr(lineTextIndex, tag),
          scopes: currentTokenScopes.slice()
        });
        lineTextIndex += tag;
      }
    }
    return tokens;
  }

  screenLineForScreenRow(screenRow) {
    return this.displayLayer.getScreenLine(screenRow);
  }

  bufferRowForScreenRow(screenRow) {
    return this.displayLayer.translateScreenPosition(Point(screenRow, 0)).row;
  }

  bufferRowsForScreenRows(startScreenRow, endScreenRow) {
    return this.displayLayer.bufferRowsForScreenRows(
      startScreenRow,
      endScreenRow + 1
    );
  }

  screenRowForBufferRow(row) {
    return this.displayLayer.translateBufferPosition(Point(row, 0)).row;
  }

  getRightmostScreenPosition() {
    return this.displayLayer.getRightmostScreenPosition();
  }

  getApproximateRightmostScreenPosition() {
    return this.displayLayer.getApproximateRightmostScreenPosition();
  }

  getMaxScreenLineLength() {
    return this.getRightmostScreenPosition().column;
  }

  getLongestScreenRow() {
    return this.getRightmostScreenPosition().row;
  }

  getApproximateLongestScreenRow() {
    return this.getApproximateRightmostScreenPosition().row;
  }

  lineLengthForScreenRow(screenRow) {
    return this.displayLayer.lineLengthForScreenRow(screenRow);
  }

  // Returns the range for the given buffer row.
  //
  // * `row` A row {Number}.
  // * `options` (optional) An options hash with an `includeNewline` key.
  //
  // Returns a {Range}.
  bufferRangeForBufferRow(row, options) {
    return this.buffer.rangeForRow(row, options && options.includeNewline);
  }

  // Get the text in the given {Range}.
  //
  // Returns a {String}.
  getTextInRange(range) {
    return this.buffer.getTextInRange(range);
  }

  // {Delegates to: TextBuffer.isRowBlank}
  isBufferRowBlank(bufferRow) {
    return this.buffer.isRowBlank(bufferRow);
  }

  // {Delegates to: TextBuffer.nextNonBlankRow}
  nextNonBlankBufferRow(bufferRow) {
    return this.buffer.nextNonBlankRow(bufferRow);
  }

  // {Delegates to: TextBuffer.getEndPosition}
  getEofBufferPosition() {
    return this.buffer.getEndPosition();
  }

  // Essential: Get the {Range} of the paragraph surrounding the most recently added
  // cursor.
  //
  // Returns a {Range}.
  getCurrentParagraphBufferRange() {
    return this.getLastCursor().getCurrentParagraphBufferRange();
  }

  /*
  Section: Mutating Text
  */

  // Essential: Replaces the entire contents of the buffer with the given {String}.
  //
  // * `text` A {String} to replace with
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
  setText(text, options = {}) {
    if (!this.ensureWritable('setText', options)) return;
    return this.buffer.setText(text);
  }

  // Essential: Set the text in the given {Range} in buffer coordinates.
  //
  // * `range` A {Range} or range-compatible {Array}.
  // * `text` A {String}
  // * `options` (optional) {Object}
  //   * `normalizeLineEndings` (optional) {Boolean} (default: true)
  //   * `undo` (optional) *Deprecated* {String} 'skip' will skip the undo system. This property is deprecated. Call groupLastChanges() on the {TextBuffer} afterward instead.
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  //
  // Returns the {Range} of the newly-inserted text.
  setTextInBufferRange(range, text, options = {}) {
    if (!this.ensureWritable('setTextInBufferRange', options)) return;
    return this.getBuffer().setTextInRange(range, text, options);
  }

  // Essential: For each selection, replace the selected text with the given text.
  //
  // * `text` A {String} representing the text to insert.
  // * `options` (optional) See {Selection::insertText}.
  //
  // Returns a {Range} when the text has been inserted. Returns a {Boolean} `false` when the text has not been inserted.
  insertText(text, options = {}) {
    if (!this.ensureWritable('insertText', options)) return;
    if (!this.emitWillInsertTextEvent(text)) return false;

    let groupLastChanges = false;
    if (options.undo === 'skip') {
      options = Object.assign({}, options);
      delete options.undo;
      groupLastChanges = true;
    }

    const groupingInterval = options.groupUndo ? this.undoGroupingInterval : 0;
    if (options.autoIndentNewline == null)
      options.autoIndentNewline = this.shouldAutoIndent();
    if (options.autoDecreaseIndent == null)
      options.autoDecreaseIndent = this.shouldAutoIndent();
    const result = this.mutateSelectedText(selection => {
      const range = selection.insertText(text, options);
      const didInsertEvent = { text, range };
      this.emitter.emit('did-insert-text', didInsertEvent);
      return range;
    }, groupingInterval);
    if (groupLastChanges) this.buffer.groupLastChanges();
    return result;
  }

  // Essential: For each selection, replace the selected text with a newline.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  insertNewline(options = {}) {
    return this.insertText('\n', options);
  }

  // Essential: For each selection, if the selection is empty, delete the character
  // following the cursor. Otherwise delete the selected text.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  delete(options = {}) {
    if (!this.ensureWritable('delete', options)) return;
    return this.mutateSelectedText(selection => selection.delete(options));
  }

  // Essential: For each selection, if the selection is empty, delete the character
  // preceding the cursor. Otherwise delete the selected text.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  backspace(options = {}) {
    if (!this.ensureWritable('backspace', options)) return;
    return this.mutateSelectedText(selection => selection.backspace(options));
  }

  // Extended: Mutate the text of all the selections in a single transaction.
  //
  // All the changes made inside the given {Function} can be reverted with a
  // single call to {::undo}.
  //
  // * `fn` A {Function} that will be called once for each {Selection}. The first
  //      argument will be a {Selection} and the second argument will be the
  //      {Number} index of that selection.
  mutateSelectedText(fn, groupingInterval = 0) {
    return this.mergeIntersectingSelections(() => {
      return this.transact(groupingInterval, () => {
        return this.getSelectionsOrderedByBufferPosition().map(
          (selection, index) => fn(selection, index)
        );
      });
    });
  }

  // Move lines intersecting the most recent selection or multiple selections
  // up by one row in screen coordinates.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  moveLineUp(options = {}) {
    if (!this.ensureWritable('moveLineUp', options)) return;

    const selections = this.getSelectedBufferRanges().sort((a, b) =>
      a.compare(b)
    );

    if (selections[0].start.row === 0) return;
    if (
      selections[selections.length - 1].start.row === this.getLastBufferRow() &&
      this.buffer.getLastLine() === ''
    )
      return;

    this.transact(() => {
      const newSelectionRanges = [];

      while (selections.length > 0) {
        // Find selections spanning a contiguous set of lines
        const selection = selections.shift();
        const selectionsToMove = [selection];

        while (
          selection.end.row ===
          (selections[0] != null ? selections[0].start.row : undefined)
        ) {
          selectionsToMove.push(selections[0]);
          selection.end.row = selections[0].end.row;
          selections.shift();
        }

        // Compute the buffer range spanned by all these selections, expanding it
        // so that it includes any folded region that intersects them.
        let startRow = selection.start.row;
        let endRow = selection.end.row;
        if (
          selection.end.row > selection.start.row &&
          selection.end.column === 0
        ) {
          // Don't move the last line of a multi-line selection if the selection ends at column 0
          endRow--;
        }

        startRow = this.displayLayer.findBoundaryPrecedingBufferRow(startRow);
        endRow = this.displayLayer.findBoundaryFollowingBufferRow(endRow + 1);
        const linesRange = new Range(Point(startRow, 0), Point(endRow, 0));

        // If selected line range is preceded by a fold, one line above on screen
        // could be multiple lines in the buffer.
        const precedingRow = this.displayLayer.findBoundaryPrecedingBufferRow(
          startRow - 1
        );
        const insertDelta = linesRange.start.row - precedingRow;

        // Any folds in the text that is moved will need to be re-created.
        // It includes the folds that were intersecting with the selection.
        const rangesToRefold = this.displayLayer
          .destroyFoldsIntersectingBufferRange(linesRange)
          .map(range => range.translate([-insertDelta, 0]));

        // Delete lines spanned by selection and insert them on the preceding buffer row
        let lines = this.buffer.getTextInRange(linesRange);
        if (lines[lines.length - 1] !== '\n') {
          lines += this.buffer.lineEndingForRow(linesRange.end.row - 2);
        }
        this.buffer.delete(linesRange);
        this.buffer.insert([precedingRow, 0], lines);

        // Restore folds that existed before the lines were moved
        for (let rangeToRefold of rangesToRefold) {
          this.displayLayer.foldBufferRange(rangeToRefold);
        }

        for (const selectionToMove of selectionsToMove) {
          newSelectionRanges.push(selectionToMove.translate([-insertDelta, 0]));
        }
      }

      this.setSelectedBufferRanges(newSelectionRanges, {
        autoscroll: false,
        preserveFolds: true
      });
      if (this.shouldAutoIndent()) this.autoIndentSelectedRows();
      this.scrollToBufferPosition([newSelectionRanges[0].start.row, 0]);
    });
  }

  // Move lines intersecting the most recent selection or multiple selections
  // down by one row in screen coordinates.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  moveLineDown(options = {}) {
    if (!this.ensureWritable('moveLineDown', options)) return;

    const selections = this.getSelectedBufferRanges();
    selections.sort((a, b) => b.compare(a));

    this.transact(() => {
      this.consolidateSelections();
      const newSelectionRanges = [];

      while (selections.length > 0) {
        // Find selections spanning a contiguous set of lines
        const selection = selections.shift();
        const selectionsToMove = [selection];

        // if the current selection start row matches the next selections' end row - make them one selection
        while (
          selection.start.row ===
          (selections[0] != null ? selections[0].end.row : undefined)
        ) {
          selectionsToMove.push(selections[0]);
          selection.start.row = selections[0].start.row;
          selections.shift();
        }

        // Compute the buffer range spanned by all these selections, expanding it
        // so that it includes any folded region that intersects them.
        let startRow = selection.start.row;
        let endRow = selection.end.row;
        if (
          selection.end.row > selection.start.row &&
          selection.end.column === 0
        ) {
          // Don't move the last line of a multi-line selection if the selection ends at column 0
          endRow--;
        }

        startRow = this.displayLayer.findBoundaryPrecedingBufferRow(startRow);
        endRow = this.displayLayer.findBoundaryFollowingBufferRow(endRow + 1);
        const linesRange = new Range(Point(startRow, 0), Point(endRow, 0));

        // If selected line range is followed by a fold, one line below on screen
        // could be multiple lines in the buffer. But at the same time, if the
        // next buffer row is wrapped, one line in the buffer can represent many
        // screen rows.
        const followingRow = Math.min(
          this.buffer.getLineCount(),
          this.displayLayer.findBoundaryFollowingBufferRow(endRow + 1)
        );
        const insertDelta = followingRow - linesRange.end.row;

        // Any folds in the text that is moved will need to be re-created.
        // It includes the folds that were intersecting with the selection.
        const rangesToRefold = this.displayLayer
          .destroyFoldsIntersectingBufferRange(linesRange)
          .map(range => range.translate([insertDelta, 0]));

        // Delete lines spanned by selection and insert them on the following correct buffer row
        let lines = this.buffer.getTextInRange(linesRange);
        if (followingRow - 1 === this.buffer.getLastRow()) {
          lines = `\n${lines}`;
        }

        this.buffer.insert([followingRow, 0], lines);
        this.buffer.delete(linesRange);

        // Restore folds that existed before the lines were moved
        for (let rangeToRefold of rangesToRefold) {
          this.displayLayer.foldBufferRange(rangeToRefold);
        }

        for (const selectionToMove of selectionsToMove) {
          newSelectionRanges.push(selectionToMove.translate([insertDelta, 0]));
        }
      }

      this.setSelectedBufferRanges(newSelectionRanges, {
        autoscroll: false,
        preserveFolds: true
      });
      if (this.shouldAutoIndent()) this.autoIndentSelectedRows();
      this.scrollToBufferPosition([newSelectionRanges[0].start.row - 1, 0]);
    });
  }

  // Move any active selections one column to the left.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  moveSelectionLeft(options = {}) {
    if (!this.ensureWritable('moveSelectionLeft', options)) return;
    const selections = this.getSelectedBufferRanges();
    const noSelectionAtStartOfLine = selections.every(
      selection => selection.start.column !== 0
    );

    const translationDelta = [0, -1];
    const translatedRanges = [];

    if (noSelectionAtStartOfLine) {
      this.transact(() => {
        for (let selection of selections) {
          const charToLeftOfSelection = new Range(
            selection.start.translate(translationDelta),
            selection.start
          );
          const charTextToLeftOfSelection = this.buffer.getTextInRange(
            charToLeftOfSelection
          );

          this.buffer.insert(selection.end, charTextToLeftOfSelection);
          this.buffer.delete(charToLeftOfSelection);
          translatedRanges.push(selection.translate(translationDelta));
        }

        this.setSelectedBufferRanges(translatedRanges);
      });
    }
  }

  // Move any active selections one column to the right.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  moveSelectionRight(options = {}) {
    if (!this.ensureWritable('moveSelectionRight', options)) return;
    const selections = this.getSelectedBufferRanges();
    const noSelectionAtEndOfLine = selections.every(selection => {
      return (
        selection.end.column !== this.buffer.lineLengthForRow(selection.end.row)
      );
    });

    const translationDelta = [0, 1];
    const translatedRanges = [];

    if (noSelectionAtEndOfLine) {
      this.transact(() => {
        for (let selection of selections) {
          const charToRightOfSelection = new Range(
            selection.end,
            selection.end.translate(translationDelta)
          );
          const charTextToRightOfSelection = this.buffer.getTextInRange(
            charToRightOfSelection
          );

          this.buffer.delete(charToRightOfSelection);
          this.buffer.insert(selection.start, charTextToRightOfSelection);
          translatedRanges.push(selection.translate(translationDelta));
        }

        this.setSelectedBufferRanges(translatedRanges);
      });
    }
  }

  // Duplicate all lines containing active selections.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  duplicateLines(options = {}) {
    if (!this.ensureWritable('duplicateLines', options)) return;
    this.transact(() => {
      const selections = this.getSelectionsOrderedByBufferPosition();
      const previousSelectionRanges = [];

      let i = selections.length - 1;
      while (i >= 0) {
        const j = i;
        previousSelectionRanges[i] = selections[i].getBufferRange();
        if (selections[i].isEmpty()) {
          const { start } = selections[i].getScreenRange();
          selections[i].setScreenRange([[start.row, 0], [start.row + 1, 0]], {
            preserveFolds: true
          });
        }
        let [startRow, endRow] = selections[i].getBufferRowRange();
        endRow++;
        while (i > 0) {
          const [
            previousSelectionStartRow,
            previousSelectionEndRow
          ] = selections[i - 1].getBufferRowRange();
          if (previousSelectionEndRow === startRow) {
            startRow = previousSelectionStartRow;
            previousSelectionRanges[i - 1] = selections[i - 1].getBufferRange();
            i--;
          } else {
            break;
          }
        }

        const intersectingFolds = this.displayLayer.foldsIntersectingBufferRange(
          [[startRow, 0], [endRow, 0]]
        );
        let textToDuplicate = this.getTextInBufferRange([
          [startRow, 0],
          [endRow, 0]
        ]);
        if (endRow > this.getLastBufferRow())
          textToDuplicate = `\n${textToDuplicate}`;
        this.buffer.insert([endRow, 0], textToDuplicate);

        const insertedRowCount = endRow - startRow;

        for (let k = i; k <= j; k++) {
          selections[k].setBufferRange(
            previousSelectionRanges[k].translate([insertedRowCount, 0])
          );
        }

        for (const fold of intersectingFolds) {
          const foldRange = this.displayLayer.bufferRangeForFold(fold);
          this.displayLayer.foldBufferRange(
            foldRange.translate([insertedRowCount, 0])
          );
        }

        i--;
      }
    });
  }

  replaceSelectedText(options, fn) {
    this.mutateSelectedText(selection => {
      selection.getBufferRange();
      if (options && options.selectWordIfEmpty && selection.isEmpty()) {
        selection.selectWord();
      }
      const text = selection.getText();
      selection.deleteSelectedText();
      const range = selection.insertText(fn(text));
      selection.setBufferRange(range);
    });
  }

  // Split multi-line selections into one selection per line.
  //
  // Operates on all selections. This method breaks apart all multi-line
  // selections to create multiple single-line selections that cumulatively cover
  // the same original area.
  splitSelectionsIntoLines() {
    this.mergeIntersectingSelections(() => {
      for (const selection of this.getSelections()) {
        const range = selection.getBufferRange();
        if (range.isSingleLine()) continue;

        const { start, end } = range;
        this.addSelectionForBufferRange([start, [start.row, Infinity]]);
        let { row } = start;
        while (++row < end.row) {
          this.addSelectionForBufferRange([[row, 0], [row, Infinity]]);
        }
        if (end.column !== 0)
          this.addSelectionForBufferRange([
            [end.row, 0],
            [end.row, end.column]
          ]);
        selection.destroy();
      }
    });
  }

  // Extended: For each selection, transpose the selected text.
  //
  // If the selection is empty, the characters preceding and following the cursor
  // are swapped. Otherwise, the selected characters are reversed.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  transpose(options = {}) {
    if (!this.ensureWritable('transpose', options)) return;
    this.mutateSelectedText(selection => {
      if (selection.isEmpty()) {
        selection.selectRight();
        const text = selection.getText();
        selection.delete();
        selection.cursor.moveLeft();
        selection.insertText(text);
      } else {
        selection.insertText(
          selection
            .getText()
            .split('')
            .reverse()
            .join('')
        );
      }
    });
  }

  // Extended: Convert the selected text to upper case.
  //
  // For each selection, if the selection is empty, converts the containing word
  // to upper case. Otherwise convert the selected text to upper case.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  upperCase(options = {}) {
    if (!this.ensureWritable('upperCase', options)) return;
    this.replaceSelectedText({ selectWordIfEmpty: true }, text =>
      text.toUpperCase(options)
    );
  }

  // Extended: Convert the selected text to lower case.
  //
  // For each selection, if the selection is empty, converts the containing word
  // to upper case. Otherwise convert the selected text to upper case.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  lowerCase(options = {}) {
    if (!this.ensureWritable('lowerCase', options)) return;
    this.replaceSelectedText({ selectWordIfEmpty: true }, text =>
      text.toLowerCase(options)
    );
  }

  // Extended: Toggle line comments for rows intersecting selections.
  //
  // If the current grammar doesn't support comments, does nothing.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  toggleLineCommentsInSelection(options = {}) {
    if (!this.ensureWritable('toggleLineCommentsInSelection', options)) return;
    this.mutateSelectedText(selection => selection.toggleLineComments(options));
  }

  // Convert multiple lines to a single line.
  //
  // Operates on all selections. If the selection is empty, joins the current
  // line with the next line. Otherwise it joins all lines that intersect the
  // selection.
  //
  // Joining a line means that multiple lines are converted to a single line with
  // the contents of each of the original non-empty lines separated by a space.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  joinLines(options = {}) {
    if (!this.ensureWritable('joinLines', options)) return;
    this.mutateSelectedText(selection => selection.joinLines());
  }

  // Extended: For each cursor, insert a newline at beginning the following line.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  insertNewlineBelow(options = {}) {
    if (!this.ensureWritable('insertNewlineBelow', options)) return;
    this.transact(() => {
      this.moveToEndOfLine();
      this.insertNewline(options);
    });
  }

  // Extended: For each cursor, insert a newline at the end of the preceding line.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  insertNewlineAbove(options = {}) {
    if (!this.ensureWritable('insertNewlineAbove', options)) return;
    this.transact(() => {
      const bufferRow = this.getCursorBufferPosition().row;
      const indentLevel = this.indentationForBufferRow(bufferRow);
      const onFirstLine = bufferRow === 0;

      this.moveToBeginningOfLine();
      this.moveLeft();
      this.insertNewline(options);

      if (
        this.shouldAutoIndent() &&
        this.indentationForBufferRow(bufferRow) < indentLevel
      ) {
        this.setIndentationForBufferRow(bufferRow, indentLevel);
      }

      if (onFirstLine) {
        this.moveUp();
        this.moveToEndOfLine();
      }
    });
  }

  // Extended: For each selection, if the selection is empty, delete all characters
  // of the containing word that precede the cursor. Otherwise delete the
  // selected text.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  deleteToBeginningOfWord(options = {}) {
    if (!this.ensureWritable('deleteToBeginningOfWord', options)) return;
    this.mutateSelectedText(selection =>
      selection.deleteToBeginningOfWord(options)
    );
  }

  // Extended: Similar to {::deleteToBeginningOfWord}, but deletes only back to the
  // previous word boundary.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  deleteToPreviousWordBoundary(options = {}) {
    if (!this.ensureWritable('deleteToPreviousWordBoundary', options)) return;
    this.mutateSelectedText(selection =>
      selection.deleteToPreviousWordBoundary(options)
    );
  }

  // Extended: Similar to {::deleteToEndOfWord}, but deletes only up to the
  // next word boundary.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  deleteToNextWordBoundary(options = {}) {
    if (!this.ensureWritable('deleteToNextWordBoundary', options)) return;
    this.mutateSelectedText(selection =>
      selection.deleteToNextWordBoundary(options)
    );
  }

  // Extended: For each selection, if the selection is empty, delete all characters
  // of the containing subword following the cursor. Otherwise delete the selected
  // text.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  deleteToBeginningOfSubword(options = {}) {
    if (!this.ensureWritable('deleteToBeginningOfSubword', options)) return;
    this.mutateSelectedText(selection =>
      selection.deleteToBeginningOfSubword(options)
    );
  }

  // Extended: For each selection, if the selection is empty, delete all characters
  // of the containing subword following the cursor. Otherwise delete the selected
  // text.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  deleteToEndOfSubword(options = {}) {
    if (!this.ensureWritable('deleteToEndOfSubword', options)) return;
    this.mutateSelectedText(selection =>
      selection.deleteToEndOfSubword(options)
    );
  }

  // Extended: For each selection, if the selection is empty, delete all characters
  // of the containing line that precede the cursor. Otherwise delete the
  // selected text.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  deleteToBeginningOfLine(options = {}) {
    if (!this.ensureWritable('deleteToBeginningOfLine', options)) return;
    this.mutateSelectedText(selection =>
      selection.deleteToBeginningOfLine(options)
    );
  }

  // Extended: For each selection, if the selection is not empty, deletes the
  // selection; otherwise, deletes all characters of the containing line
  // following the cursor. If the cursor is already at the end of the line,
  // deletes the following newline.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  deleteToEndOfLine(options = {}) {
    if (!this.ensureWritable('deleteToEndOfLine', options)) return;
    this.mutateSelectedText(selection => selection.deleteToEndOfLine(options));
  }

  // Extended: For each selection, if the selection is empty, delete all characters
  // of the containing word following the cursor. Otherwise delete the selected
  // text.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  deleteToEndOfWord(options = {}) {
    if (!this.ensureWritable('deleteToEndOfWord', options)) return;
    this.mutateSelectedText(selection => selection.deleteToEndOfWord(options));
  }

  // Extended: Delete all lines intersecting selections.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  deleteLine(options = {}) {
    if (!this.ensureWritable('deleteLine', options)) return;
    this.mergeSelectionsOnSameRows();
    this.mutateSelectedText(selection => selection.deleteLine(options));
  }

  // Private: Ensure that this editor is not marked read-only before allowing a buffer modification to occur. If
  // the editor is read-only, require an explicit opt-in option to proceed (`bypassReadOnly`) or throw an Error.
  ensureWritable(methodName, opts) {
    if (!opts.bypassReadOnly && this.isReadOnly()) {
      if (atom.inDevMode() || atom.inSpecMode()) {
        const e = new Error('Attempt to mutate a read-only TextEditor');
        e.detail =
          `Your package is attempting to call ${methodName} on an editor that has been marked read-only. ` +
          'Pass {bypassReadOnly: true} to modify it anyway, or test editors with .isReadOnly() before attempting ' +
          'modifications.';
        throw e;
      }

      return false;
    }

    return true;
  }

  /*
  Section: History
  */

  // Essential: Undo the last change.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  undo(options = {}) {
    if (!this.ensureWritable('undo', options)) return;
    this.avoidMergingSelections(() =>
      this.buffer.undo({ selectionsMarkerLayer: this.selectionsMarkerLayer })
    );
    this.getLastSelection().autoscroll();
  }

  // Essential: Redo the last change.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
  redo(options = {}) {
    if (!this.ensureWritable('redo', options)) return;
    this.avoidMergingSelections(() =>
      this.buffer.redo({ selectionsMarkerLayer: this.selectionsMarkerLayer })
    );
    this.getLastSelection().autoscroll();
  }

  // Extended: Batch multiple operations as a single undo/redo step.
  //
  // Any group of operations that are logically grouped from the perspective of
  // undoing and redoing should be performed in a transaction. If you want to
  // abort the transaction, call {::abortTransaction} to terminate the function's
  // execution and revert any changes performed up to the abortion.
  //
  // * `groupingInterval` (optional) The {Number} of milliseconds for which this
  //   transaction should be considered 'groupable' after it begins. If a transaction
  //   with a positive `groupingInterval` is committed while the previous transaction is
  //   still 'groupable', the two transactions are merged with respect to undo and redo.
  // * `fn` A {Function} to call inside the transaction.
  transact(groupingInterval, fn) {
    const options = { selectionsMarkerLayer: this.selectionsMarkerLayer };
    if (typeof groupingInterval === 'function') {
      fn = groupingInterval;
    } else {
      options.groupingInterval = groupingInterval;
    }
    return this.buffer.transact(options, fn);
  }

  // Extended: Abort an open transaction, undoing any operations performed so far
  // within the transaction.
  abortTransaction() {
    return this.buffer.abortTransaction();
  }

  // Extended: Create a pointer to the current state of the buffer for use
  // with {::revertToCheckpoint} and {::groupChangesSinceCheckpoint}.
  //
  // Returns a checkpoint value.
  createCheckpoint() {
    return this.buffer.createCheckpoint({
      selectionsMarkerLayer: this.selectionsMarkerLayer
    });
  }

  // Extended: Revert the buffer to the state it was in when the given
  // checkpoint was created.
  //
  // The redo stack will be empty following this operation, so changes since the
  // checkpoint will be lost. If the given checkpoint is no longer present in the
  // undo history, no changes will be made to the buffer and this method will
  // return `false`.
  //
  // * `checkpoint` The checkpoint to revert to.
  //
  // Returns a {Boolean} indicating whether the operation succeeded.
  revertToCheckpoint(checkpoint) {
    return this.buffer.revertToCheckpoint(checkpoint);
  }

  // Extended: Group all changes since the given checkpoint into a single
  // transaction for purposes of undo/redo.
  //
  // If the given checkpoint is no longer present in the undo history, no
  // grouping will be performed and this method will return `false`.
  //
  // * `checkpoint` The checkpoint from which to group changes.
  //
  // Returns a {Boolean} indicating whether the operation succeeded.
  groupChangesSinceCheckpoint(checkpoint) {
    return this.buffer.groupChangesSinceCheckpoint(checkpoint, {
      selectionsMarkerLayer: this.selectionsMarkerLayer
    });
  }

  /*
  Section: TextEditor Coordinates
  */

  // Essential: Convert a position in buffer-coordinates to screen-coordinates.
  //
  // The position is clipped via {::clipBufferPosition} prior to the conversion.
  // The position is also clipped via {::clipScreenPosition} following the
  // conversion, which only makes a difference when `options` are supplied.
  //
  // * `bufferPosition` A {Point} or {Array} of [row, column].
  // * `options` (optional) An options hash for {::clipScreenPosition}.
  //
  // Returns a {Point}.
  screenPositionForBufferPosition(bufferPosition, options) {
    if (options && options.clip) {
      Grim.deprecate(
        'The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.'
      );
      if (options.clipDirection) options.clipDirection = options.clip;
    }
    if (options && options.wrapAtSoftNewlines != null) {
      Grim.deprecate(
        "The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead."
      );
      if (options.clipDirection)
        options.clipDirection = options.wrapAtSoftNewlines
          ? 'forward'
          : 'backward';
    }
    if (options && options.wrapBeyondNewlines != null) {
      Grim.deprecate(
        "The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead."
      );
      if (options.clipDirection)
        options.clipDirection = options.wrapBeyondNewlines
          ? 'forward'
          : 'backward';
    }

    return this.displayLayer.translateBufferPosition(bufferPosition, options);
  }

  // Essential: Convert a position in screen-coordinates to buffer-coordinates.
  //
  // The position is clipped via {::clipScreenPosition} prior to the conversion.
  //
  // * `bufferPosition` A {Point} or {Array} of [row, column].
  // * `options` (optional) An options hash for {::clipScreenPosition}.
  //
  // Returns a {Point}.
  bufferPositionForScreenPosition(screenPosition, options) {
    if (options && options.clip) {
      Grim.deprecate(
        'The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.'
      );
      if (options.clipDirection) options.clipDirection = options.clip;
    }
    if (options && options.wrapAtSoftNewlines != null) {
      Grim.deprecate(
        "The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead."
      );
      if (options.clipDirection)
        options.clipDirection = options.wrapAtSoftNewlines
          ? 'forward'
          : 'backward';
    }
    if (options && options.wrapBeyondNewlines != null) {
      Grim.deprecate(
        "The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead."
      );
      if (options.clipDirection)
        options.clipDirection = options.wrapBeyondNewlines
          ? 'forward'
          : 'backward';
    }

    return this.displayLayer.translateScreenPosition(screenPosition, options);
  }

  // Essential: Convert a range in buffer-coordinates to screen-coordinates.
  //
  // * `bufferRange` {Range} in buffer coordinates to translate into screen coordinates.
  //
  // Returns a {Range}.
  screenRangeForBufferRange(bufferRange, options) {
    bufferRange = Range.fromObject(bufferRange);
    const start = this.screenPositionForBufferPosition(
      bufferRange.start,
      options
    );
    const end = this.screenPositionForBufferPosition(bufferRange.end, options);
    return new Range(start, end);
  }

  // Essential: Convert a range in screen-coordinates to buffer-coordinates.
  //
  // * `screenRange` {Range} in screen coordinates to translate into buffer coordinates.
  //
  // Returns a {Range}.
  bufferRangeForScreenRange(screenRange) {
    screenRange = Range.fromObject(screenRange);
    const start = this.bufferPositionForScreenPosition(screenRange.start);
    const end = this.bufferPositionForScreenPosition(screenRange.end);
    return new Range(start, end);
  }

  // Extended: Clip the given {Point} to a valid position in the buffer.
  //
  // If the given {Point} describes a position that is actually reachable by the
  // cursor based on the current contents of the buffer, it is returned
  // unchanged. If the {Point} does not describe a valid position, the closest
  // valid position is returned instead.
  //
  // ## Examples
  //
  // ```js
  // editor.clipBufferPosition([-1, -1]) // -> `[0, 0]`
  //
  // // When the line at buffer row 2 is 10 characters long
  // editor.clipBufferPosition([2, Infinity]) // -> `[2, 10]`
  // ```
  //
  // * `bufferPosition` The {Point} representing the position to clip.
  //
  // Returns a {Point}.
  clipBufferPosition(bufferPosition) {
    return this.buffer.clipPosition(bufferPosition);
  }

  // Extended: Clip the start and end of the given range to valid positions in the
  // buffer. See {::clipBufferPosition} for more information.
  //
  // * `range` The {Range} to clip.
  //
  // Returns a {Range}.
  clipBufferRange(range) {
    return this.buffer.clipRange(range);
  }

  // Extended: Clip the given {Point} to a valid position on screen.
  //
  // If the given {Point} describes a position that is actually reachable by the
  // cursor based on the current contents of the screen, it is returned
  // unchanged. If the {Point} does not describe a valid position, the closest
  // valid position is returned instead.
  //
  // ## Examples
  //
  // ```js
  // editor.clipScreenPosition([-1, -1]) // -> `[0, 0]`
  //
  // // When the line at screen row 2 is 10 characters long
  // editor.clipScreenPosition([2, Infinity]) // -> `[2, 10]`
  // ```
  //
  // * `screenPosition` The {Point} representing the position to clip.
  // * `options` (optional) {Object}
  //   * `clipDirection` {String} If `'backward'`, returns the first valid
  //     position preceding an invalid position. If `'forward'`, returns the
  //     first valid position following an invalid position. If `'closest'`,
  //     returns the first valid position closest to an invalid position.
  //     Defaults to `'closest'`.
  //
  // Returns a {Point}.
  clipScreenPosition(screenPosition, options) {
    if (options && options.clip) {
      Grim.deprecate(
        'The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.'
      );
      if (options.clipDirection) options.clipDirection = options.clip;
    }
    if (options && options.wrapAtSoftNewlines != null) {
      Grim.deprecate(
        "The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead."
      );
      if (options.clipDirection)
        options.clipDirection = options.wrapAtSoftNewlines
          ? 'forward'
          : 'backward';
    }
    if (options && options.wrapBeyondNewlines != null) {
      Grim.deprecate(
        "The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead."
      );
      if (options.clipDirection)
        options.clipDirection = options.wrapBeyondNewlines
          ? 'forward'
          : 'backward';
    }

    return this.displayLayer.clipScreenPosition(screenPosition, options);
  }

  // Extended: Clip the start and end of the given range to valid positions on screen.
  // See {::clipScreenPosition} for more information.
  //
  // * `range` The {Range} to clip.
  // * `options` (optional) See {::clipScreenPosition} `options`.
  //
  // Returns a {Range}.
  clipScreenRange(screenRange, options) {
    screenRange = Range.fromObject(screenRange);
    const start = this.displayLayer.clipScreenPosition(
      screenRange.start,
      options
    );
    const end = this.displayLayer.clipScreenPosition(screenRange.end, options);
    return Range(start, end);
  }

  /*
  Section: Decorations
  */

  // Essential: Add a decoration that tracks a {DisplayMarker}. When the
  // marker moves, is invalidated, or is destroyed, the decoration will be
  // updated to reflect the marker's state.
  //
  // The following are the supported decorations types:
  //
  // * __line__: Adds the given CSS `class` to the lines overlapping the rows
  //     spanned by the marker.
  // * __line-number__: Adds the given CSS `class` to the line numbers overlapping
  //     the rows spanned by the marker
  // * __text__: Injects spans into all text overlapping the marked range, then adds
  //     the given `class` or `style` to these spans. Use this to manipulate the foreground
  //     color or styling of text in a range.
  // * __highlight__: Creates an absolutely-positioned `.highlight` div to the editor
  //     containing nested divs that cover the marked region. For example, when the user
  //     selects text, the selection is implemented with a highlight decoration. The structure
  //     of this highlight will be:
  //     ```html
  //     <div class="highlight <your-class>">
  //       <!-- Will be one region for each row in the range. Spans 2 lines? There will be 2 regions. -->
  //       <div class="region"></div>
  //     </div>
  //     ```
  // * __overlay__: Positions the view associated with the given item at the head
  //     or tail of the given `DisplayMarker`, depending on the `position` property.
  // * __gutter__: Tracks a {DisplayMarker} in a {Gutter}. Gutter decorations are created
  //     by calling {Gutter::decorateMarker} on the desired `Gutter` instance.
  // * __block__: Positions the view associated with the given item before or
  //     after the row of the given {DisplayMarker}, depending on the `position` property.
  //     Block decorations at the same screen row are ordered by their `order` property.
  // * __cursor__: Render a cursor at the head of the {DisplayMarker}. If multiple cursor decorations
  //     are created for the same marker, their class strings and style objects are combined
  //     into a single cursor. This decoration type may be used to style existing cursors
  //     by passing in their markers or to render artificial cursors that don't actually
  //     exist in the model by passing a marker that isn't associated with a real cursor.
  //
  // ## Arguments
  //
  // * `marker` A {DisplayMarker} you want this decoration to follow.
  // * `decorationParams` An {Object} representing the decoration e.g.
  //   `{type: 'line-number', class: 'linter-error'}`
  //   * `type` Determines the behavior and appearance of this {Decoration}. Supported decoration types
  //     and their uses are listed above.
  //   * `class` This CSS class will be applied to the decorated line number,
  //     line, text spans, highlight regions, cursors, or overlay.
  //   * `style` An {Object} containing CSS style properties to apply to the
  //     relevant DOM node. Currently this only works with a `type` of `cursor`
  //     or `text`.
  //   * `item` (optional) An {HTMLElement} or a model {Object} with a
  //     corresponding view registered. Only applicable to the `gutter`,
  //     `overlay` and `block` decoration types.
  //   * `onlyHead` (optional) If `true`, the decoration will only be applied to
  //     the head of the `DisplayMarker`. Only applicable to the `line` and
  //     `line-number` decoration types.
  //   * `onlyEmpty` (optional) If `true`, the decoration will only be applied if
  //     the associated `DisplayMarker` is empty. Only applicable to the `gutter`,
  //     `line`, and `line-number` decoration types.
  //   * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied
  //     if the associated `DisplayMarker` is non-empty. Only applicable to the
  //     `gutter`, `line`, and `line-number` decoration types.
  //   * `omitEmptyLastRow` (optional) If `false`, the decoration will be applied
  //     to the last row of a non-empty range, even if it ends at column 0.
  //     Defaults to `true`. Only applicable to the `gutter`, `line`, and
  //     `line-number` decoration types.
  //   * `position` (optional) Only applicable to decorations of type `overlay` and `block`.
  //     Controls where the view is positioned relative to the `TextEditorMarker`.
  //     Values can be `'head'` (the default) or `'tail'` for overlay decorations, and
  //     `'before'` (the default) or `'after'` for block decorations.
  //   * `order` (optional) Only applicable to decorations of type `block`. Controls
  //      where the view is positioned relative to other block decorations at the
  //      same screen row. If unspecified, block decorations render oldest to newest.
  //   * `avoidOverflow` (optional) Only applicable to decorations of type
  //      `overlay`. Determines whether the decoration adjusts its horizontal or
  //      vertical position to remain fully visible when it would otherwise
  //      overflow the editor. Defaults to `true`.
  //
  // Returns the created {Decoration} object.
  decorateMarker(marker, decorationParams) {
    return this.decorationManager.decorateMarker(marker, decorationParams);
  }

  // Essential: Add a decoration to every marker in the given marker layer. Can
  // be used to decorate a large number of markers without having to create and
  // manage many individual decorations.
  //
  // * `markerLayer` A {DisplayMarkerLayer} or {MarkerLayer} to decorate.
  // * `decorationParams` The same parameters that are passed to
  //   {TextEditor::decorateMarker}, except the `type` cannot be `overlay` or `gutter`.
  //
  // Returns a {LayerDecoration}.
  decorateMarkerLayer(markerLayer, decorationParams) {
    return this.decorationManager.decorateMarkerLayer(
      markerLayer,
      decorationParams
    );
  }

  // Deprecated: Get all the decorations within a screen row range on the default
  // layer.
  //
  // * `startScreenRow` the {Number} beginning screen row
  // * `endScreenRow` the {Number} end screen row (inclusive)
  //
  // Returns an {Object} of decorations in the form
  //  `{1: [{id: 10, type: 'line-number', class: 'someclass'}], 2: ...}`
  //   where the keys are {DisplayMarker} IDs, and the values are an array of decoration
  //   params objects attached to the marker.
  // Returns an empty object when no decorations are found
  decorationsForScreenRowRange(startScreenRow, endScreenRow) {
    return this.decorationManager.decorationsForScreenRowRange(
      startScreenRow,
      endScreenRow
    );
  }

  decorationsStateForScreenRowRange(startScreenRow, endScreenRow) {
    return this.decorationManager.decorationsStateForScreenRowRange(
      startScreenRow,
      endScreenRow
    );
  }

  // Extended: Get all decorations.
  //
  // * `propertyFilter` (optional) An {Object} containing key value pairs that
  //   the returned decorations' properties must match.
  //
  // Returns an {Array} of {Decoration}s.
  getDecorations(propertyFilter) {
    return this.decorationManager.getDecorations(propertyFilter);
  }

  // Extended: Get all decorations of type 'line'.
  //
  // * `propertyFilter` (optional) An {Object} containing key value pairs that
  //   the returned decorations' properties must match.
  //
  // Returns an {Array} of {Decoration}s.
  getLineDecorations(propertyFilter) {
    return this.decorationManager.getLineDecorations(propertyFilter);
  }

  // Extended: Get all decorations of type 'line-number'.
  //
  // * `propertyFilter` (optional) An {Object} containing key value pairs that
  //   the returned decorations' properties must match.
  //
  // Returns an {Array} of {Decoration}s.
  getLineNumberDecorations(propertyFilter) {
    return this.decorationManager.getLineNumberDecorations(propertyFilter);
  }

  // Extended: Get all decorations of type 'highlight'.
  //
  // * `propertyFilter` (optional) An {Object} containing key value pairs that
  //   the returned decorations' properties must match.
  //
  // Returns an {Array} of {Decoration}s.
  getHighlightDecorations(propertyFilter) {
    return this.decorationManager.getHighlightDecorations(propertyFilter);
  }

  // Extended: Get all decorations of type 'overlay'.
  //
  // * `propertyFilter` (optional) An {Object} containing key value pairs that
  //   the returned decorations' properties must match.
  //
  // Returns an {Array} of {Decoration}s.
  getOverlayDecorations(propertyFilter) {
    return this.decorationManager.getOverlayDecorations(propertyFilter);
  }

  /*
  Section: Markers
  */

  // Essential: Create a marker on the default marker layer with the given range
  // in buffer coordinates. This marker will maintain its logical location as the
  // buffer is changed, so if you mark a particular word, the marker will remain
  // over that word even if the word's location in the buffer changes.
  //
  // * `range` A {Range} or range-compatible {Array}
  // * `properties` A hash of key-value pairs to associate with the marker. There
  //   are also reserved property names that have marker-specific meaning.
  //   * `maintainHistory` (optional) {Boolean} Whether to store this marker's
  //     range before and after each change in the undo history. This allows the
  //     marker's position to be restored more accurately for certain undo/redo
  //     operations, but uses more time and memory. (default: false)
  //   * `reversed` (optional) {Boolean} Creates the marker in a reversed
  //     orientation. (default: false)
  //   * `invalidate` (optional) {String} Determines the rules by which changes
  //     to the buffer *invalidate* the marker. (default: 'overlap') It can be
  //     any of the following strategies, in order of fragility:
  //     * __never__: The marker is never marked as invalid. This is a good choice for
  //       markers representing selections in an editor.
  //     * __surround__: The marker is invalidated by changes that completely surround it.
  //     * __overlap__: The marker is invalidated by changes that surround the
  //       start or end of the marker. This is the default.
  //     * __inside__: The marker is invalidated by changes that extend into the
  //       inside of the marker. Changes that end at the marker's start or
  //       start at the marker's end do not invalidate the marker.
  //     * __touch__: The marker is invalidated by a change that touches the marked
  //       region in any way, including changes that end at the marker's
  //       start or start at the marker's end. This is the most fragile strategy.
  //
  // Returns a {DisplayMarker}.
  markBufferRange(bufferRange, options) {
    return this.defaultMarkerLayer.markBufferRange(bufferRange, options);
  }

  // Essential: Create a marker on the default marker layer with the given range
  // in screen coordinates. This marker will maintain its logical location as the
  // buffer is changed, so if you mark a particular word, the marker will remain
  // over that word even if the word's location in the buffer changes.
  //
  // * `range` A {Range} or range-compatible {Array}
  // * `properties` A hash of key-value pairs to associate with the marker. There
  //   are also reserved property names that have marker-specific meaning.
  //   * `maintainHistory` (optional) {Boolean} Whether to store this marker's
  //     range before and after each change in the undo history. This allows the
  //     marker's position to be restored more accurately for certain undo/redo
  //     operations, but uses more time and memory. (default: false)
  //   * `reversed` (optional) {Boolean} Creates the marker in a reversed
  //     orientation. (default: false)
  //   * `invalidate` (optional) {String} Determines the rules by which changes
  //     to the buffer *invalidate* the marker. (default: 'overlap') It can be
  //     any of the following strategies, in order of fragility:
  //     * __never__: The marker is never marked as invalid. This is a good choice for
  //       markers representing selections in an editor.
  //     * __surround__: The marker is invalidated by changes that completely surround it.
  //     * __overlap__: The marker is invalidated by changes that surround the
  //       start or end of the marker. This is the default.
  //     * __inside__: The marker is invalidated by changes that extend into the
  //       inside of the marker. Changes that end at the marker's start or
  //       start at the marker's end do not invalidate the marker.
  //     * __touch__: The marker is invalidated by a change that touches the marked
  //       region in any way, including changes that end at the marker's
  //       start or start at the marker's end. This is the most fragile strategy.
  //
  // Returns a {DisplayMarker}.
  markScreenRange(screenRange, options) {
    return this.defaultMarkerLayer.markScreenRange(screenRange, options);
  }

  // Essential: Create a marker on the default marker layer with the given buffer
  // position and no tail. To group multiple markers together in their own
  // private layer, see {::addMarkerLayer}.
  //
  // * `bufferPosition` A {Point} or point-compatible {Array}
  // * `options` (optional) An {Object} with the following keys:
  //   * `invalidate` (optional) {String} Determines the rules by which changes
  //     to the buffer *invalidate* the marker. (default: 'overlap') It can be
  //     any of the following strategies, in order of fragility:
  //     * __never__: The marker is never marked as invalid. This is a good choice for
  //       markers representing selections in an editor.
  //     * __surround__: The marker is invalidated by changes that completely surround it.
  //     * __overlap__: The marker is invalidated by changes that surround the
  //       start or end of the marker. This is the default.
  //     * __inside__: The marker is invalidated by changes that extend into the
  //       inside of the marker. Changes that end at the marker's start or
  //       start at the marker's end do not invalidate the marker.
  //     * __touch__: The marker is invalidated by a change that touches the marked
  //       region in any way, including changes that end at the marker's
  //       start or start at the marker's end. This is the most fragile strategy.
  //
  // Returns a {DisplayMarker}.
  markBufferPosition(bufferPosition, options) {
    return this.defaultMarkerLayer.markBufferPosition(bufferPosition, options);
  }

  // Essential: Create a marker on the default marker layer with the given screen
  // position and no tail. To group multiple markers together in their own
  // private layer, see {::addMarkerLayer}.
  //
  // * `screenPosition` A {Point} or point-compatible {Array}
  // * `options` (optional) An {Object} with the following keys:
  //   * `invalidate` (optional) {String} Determines the rules by which changes
  //     to the buffer *invalidate* the marker. (default: 'overlap') It can be
  //     any of the following strategies, in order of fragility:
  //     * __never__: The marker is never marked as invalid. This is a good choice for
  //       markers representing selections in an editor.
  //     * __surround__: The marker is invalidated by changes that completely surround it.
  //     * __overlap__: The marker is invalidated by changes that surround the
  //       start or end of the marker. This is the default.
  //     * __inside__: The marker is invalidated by changes that extend into the
  //       inside of the marker. Changes that end at the marker's start or
  //       start at the marker's end do not invalidate the marker.
  //     * __touch__: The marker is invalidated by a change that touches the marked
  //       region in any way, including changes that end at the marker's
  //       start or start at the marker's end. This is the most fragile strategy.
  //   * `clipDirection` {String} If `'backward'`, returns the first valid
  //     position preceding an invalid position. If `'forward'`, returns the
  //     first valid position following an invalid position. If `'closest'`,
  //     returns the first valid position closest to an invalid position.
  //     Defaults to `'closest'`.
  //
  // Returns a {DisplayMarker}.
  markScreenPosition(screenPosition, options) {
    return this.defaultMarkerLayer.markScreenPosition(screenPosition, options);
  }

  // Essential: Find all {DisplayMarker}s on the default marker layer that
  // match the given properties.
  //
  // This method finds markers based on the given properties. Markers can be
  // associated with custom properties that will be compared with basic equality.
  // In addition, there are several special properties that will be compared
  // with the range of the markers rather than their properties.
  //
  // * `properties` An {Object} containing properties that each returned marker
  //   must satisfy. Markers can be associated with custom properties, which are
  //   compared with basic equality. In addition, several reserved properties
  //   can be used to filter markers based on their current range:
  //   * `startBufferRow` Only include markers starting at this row in buffer
  //       coordinates.
  //   * `endBufferRow` Only include markers ending at this row in buffer
  //       coordinates.
  //   * `containsBufferRange` Only include markers containing this {Range} or
  //       in range-compatible {Array} in buffer coordinates.
  //   * `containsBufferPosition` Only include markers containing this {Point}
  //       or {Array} of `[row, column]` in buffer coordinates.
  //
  // Returns an {Array} of {DisplayMarker}s
  findMarkers(params) {
    return this.defaultMarkerLayer.findMarkers(params);
  }

  // Extended: Get the {DisplayMarker} on the default layer for the given
  // marker id.
  //
  // * `id` {Number} id of the marker
  getMarker(id) {
    return this.defaultMarkerLayer.getMarker(id);
  }

  // Extended: Get all {DisplayMarker}s on the default marker layer. Consider
  // using {::findMarkers}
  getMarkers() {
    return this.defaultMarkerLayer.getMarkers();
  }

  // Extended: Get the number of markers in the default marker layer.
  //
  // Returns a {Number}.
  getMarkerCount() {
    return this.defaultMarkerLayer.getMarkerCount();
  }

  destroyMarker(id) {
    const marker = this.getMarker(id);
    if (marker) marker.destroy();
  }

  // Essential: Create a marker layer to group related markers.
  //
  // * `options` An {Object} containing the following keys:
  //   * `maintainHistory` A {Boolean} indicating whether marker state should be
  //     restored on undo/redo. Defaults to `false`.
  //   * `persistent` A {Boolean} indicating whether or not this marker layer
  //     should be serialized and deserialized along with the rest of the
  //     buffer. Defaults to `false`. If `true`, the marker layer's id will be
  //     maintained across the serialization boundary, allowing you to retrieve
  //     it via {::getMarkerLayer}.
  //
  // Returns a {DisplayMarkerLayer}.
  addMarkerLayer(options) {
    return this.displayLayer.addMarkerLayer(options);
  }

  // Essential: Get a {DisplayMarkerLayer} by id.
  //
  // * `id` The id of the marker layer to retrieve.
  //
  // Returns a {DisplayMarkerLayer} or `undefined` if no layer exists with the
  // given id.
  getMarkerLayer(id) {
    return this.displayLayer.getMarkerLayer(id);
  }

  // Essential: Get the default {DisplayMarkerLayer}.
  //
  // All marker APIs not tied to an explicit layer interact with this default
  // layer.
  //
  // Returns a {DisplayMarkerLayer}.
  getDefaultMarkerLayer() {
    return this.defaultMarkerLayer;
  }

  /*
  Section: Cursors
  */

  // Essential: Get the position of the most recently added cursor in buffer
  // coordinates.
  //
  // Returns a {Point}
  getCursorBufferPosition() {
    return this.getLastCursor().getBufferPosition();
  }

  // Essential: Get the position of all the cursor positions in buffer coordinates.
  //
  // Returns {Array} of {Point}s in the order they were added
  getCursorBufferPositions() {
    return this.getCursors().map(cursor => cursor.getBufferPosition());
  }

  // Essential: Move the cursor to the given position in buffer coordinates.
  //
  // If there are multiple cursors, they will be consolidated to a single cursor.
  //
  // * `position` A {Point} or {Array} of `[row, column]`
  // * `options` (optional) An {Object} containing the following keys:
  //   * `autoscroll` Determines whether the editor scrolls to the new cursor's
  //     position. Defaults to true.
  setCursorBufferPosition(position, options) {
    return this.moveCursors(cursor =>
      cursor.setBufferPosition(position, options)
    );
  }

  // Essential: Get a {Cursor} at given screen coordinates {Point}
  //
  // * `position` A {Point} or {Array} of `[row, column]`
  //
  // Returns the first matched {Cursor} or undefined
  getCursorAtScreenPosition(position) {
    const selection = this.getSelectionAtScreenPosition(position);
    if (selection && selection.getHeadScreenPosition().isEqual(position)) {
      return selection.cursor;
    }
  }

  // Essential: Get the position of the most recently added cursor in screen
  // coordinates.
  //
  // Returns a {Point}.
  getCursorScreenPosition() {
    return this.getLastCursor().getScreenPosition();
  }

  // Essential: Get the position of all the cursor positions in screen coordinates.
  //
  // Returns {Array} of {Point}s in the order the cursors were added
  getCursorScreenPositions() {
    return this.getCursors().map(cursor => cursor.getScreenPosition());
  }

  // Essential: Move the cursor to the given position in screen coordinates.
  //
  // If there are multiple cursors, they will be consolidated to a single cursor.
  //
  // * `position` A {Point} or {Array} of `[row, column]`
  // * `options` (optional) An {Object} combining options for {::clipScreenPosition} with:
  //   * `autoscroll` Determines whether the editor scrolls to the new cursor's
  //     position. Defaults to true.
  setCursorScreenPosition(position, options) {
    if (options && options.clip) {
      Grim.deprecate(
        'The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.'
      );
      if (options.clipDirection) options.clipDirection = options.clip;
    }
    if (options && options.wrapAtSoftNewlines != null) {
      Grim.deprecate(
        "The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead."
      );
      if (options.clipDirection)
        options.clipDirection = options.wrapAtSoftNewlines
          ? 'forward'
          : 'backward';
    }
    if (options && options.wrapBeyondNewlines != null) {
      Grim.deprecate(
        "The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead."
      );
      if (options.clipDirection)
        options.clipDirection = options.wrapBeyondNewlines
          ? 'forward'
          : 'backward';
    }

    return this.moveCursors(cursor =>
      cursor.setScreenPosition(position, options)
    );
  }

  // Essential: Add a cursor at the given position in buffer coordinates.
  //
  // * `bufferPosition` A {Point} or {Array} of `[row, column]`
  //
  // Returns a {Cursor}.
  addCursorAtBufferPosition(bufferPosition, options) {
    this.selectionsMarkerLayer.markBufferPosition(bufferPosition, {
      invalidate: 'never'
    });
    if (!options || options.autoscroll !== false)
      this.getLastSelection().cursor.autoscroll();
    return this.getLastSelection().cursor;
  }

  // Essential: Add a cursor at the position in screen coordinates.
  //
  // * `screenPosition` A {Point} or {Array} of `[row, column]`
  //
  // Returns a {Cursor}.
  addCursorAtScreenPosition(screenPosition, options) {
    this.selectionsMarkerLayer.markScreenPosition(screenPosition, {
      invalidate: 'never'
    });
    if (!options || options.autoscroll !== false)
      this.getLastSelection().cursor.autoscroll();
    return this.getLastSelection().cursor;
  }

  // Essential: Returns {Boolean} indicating whether or not there are multiple cursors.
  hasMultipleCursors() {
    return this.getCursors().length > 1;
  }

  // Essential: Move every cursor up one row in screen coordinates.
  //
  // * `lineCount` (optional) {Number} number of lines to move
  moveUp(lineCount) {
    return this.moveCursors(cursor =>
      cursor.moveUp(lineCount, { moveToEndOfSelection: true })
    );
  }

  // Essential: Move every cursor down one row in screen coordinates.
  //
  // * `lineCount` (optional) {Number} number of lines to move
  moveDown(lineCount) {
    return this.moveCursors(cursor =>
      cursor.moveDown(lineCount, { moveToEndOfSelection: true })
    );
  }

  // Essential: Move every cursor left one column.
  //
  // * `columnCount` (optional) {Number} number of columns to move (default: 1)
  moveLeft(columnCount) {
    return this.moveCursors(cursor =>
      cursor.moveLeft(columnCount, { moveToEndOfSelection: true })
    );
  }

  // Essential: Move every cursor right one column.
  //
  // * `columnCount` (optional) {Number} number of columns to move (default: 1)
  moveRight(columnCount) {
    return this.moveCursors(cursor =>
      cursor.moveRight(columnCount, { moveToEndOfSelection: true })
    );
  }

  // Essential: Move every cursor to the beginning of its line in buffer coordinates.
  moveToBeginningOfLine() {
    return this.moveCursors(cursor => cursor.moveToBeginningOfLine());
  }

  // Essential: Move every cursor to the beginning of its line in screen coordinates.
  moveToBeginningOfScreenLine() {
    return this.moveCursors(cursor => cursor.moveToBeginningOfScreenLine());
  }

  // Essential: Move every cursor to the first non-whitespace character of its line.
  moveToFirstCharacterOfLine() {
    return this.moveCursors(cursor => cursor.moveToFirstCharacterOfLine());
  }

  // Essential: Move every cursor to the end of its line in buffer coordinates.
  moveToEndOfLine() {
    return this.moveCursors(cursor => cursor.moveToEndOfLine());
  }

  // Essential: Move every cursor to the end of its line in screen coordinates.
  moveToEndOfScreenLine() {
    return this.moveCursors(cursor => cursor.moveToEndOfScreenLine());
  }

  // Essential: Move every cursor to the beginning of its surrounding word.
  moveToBeginningOfWord() {
    return this.moveCursors(cursor => cursor.moveToBeginningOfWord());
  }

  // Essential: Move every cursor to the end of its surrounding word.
  moveToEndOfWord() {
    return this.moveCursors(cursor => cursor.moveToEndOfWord());
  }

  // Cursor Extended

  // Extended: Move every cursor to the top of the buffer.
  //
  // If there are multiple cursors, they will be merged into a single cursor.
  moveToTop() {
    return this.moveCursors(cursor => cursor.moveToTop());
  }

  // Extended: Move every cursor to the bottom of the buffer.
  //
  // If there are multiple cursors, they will be merged into a single cursor.
  moveToBottom() {
    return this.moveCursors(cursor => cursor.moveToBottom());
  }

  // Extended: Move every cursor to the beginning of the next word.
  moveToBeginningOfNextWord() {
    return this.moveCursors(cursor => cursor.moveToBeginningOfNextWord());
  }

  // Extended: Move every cursor to the previous word boundary.
  moveToPreviousWordBoundary() {
    return this.moveCursors(cursor => cursor.moveToPreviousWordBoundary());
  }

  // Extended: Move every cursor to the next word boundary.
  moveToNextWordBoundary() {
    return this.moveCursors(cursor => cursor.moveToNextWordBoundary());
  }

  // Extended: Move every cursor to the previous subword boundary.
  moveToPreviousSubwordBoundary() {
    return this.moveCursors(cursor => cursor.moveToPreviousSubwordBoundary());
  }

  // Extended: Move every cursor to the next subword boundary.
  moveToNextSubwordBoundary() {
    return this.moveCursors(cursor => cursor.moveToNextSubwordBoundary());
  }

  // Extended: Move every cursor to the beginning of the next paragraph.
  moveToBeginningOfNextParagraph() {
    return this.moveCursors(cursor => cursor.moveToBeginningOfNextParagraph());
  }

  // Extended: Move every cursor to the beginning of the previous paragraph.
  moveToBeginningOfPreviousParagraph() {
    return this.moveCursors(cursor =>
      cursor.moveToBeginningOfPreviousParagraph()
    );
  }

  // Extended: Returns the most recently added {Cursor}
  getLastCursor() {
    this.createLastSelectionIfNeeded();
    return _.last(this.cursors);
  }

  // Extended: Returns the word surrounding the most recently added cursor.
  //
  // * `options` (optional) See {Cursor::getBeginningOfCurrentWordBufferPosition}.
  getWordUnderCursor(options) {
    return this.getTextInBufferRange(
      this.getLastCursor().getCurrentWordBufferRange(options)
    );
  }

  // Extended: Get an Array of all {Cursor}s.
  getCursors() {
    this.createLastSelectionIfNeeded();
    return this.cursors.slice();
  }

  // Extended: Get all {Cursor}s, ordered by their position in the buffer
  // instead of the order in which they were added.
  //
  // Returns an {Array} of {Selection}s.
  getCursorsOrderedByBufferPosition() {
    return this.getCursors().sort((a, b) => a.compare(b));
  }

  cursorsForScreenRowRange(startScreenRow, endScreenRow) {
    const cursors = [];
    for (let marker of this.selectionsMarkerLayer.findMarkers({
      intersectsScreenRowRange: [startScreenRow, endScreenRow]
    })) {
      const cursor = this.cursorsByMarkerId.get(marker.id);
      if (cursor) cursors.push(cursor);
    }
    return cursors;
  }

  // Add a cursor based on the given {DisplayMarker}.
  addCursor(marker) {
    const cursor = new Cursor({
      editor: this,
      marker,
      showCursorOnSelection: this.showCursorOnSelection
    });
    this.cursors.push(cursor);
    this.cursorsByMarkerId.set(marker.id, cursor);
    return cursor;
  }

  moveCursors(fn) {
    return this.transact(() => {
      this.getCursors().forEach(fn);
      return this.mergeCursors();
    });
  }

  cursorMoved(event) {
    return this.emitter.emit('did-change-cursor-position', event);
  }

  // Merge cursors that have the same screen position
  mergeCursors() {
    const positions = {};
    for (let cursor of this.getCursors()) {
      const position = cursor.getBufferPosition().toString();
      if (positions.hasOwnProperty(position)) {
        cursor.destroy();
      } else {
        positions[position] = true;
      }
    }
  }

  /*
  Section: Selections
  */

  // Essential: Get the selected text of the most recently added selection.
  //
  // Returns a {String}.
  getSelectedText() {
    return this.getLastSelection().getText();
  }

  // Essential: Get the {Range} of the most recently added selection in buffer
  // coordinates.
  //
  // Returns a {Range}.
  getSelectedBufferRange() {
    return this.getLastSelection().getBufferRange();
  }

  // Essential: Get the {Range}s of all selections in buffer coordinates.
  //
  // The ranges are sorted by when the selections were added. Most recent at the end.
  //
  // Returns an {Array} of {Range}s.
  getSelectedBufferRanges() {
    return this.getSelections().map(selection => selection.getBufferRange());
  }

  // Essential: Set the selected range in buffer coordinates. If there are multiple
  // selections, they are reduced to a single selection with the given range.
  //
  // * `bufferRange` A {Range} or range-compatible {Array}.
  // * `options` (optional) An options {Object}:
  //   * `reversed` A {Boolean} indicating whether to create the selection in a
  //     reversed orientation.
  //   * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the
  //     selection is set.
  setSelectedBufferRange(bufferRange, options) {
    return this.setSelectedBufferRanges([bufferRange], options);
  }

  // Essential: Set the selected ranges in buffer coordinates. If there are multiple
  // selections, they are replaced by new selections with the given ranges.
  //
  // * `bufferRanges` An {Array} of {Range}s or range-compatible {Array}s.
  // * `options` (optional) An options {Object}:
  //   * `reversed` A {Boolean} indicating whether to create the selection in a
  //     reversed orientation.
  //   * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the
  //     selection is set.
  setSelectedBufferRanges(bufferRanges, options = {}) {
    if (!bufferRanges.length)
      throw new Error('Passed an empty array to setSelectedBufferRanges');

    const selections = this.getSelections();
    for (let selection of selections.slice(bufferRanges.length)) {
      selection.destroy();
    }

    this.mergeIntersectingSelections(options, () => {
      for (let i = 0; i < bufferRanges.length; i++) {
        let bufferRange = bufferRanges[i];
        bufferRange = Range.fromObject(bufferRange);
        if (selections[i]) {
          selections[i].setBufferRange(bufferRange, options);
        } else {
          this.addSelectionForBufferRange(bufferRange, options);
        }
      }
    });
  }

  // Essential: Get the {Range} of the most recently added selection in screen
  // coordinates.
  //
  // Returns a {Range}.
  getSelectedScreenRange() {
    return this.getLastSelection().getScreenRange();
  }

  // Essential: Get the {Range}s of all selections in screen coordinates.
  //
  // The ranges are sorted by when the selections were added. Most recent at the end.
  //
  // Returns an {Array} of {Range}s.
  getSelectedScreenRanges() {
    return this.getSelections().map(selection => selection.getScreenRange());
  }

  // Essential: Set the selected range in screen coordinates. If there are multiple
  // selections, they are reduced to a single selection with the given range.
  //
  // * `screenRange` A {Range} or range-compatible {Array}.
  // * `options` (optional) An options {Object}:
  //   * `reversed` A {Boolean} indicating whether to create the selection in a
  //     reversed orientation.
  setSelectedScreenRange(screenRange, options) {
    return this.setSelectedBufferRange(
      this.bufferRangeForScreenRange(screenRange, options),
      options
    );
  }

  // Essential: Set the selected ranges in screen coordinates. If there are multiple
  // selections, they are replaced by new selections with the given ranges.
  //
  // * `screenRanges` An {Array} of {Range}s or range-compatible {Array}s.
  // * `options` (optional) An options {Object}:
  //   * `reversed` A {Boolean} indicating whether to create the selection in a
  //     reversed orientation.
  setSelectedScreenRanges(screenRanges, options = {}) {
    if (!screenRanges.length)
      throw new Error('Passed an empty array to setSelectedScreenRanges');

    const selections = this.getSelections();
    for (let selection of selections.slice(screenRanges.length)) {
      selection.destroy();
    }

    this.mergeIntersectingSelections(options, () => {
      for (let i = 0; i < screenRanges.length; i++) {
        let screenRange = screenRanges[i];
        screenRange = Range.fromObject(screenRange);
        if (selections[i]) {
          selections[i].setScreenRange(screenRange, options);
        } else {
          this.addSelectionForScreenRange(screenRange, options);
        }
      }
    });
  }

  // Essential: Add a selection for the given range in buffer coordinates.
  //
  // * `bufferRange` A {Range}
  // * `options` (optional) An options {Object}:
  //   * `reversed` A {Boolean} indicating whether to create the selection in a
  //     reversed orientation.
  //   * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the
  //     selection is set.
  //
  // Returns the added {Selection}.
  addSelectionForBufferRange(bufferRange, options = {}) {
    bufferRange = Range.fromObject(bufferRange);
    if (!options.preserveFolds) {
      this.displayLayer.destroyFoldsContainingBufferPositions(
        [bufferRange.start, bufferRange.end],
        true
      );
    }
    this.selectionsMarkerLayer.markBufferRange(bufferRange, {
      invalidate: 'never',
      reversed: options.reversed != null ? options.reversed : false
    });
    if (options.autoscroll !== false) this.getLastSelection().autoscroll();
    return this.getLastSelection();
  }

  // Essential: Add a selection for the given range in screen coordinates.
  //
  // * `screenRange` A {Range}
  // * `options` (optional) An options {Object}:
  //   * `reversed` A {Boolean} indicating whether to create the selection in a
  //     reversed orientation.
  //   * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the
  //     selection is set.
  // Returns the added {Selection}.
  addSelectionForScreenRange(screenRange, options = {}) {
    return this.addSelectionForBufferRange(
      this.bufferRangeForScreenRange(screenRange),
      options
    );
  }

  // Essential: Select from the current cursor position to the given position in
  // buffer coordinates.
  //
  // This method may merge selections that end up intersecting.
  //
  // * `position` An instance of {Point}, with a given `row` and `column`.
  selectToBufferPosition(position) {
    const lastSelection = this.getLastSelection();
    lastSelection.selectToBufferPosition(position);
    return this.mergeIntersectingSelections({
      reversed: lastSelection.isReversed()
    });
  }

  // Essential: Select from the current cursor position to the given position in
  // screen coordinates.
  //
  // This method may merge selections that end up intersecting.
  //
  // * `position` An instance of {Point}, with a given `row` and `column`.
  selectToScreenPosition(position, options) {
    const lastSelection = this.getLastSelection();
    lastSelection.selectToScreenPosition(position, options);
    if (!options || !options.suppressSelectionMerge) {
      return this.mergeIntersectingSelections({
        reversed: lastSelection.isReversed()
      });
    }
  }

  // Essential: Move the cursor of each selection one character upward while
  // preserving the selection's tail position.
  //
  // * `rowCount` (optional) {Number} number of rows to select (default: 1)
  //
  // This method may merge selections that end up intersecting.
  selectUp(rowCount) {
    return this.expandSelectionsBackward(selection =>
      selection.selectUp(rowCount)
    );
  }

  // Essential: Move the cursor of each selection one character downward while
  // preserving the selection's tail position.
  //
  // * `rowCount` (optional) {Number} number of rows to select (default: 1)
  //
  // This method may merge selections that end up intersecting.
  selectDown(rowCount) {
    return this.expandSelectionsForward(selection =>
      selection.selectDown(rowCount)
    );
  }

  // Essential: Move the cursor of each selection one character leftward while
  // preserving the selection's tail position.
  //
  // * `columnCount` (optional) {Number} number of columns to select (default: 1)
  //
  // This method may merge selections that end up intersecting.
  selectLeft(columnCount) {
    return this.expandSelectionsBackward(selection =>
      selection.selectLeft(columnCount)
    );
  }

  // Essential: Move the cursor of each selection one character rightward while
  // preserving the selection's tail position.
  //
  // * `columnCount` (optional) {Number} number of columns to select (default: 1)
  //
  // This method may merge selections that end up intersecting.
  selectRight(columnCount) {
    return this.expandSelectionsForward(selection =>
      selection.selectRight(columnCount)
    );
  }

  // Essential: Select from the top of the buffer to the end of the last selection
  // in the buffer.
  //
  // This method merges multiple selections into a single selection.
  selectToTop() {
    return this.expandSelectionsBackward(selection => selection.selectToTop());
  }

  // Essential: Selects from the top of the first selection in the buffer to the end
  // of the buffer.
  //
  // This method merges multiple selections into a single selection.
  selectToBottom() {
    return this.expandSelectionsForward(selection =>
      selection.selectToBottom()
    );
  }

  // Essential: Select all text in the buffer.
  //
  // This method merges multiple selections into a single selection.
  selectAll() {
    return this.expandSelectionsForward(selection => selection.selectAll());
  }

  // Essential: Move the cursor of each selection to the beginning of its line
  // while preserving the selection's tail position.
  //
  // This method may merge selections that end up intersecting.
  selectToBeginningOfLine() {
    return this.expandSelectionsBackward(selection =>
      selection.selectToBeginningOfLine()
    );
  }

  // Essential: Move the cursor of each selection to the first non-whitespace
  // character of its line while preserving the selection's tail position. If the
  // cursor is already on the first character of the line, move it to the
  // beginning of the line.
  //
  // This method may merge selections that end up intersecting.
  selectToFirstCharacterOfLine() {
    return this.expandSelectionsBackward(selection =>
      selection.selectToFirstCharacterOfLine()
    );
  }

  // Essential: Move the cursor of each selection to the end of its line while
  // preserving the selection's tail position.
  //
  // This method may merge selections that end up intersecting.
  selectToEndOfLine() {
    return this.expandSelectionsForward(selection =>
      selection.selectToEndOfLine()
    );
  }

  // Essential: Expand selections to the beginning of their containing word.
  //
  // Operates on all selections. Moves the cursor to the beginning of the
  // containing word while preserving the selection's tail position.
  selectToBeginningOfWord() {
    return this.expandSelectionsBackward(selection =>
      selection.selectToBeginningOfWord()
    );
  }

  // Essential: Expand selections to the end of their containing word.
  //
  // Operates on all selections. Moves the cursor to the end of the containing
  // word while preserving the selection's tail position.
  selectToEndOfWord() {
    return this.expandSelectionsForward(selection =>
      selection.selectToEndOfWord()
    );
  }

  // Extended: For each selection, move its cursor to the preceding subword
  // boundary while maintaining the selection's tail position.
  //
  // This method may merge selections that end up intersecting.
  selectToPreviousSubwordBoundary() {
    return this.expandSelectionsBackward(selection =>
      selection.selectToPreviousSubwordBoundary()
    );
  }

  // Extended: For each selection, move its cursor to the next subword boundary
  // while maintaining the selection's tail position.
  //
  // This method may merge selections that end up intersecting.
  selectToNextSubwordBoundary() {
    return this.expandSelectionsForward(selection =>
      selection.selectToNextSubwordBoundary()
    );
  }

  // Essential: For each cursor, select the containing line.
  //
  // This method merges selections on successive lines.
  selectLinesContainingCursors() {
    return this.expandSelectionsForward(selection => selection.selectLine());
  }

  // Essential: Select the word surrounding each cursor.
  selectWordsContainingCursors() {
    return this.expandSelectionsForward(selection => selection.selectWord());
  }

  // Selection Extended

  // Extended: For each selection, move its cursor to the preceding word boundary
  // while maintaining the selection's tail position.
  //
  // This method may merge selections that end up intersecting.
  selectToPreviousWordBoundary() {
    return this.expandSelectionsBackward(selection =>
      selection.selectToPreviousWordBoundary()
    );
  }

  // Extended: For each selection, move its cursor to the next word boundary while
  // maintaining the selection's tail position.
  //
  // This method may merge selections that end up intersecting.
  selectToNextWordBoundary() {
    return this.expandSelectionsForward(selection =>
      selection.selectToNextWordBoundary()
    );
  }

  // Extended: Expand selections to the beginning of the next word.
  //
  // Operates on all selections. Moves the cursor to the beginning of the next
  // word while preserving the selection's tail position.
  selectToBeginningOfNextWord() {
    return this.expandSelectionsForward(selection =>
      selection.selectToBeginningOfNextWord()
    );
  }

  // Extended: Expand selections to the beginning of the next paragraph.
  //
  // Operates on all selections. Moves the cursor to the beginning of the next
  // paragraph while preserving the selection's tail position.
  selectToBeginningOfNextParagraph() {
    return this.expandSelectionsForward(selection =>
      selection.selectToBeginningOfNextParagraph()
    );
  }

  // Extended: Expand selections to the beginning of the next paragraph.
  //
  // Operates on all selections. Moves the cursor to the beginning of the next
  // paragraph while preserving the selection's tail position.
  selectToBeginningOfPreviousParagraph() {
    return this.expandSelectionsBackward(selection =>
      selection.selectToBeginningOfPreviousParagraph()
    );
  }

  // Extended: For each selection, select the syntax node that contains
  // that selection.
  selectLargerSyntaxNode() {
    const languageMode = this.buffer.getLanguageMode();
    if (!languageMode.getRangeForSyntaxNodeContainingRange) return;

    this.expandSelectionsForward(selection => {
      const currentRange = selection.getBufferRange();
      const newRange = languageMode.getRangeForSyntaxNodeContainingRange(
        currentRange
      );
      if (newRange) {
        if (!selection._rangeStack) selection._rangeStack = [];
        selection._rangeStack.push(currentRange);
        selection.setBufferRange(newRange);
      }
    });
  }

  // Extended: Undo the effect a preceding call to {::selectLargerSyntaxNode}.
  selectSmallerSyntaxNode() {
    this.expandSelectionsForward(selection => {
      if (selection._rangeStack) {
        const lastRange =
          selection._rangeStack[selection._rangeStack.length - 1];
        if (lastRange && selection.getBufferRange().containsRange(lastRange)) {
          selection._rangeStack.length--;
          selection.setBufferRange(lastRange);
        }
      }
    });
  }

  // Extended: Select the range of the given marker if it is valid.
  //
  // * `marker` A {DisplayMarker}
  //
  // Returns the selected {Range} or `undefined` if the marker is invalid.
  selectMarker(marker) {
    if (marker.isValid()) {
      const range = marker.getBufferRange();
      this.setSelectedBufferRange(range);
      return range;
    }
  }

  // Extended: Get the most recently added {Selection}.
  //
  // Returns a {Selection}.
  getLastSelection() {
    this.createLastSelectionIfNeeded();
    return _.last(this.selections);
  }

  getSelectionAtScreenPosition(position) {
    const markers = this.selectionsMarkerLayer.findMarkers({
      containsScreenPosition: position
    });
    if (markers.length > 0)
      return this.cursorsByMarkerId.get(markers[0].id).selection;
  }

  // Extended: Get current {Selection}s.
  //
  // Returns: An {Array} of {Selection}s.
  getSelections() {
    this.createLastSelectionIfNeeded();
    return this.selections.slice();
  }

  // Extended: Get all {Selection}s, ordered by their position in the buffer
  // instead of the order in which they were added.
  //
  // Returns an {Array} of {Selection}s.
  getSelectionsOrderedByBufferPosition() {
    return this.getSelections().sort((a, b) => a.compare(b));
  }

  // Extended: Determine if a given range in buffer coordinates intersects a
  // selection.
  //
  // * `bufferRange` A {Range} or range-compatible {Array}.
  //
  // Returns a {Boolean}.
  selectionIntersectsBufferRange(bufferRange) {
    return this.getSelections().some(selection =>
      selection.intersectsBufferRange(bufferRange)
    );
  }

  // Selections Private

  // Add a similarly-shaped selection to the next eligible line below
  // each selection.
  //
  // Operates on all selections. If the selection is empty, adds an empty
  // selection to the next following non-empty line as close to the current
  // selection's column as possible. If the selection is non-empty, adds a
  // selection to the next line that is long enough for a non-empty selection
  // starting at the same column as the current selection to be added to it.
  addSelectionBelow() {
    return this.expandSelectionsForward(selection =>
      selection.addSelectionBelow()
    );
  }

  // Add a similarly-shaped selection to the next eligible line above
  // each selection.
  //
  // Operates on all selections. If the selection is empty, adds an empty
  // selection to the next preceding non-empty line as close to the current
  // selection's column as possible. If the selection is non-empty, adds a
  // selection to the next line that is long enough for a non-empty selection
  // starting at the same column as the current selection to be added to it.
  addSelectionAbove() {
    return this.expandSelectionsBackward(selection =>
      selection.addSelectionAbove()
    );
  }

  // Calls the given function with each selection, then merges selections
  expandSelectionsForward(fn) {
    this.mergeIntersectingSelections(() => this.getSelections().forEach(fn));
  }

  // Calls the given function with each selection, then merges selections in the
  // reversed orientation
  expandSelectionsBackward(fn) {
    this.mergeIntersectingSelections({ reversed: true }, () =>
      this.getSelections().forEach(fn)
    );
  }

  finalizeSelections() {
    for (let selection of this.getSelections()) {
      selection.finalize();
    }
  }

  selectionsForScreenRows(startRow, endRow) {
    return this.getSelections().filter(selection =>
      selection.intersectsScreenRowRange(startRow, endRow)
    );
  }

  // Merges intersecting selections. If passed a function, it executes
  // the function with merging suppressed, then merges intersecting selections
  // afterward.
  mergeIntersectingSelections(...args) {
    return this.mergeSelections(
      ...args,
      (previousSelection, currentSelection) => {
        const exclusive =
          !currentSelection.isEmpty() && !previousSelection.isEmpty();
        return previousSelection.intersectsWith(currentSelection, exclusive);
      }
    );
  }

  mergeSelectionsOnSameRows(...args) {
    return this.mergeSelections(
      ...args,
      (previousSelection, currentSelection) => {
        const screenRange = currentSelection.getScreenRange();
        return previousSelection.intersectsScreenRowRange(
          screenRange.start.row,
          screenRange.end.row
        );
      }
    );
  }

  avoidMergingSelections(...args) {
    return this.mergeSelections(...args, () => false);
  }

  mergeSelections(...args) {
    const mergePredicate = args.pop();
    let fn = args.pop();
    let options = args.pop();
    if (typeof fn !== 'function') {
      options = fn;
      fn = () => {};
    }

    if (this.suppressSelectionMerging) return fn();

    this.suppressSelectionMerging = true;
    const result = fn();
    this.suppressSelectionMerging = false;

    const selections = this.getSelectionsOrderedByBufferPosition();
    let lastSelection = selections.shift();
    for (const selection of selections) {
      if (mergePredicate(lastSelection, selection)) {
        lastSelection.merge(selection, options);
      } else {
        lastSelection = selection;
      }
    }

    return result;
  }

  // Add a {Selection} based on the given {DisplayMarker}.
  //
  // * `marker` The {DisplayMarker} to highlight
  // * `options` (optional) An {Object} that pertains to the {Selection} constructor.
  //
  // Returns the new {Selection}.
  addSelection(marker, options = {}) {
    const cursor = this.addCursor(marker);
    let selection = new Selection(
      Object.assign({ editor: this, marker, cursor }, options)
    );
    this.selections.push(selection);
    const selectionBufferRange = selection.getBufferRange();
    this.mergeIntersectingSelections({ preserveFolds: options.preserveFolds });

    if (selection.destroyed) {
      for (selection of this.getSelections()) {
        if (selection.intersectsBufferRange(selectionBufferRange))
          return selection;
      }
    } else {
      this.emitter.emit('did-add-cursor', cursor);
      this.emitter.emit('did-add-selection', selection);
      return selection;
    }
  }

  // Remove the given selection.
  removeSelection(selection) {
    _.remove(this.cursors, selection.cursor);
    _.remove(this.selections, selection);
    this.cursorsByMarkerId.delete(selection.cursor.marker.id);
    this.emitter.emit('did-remove-cursor', selection.cursor);
    return this.emitter.emit('did-remove-selection', selection);
  }

  // Reduce one or more selections to a single empty selection based on the most
  // recently added cursor.
  clearSelections(options) {
    this.consolidateSelections();
    this.getLastSelection().clear(options);
  }

  // Reduce multiple selections to the least recently added selection.
  consolidateSelections() {
    const selections = this.getSelections();
    if (selections.length > 1) {
      for (let selection of selections.slice(1, selections.length)) {
        selection.destroy();
      }
      selections[0].autoscroll({ center: true });
      return true;
    } else {
      return false;
    }
  }

  // Called by the selection
  selectionRangeChanged(event) {
    if (this.component) this.component.didChangeSelectionRange();
    this.emitter.emit('did-change-selection-range', event);
  }

  createLastSelectionIfNeeded() {
    if (this.selections.length === 0) {
      this.addSelectionForBufferRange([[0, 0], [0, 0]], {
        autoscroll: false,
        preserveFolds: true
      });
    }
  }

  /*
  Section: Searching and Replacing
  */

  // Essential: Scan regular expression matches in the entire buffer, calling the
  // given iterator function on each match.
  //
  // `::scan` functions as the replace method as well via the `replace`
  //
  // If you're programmatically modifying the results, you may want to try
  // {::backwardsScanInBufferRange} to avoid tripping over your own changes.
  //
  // * `regex` A {RegExp} to search for.
  // * `options` (optional) {Object}
  //   * `leadingContextLineCount` {Number} default `0`; The number of lines
  //      before the matched line to include in the results object.
  //   * `trailingContextLineCount` {Number} default `0`; The number of lines
  //      after the matched line to include in the results object.
  // * `iterator` A {Function} that's called on each match
  //   * `object` {Object}
  //     * `match` The current regular expression match.
  //     * `matchText` A {String} with the text of the match.
  //     * `range` The {Range} of the match.
  //     * `stop` Call this {Function} to terminate the scan.
  //     * `replace` Call this {Function} with a {String} to replace the match.
  scan(regex, options = {}, iterator) {
    if (_.isFunction(options)) {
      iterator = options;
      options = {};
    }

    return this.buffer.scan(regex, options, iterator);
  }

  // Essential: Scan regular expression matches in a given range, calling the given
  // iterator function on each match.
  //
  // * `regex` A {RegExp} to search for.
  // * `range` A {Range} in which to search.
  // * `iterator` A {Function} that's called on each match with an {Object}
  //   containing the following keys:
  //   * `match` The current regular expression match.
  //   * `matchText` A {String} with the text of the match.
  //   * `range` The {Range} of the match.
  //   * `stop` Call this {Function} to terminate the scan.
  //   * `replace` Call this {Function} with a {String} to replace the match.
  scanInBufferRange(regex, range, iterator) {
    return this.buffer.scanInRange(regex, range, iterator);
  }

  // Essential: Scan regular expression matches in a given range in reverse order,
  // calling the given iterator function on each match.
  //
  // * `regex` A {RegExp} to search for.
  // * `range` A {Range} in which to search.
  // * `iterator` A {Function} that's called on each match with an {Object}
  //   containing the following keys:
  //   * `match` The current regular expression match.
  //   * `matchText` A {String} with the text of the match.
  //   * `range` The {Range} of the match.
  //   * `stop` Call this {Function} to terminate the scan.
  //   * `replace` Call this {Function} with a {String} to replace the match.
  backwardsScanInBufferRange(regex, range, iterator) {
    return this.buffer.backwardsScanInRange(regex, range, iterator);
  }

  /*
  Section: Tab Behavior
  */

  // Essential: Returns a {Boolean} indicating whether softTabs are enabled for this
  // editor.
  getSoftTabs() {
    return this.softTabs;
  }

  // Essential: Enable or disable soft tabs for this editor.
  //
  // * `softTabs` A {Boolean}
  setSoftTabs(softTabs) {
    this.softTabs = softTabs;
    this.updateSoftTabs(this.softTabs, true);
  }

  // Returns a {Boolean} indicating whether atomic soft tabs are enabled for this editor.
  hasAtomicSoftTabs() {
    return this.displayLayer.atomicSoftTabs;
  }

  // Essential: Toggle soft tabs for this editor
  toggleSoftTabs() {
    this.setSoftTabs(!this.getSoftTabs());
  }

  // Essential: Get the on-screen length of tab characters.
  //
  // Returns a {Number}.
  getTabLength() {
    return this.displayLayer.tabLength;
  }

  // Essential: Set the on-screen length of tab characters. Setting this to a
  // {Number} This will override the `editor.tabLength` setting.
  //
  // * `tabLength` {Number} length of a single tab. Setting to `null` will
  //   fallback to using the `editor.tabLength` config setting
  setTabLength(tabLength) {
    this.updateTabLength(tabLength, true);
  }

  // Returns an {Object} representing the current invisible character
  // substitutions for this editor, whose keys are names of invisible characters
  // and whose values are 1-character {Strings}s that are displayed in place of
  // those invisible characters
  getInvisibles() {
    if (!this.mini && this.showInvisibles && this.invisibles != null) {
      return this.invisibles;
    } else {
      return {};
    }
  }

  doesShowIndentGuide() {
    return this.showIndentGuide && !this.mini;
  }

  getSoftWrapHangingIndentLength() {
    return this.displayLayer.softWrapHangingIndent;
  }

  // Extended: Determine if the buffer uses hard or soft tabs.
  //
  // Returns `true` if the first non-comment line with leading whitespace starts
  // with a space character. Returns `false` if it starts with a hard tab (`\t`).
  //
  // Returns a {Boolean} or undefined if no non-comment lines had leading
  // whitespace.
  usesSoftTabs() {
    const languageMode = this.buffer.getLanguageMode();
    const hasIsRowCommented = languageMode.isRowCommented;
    for (
      let bufferRow = 0, end = Math.min(1000, this.buffer.getLastRow());
      bufferRow <= end;
      bufferRow++
    ) {
      if (hasIsRowCommented && languageMode.isRowCommented(bufferRow)) continue;
      const line = this.buffer.lineForRow(bufferRow);
      if (line[0] === ' ') return true;
      if (line[0] === '\t') return false;
    }
  }

  // Extended: Get the text representing a single level of indent.
  //
  // If soft tabs are enabled, the text is composed of N spaces, where N is the
  // tab length. Otherwise the text is a tab character (`\t`).
  //
  // Returns a {String}.
  getTabText() {
    return this.buildIndentString(1);
  }

  // If soft tabs are enabled, convert all hard tabs to soft tabs in the given
  // {Range}.
  normalizeTabsInBufferRange(bufferRange) {
    if (!this.getSoftTabs()) {
      return;
    }
    return this.scanInBufferRange(/\t/g, bufferRange, ({ replace }) =>
      replace(this.getTabText())
    );
  }

  /*
  Section: Soft Wrap Behavior
  */

  // Essential: Determine whether lines in this editor are soft-wrapped.
  //
  // Returns a {Boolean}.
  isSoftWrapped() {
    return this.softWrapped;
  }

  // Essential: Enable or disable soft wrapping for this editor.
  //
  // * `softWrapped` A {Boolean}
  //
  // Returns a {Boolean}.
  setSoftWrapped(softWrapped) {
    this.updateSoftWrapped(softWrapped, true);
    return this.isSoftWrapped();
  }

  getPreferredLineLength() {
    return this.preferredLineLength;
  }

  // Essential: Toggle soft wrapping for this editor
  //
  // Returns a {Boolean}.
  toggleSoftWrapped() {
    return this.setSoftWrapped(!this.isSoftWrapped());
  }

  // Essential: Gets the column at which column will soft wrap
  getSoftWrapColumn() {
    if (this.isSoftWrapped() && !this.mini) {
      if (this.softWrapAtPreferredLineLength) {
        return Math.min(this.getEditorWidthInChars(), this.preferredLineLength);
      } else {
        return this.getEditorWidthInChars();
      }
    } else {
      return this.maxScreenLineLength;
    }
  }

  /*
  Section: Indentation
  */

  // Essential: Get the indentation level of the given buffer row.
  //
  // Determines how deeply the given row is indented based on the soft tabs and
  // tab length settings of this editor. Note that if soft tabs are enabled and
  // the tab length is 2, a row with 4 leading spaces would have an indentation
  // level of 2.
  //
  // * `bufferRow` A {Number} indicating the buffer row.
  //
  // Returns a {Number}.
  indentationForBufferRow(bufferRow) {
    return this.indentLevelForLine(this.lineTextForBufferRow(bufferRow));
  }

  // Essential: Set the indentation level for the given buffer row.
  //
  // Inserts or removes hard tabs or spaces based on the soft tabs and tab length
  // settings of this editor in order to bring it to the given indentation level.
  // Note that if soft tabs are enabled and the tab length is 2, a row with 4
  // leading spaces would have an indentation level of 2.
  //
  // * `bufferRow` A {Number} indicating the buffer row.
  // * `newLevel` A {Number} indicating the new indentation level.
  // * `options` (optional) An {Object} with the following keys:
  //   * `preserveLeadingWhitespace` `true` to preserve any whitespace already at
  //      the beginning of the line (default: false).
  setIndentationForBufferRow(
    bufferRow,
    newLevel,
    { preserveLeadingWhitespace } = {}
  ) {
    let endColumn;
    if (preserveLeadingWhitespace) {
      endColumn = 0;
    } else {
      endColumn = this.lineTextForBufferRow(bufferRow).match(/^\s*/)[0].length;
    }
    const newIndentString = this.buildIndentString(newLevel);
    return this.buffer.setTextInRange(
      [[bufferRow, 0], [bufferRow, endColumn]],
      newIndentString
    );
  }

  // Extended: Indent rows intersecting selections by one level.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
  indentSelectedRows(options = {}) {
    if (!this.ensureWritable('indentSelectedRows', options)) return;
    return this.mutateSelectedText(selection =>
      selection.indentSelectedRows(options)
    );
  }

  // Extended: Outdent rows intersecting selections by one level.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
  outdentSelectedRows(options = {}) {
    if (!this.ensureWritable('outdentSelectedRows', options)) return;
    return this.mutateSelectedText(selection =>
      selection.outdentSelectedRows(options)
    );
  }

  // Extended: Get the indentation level of the given line of text.
  //
  // Determines how deeply the given line is indented based on the soft tabs and
  // tab length settings of this editor. Note that if soft tabs are enabled and
  // the tab length is 2, a row with 4 leading spaces would have an indentation
  // level of 2.
  //
  // * `line` A {String} representing a line of text.
  //
  // Returns a {Number}.
  indentLevelForLine(line) {
    const tabLength = this.getTabLength();
    let indentLength = 0;
    for (let i = 0, { length } = line; i < length; i++) {
      const char = line[i];
      if (char === '\t') {
        indentLength += tabLength - (indentLength % tabLength);
      } else if (char === ' ') {
        indentLength++;
      } else {
        break;
      }
    }
    return indentLength / tabLength;
  }

  // Extended: Indent rows intersecting selections based on the grammar's suggested
  // indent level.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
  autoIndentSelectedRows(options = {}) {
    if (!this.ensureWritable('autoIndentSelectedRows', options)) return;
    return this.mutateSelectedText(selection =>
      selection.autoIndentSelectedRows(options)
    );
  }

  // Indent all lines intersecting selections. See {Selection::indent} for more
  // information.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
  indent(options = {}) {
    if (!this.ensureWritable('indent', options)) return;
    if (options.autoIndent == null)
      options.autoIndent = this.shouldAutoIndent();
    this.mutateSelectedText(selection => selection.indent(options));
  }

  // Constructs the string used for indents.
  buildIndentString(level, column = 0) {
    if (this.getSoftTabs()) {
      const tabStopViolation = column % this.getTabLength();
      return _.multiplyString(
        ' ',
        Math.floor(level * this.getTabLength()) - tabStopViolation
      );
    } else {
      const excessWhitespace = _.multiplyString(
        ' ',
        Math.round((level - Math.floor(level)) * this.getTabLength())
      );
      return _.multiplyString('\t', Math.floor(level)) + excessWhitespace;
    }
  }

  /*
  Section: Grammars
  */

  // Essential: Get the current {Grammar} of this editor.
  getGrammar() {
    const languageMode = this.buffer.getLanguageMode();
    return (
      (languageMode.getGrammar && languageMode.getGrammar()) || NullGrammar
    );
  }

  // Deprecated: Set the current {Grammar} of this editor.
  //
  // Assigning a grammar will cause the editor to re-tokenize based on the new
  // grammar.
  //
  // * `grammar` {Grammar}
  setGrammar(grammar) {
    const buffer = this.getBuffer();
    buffer.setLanguageMode(
      atom.grammars.languageModeForGrammarAndBuffer(grammar, buffer)
    );
  }

  // Experimental: Get a notification when async tokenization is completed.
  onDidTokenize(callback) {
    return this.emitter.on('did-tokenize', callback);
  }

  /*
  Section: Managing Syntax Scopes
  */

  // Essential: Returns a {ScopeDescriptor} that includes this editor's language.
  // e.g. `['.source.ruby']`, or `['.source.coffee']`. You can use this with
  // {Config::get} to get language specific config values.
  getRootScopeDescriptor() {
    return this.buffer.getLanguageMode().rootScopeDescriptor;
  }

  // Essential: Get the syntactic {ScopeDescriptor} for the given position in buffer
  // coordinates. Useful with {Config::get}.
  //
  // For example, if called with a position inside the parameter list of an
  // anonymous CoffeeScript function, this method returns a {ScopeDescriptor} with
  // the following scopes array:
  // `["source.coffee", "meta.function.inline.coffee", "meta.parameters.coffee", "variable.parameter.function.coffee"]`
  //
  // * `bufferPosition` A {Point} or {Array} of `[row, column]`.
  //
  // Returns a {ScopeDescriptor}.
  scopeDescriptorForBufferPosition(bufferPosition) {
    const languageMode = this.buffer.getLanguageMode();
    return languageMode.scopeDescriptorForPosition
      ? languageMode.scopeDescriptorForPosition(bufferPosition)
      : new ScopeDescriptor({ scopes: ['text'] });
  }

  // Essential: Get the syntactic tree {ScopeDescriptor} for the given position in buffer
  // coordinates or the syntactic {ScopeDescriptor} for TextMate language mode
  //
  // For example, if called with a position inside the parameter list of a
  // JavaScript class function, this method returns a {ScopeDescriptor} with
  // the following syntax nodes array:
  // `["source.js", "program", "expression_statement", "assignment_expression", "class", "class_body", "method_definition", "formal_parameters", "identifier"]`
  // if tree-sitter is used
  // and the following scopes array:
  // `["source.js"]`
  // if textmate is used
  //
  // * `bufferPosition` A {Point} or {Array} of `[row, column]`.
  //
  // Returns a {ScopeDescriptor}.
  syntaxTreeScopeDescriptorForBufferPosition(bufferPosition) {
    const languageMode = this.buffer.getLanguageMode();
    return languageMode.syntaxTreeScopeDescriptorForPosition
      ? languageMode.syntaxTreeScopeDescriptorForPosition(bufferPosition)
      : this.scopeDescriptorForBufferPosition(bufferPosition);
  }

  // Extended: Get the range in buffer coordinates of all tokens surrounding the
  // cursor that match the given scope selector.
  //
  // For example, if you wanted to find the string surrounding the cursor, you
  // could call `editor.bufferRangeForScopeAtCursor(".string.quoted")`.
  //
  // * `scopeSelector` {String} selector. e.g. `'.source.ruby'`
  //
  // Returns a {Range}.
  bufferRangeForScopeAtCursor(scopeSelector) {
    return this.bufferRangeForScopeAtPosition(
      scopeSelector,
      this.getCursorBufferPosition()
    );
  }

  // Extended: Get the range in buffer coordinates of all tokens surrounding the
  // given position in buffer coordinates that match the given scope selector.
  //
  // For example, if you wanted to find the string surrounding the cursor, you
  // could call `editor.bufferRangeForScopeAtPosition(".string.quoted", this.getCursorBufferPosition())`.
  //
  // * `scopeSelector` {String} selector. e.g. `'.source.ruby'`
  // * `bufferPosition` A {Point} or {Array} of [row, column]
  //
  // Returns a {Range}.
  bufferRangeForScopeAtPosition(scopeSelector, bufferPosition) {
    return this.buffer
      .getLanguageMode()
      .bufferRangeForScopeAtPosition(scopeSelector, bufferPosition);
  }

  // Extended: Determine if the given row is entirely a comment
  isBufferRowCommented(bufferRow) {
    const match = this.lineTextForBufferRow(bufferRow).match(/\S/);
    if (match) {
      if (!this.commentScopeSelector)
        this.commentScopeSelector = new TextMateScopeSelector('comment.*');
      return this.commentScopeSelector.matches(
        this.scopeDescriptorForBufferPosition([bufferRow, match.index]).scopes
      );
    }
  }

  // Get the scope descriptor at the cursor.
  getCursorScope() {
    return this.getLastCursor().getScopeDescriptor();
  }

  // Get the syntax nodes at the cursor.
  getCursorSyntaxTreeScope() {
    return this.getLastCursor().getSyntaxTreeScopeDescriptor();
  }

  tokenForBufferPosition(bufferPosition) {
    return this.buffer.getLanguageMode().tokenForPosition(bufferPosition);
  }

  /*
  Section: Clipboard Operations
  */

  // Essential: For each selection, copy the selected text.
  copySelectedText() {
    let maintainClipboard = false;
    for (let selection of this.getSelectionsOrderedByBufferPosition()) {
      if (selection.isEmpty()) {
        const previousRange = selection.getBufferRange();
        selection.selectLine();
        selection.copy(maintainClipboard, true);
        selection.setBufferRange(previousRange);
      } else {
        selection.copy(maintainClipboard, false);
      }
      maintainClipboard = true;
    }
  }

  // Private: For each selection, only copy highlighted text.
  copyOnlySelectedText() {
    let maintainClipboard = false;
    for (let selection of this.getSelectionsOrderedByBufferPosition()) {
      if (!selection.isEmpty()) {
        selection.copy(maintainClipboard, false);
        maintainClipboard = true;
      }
    }
  }

  // Essential: For each selection, cut the selected text.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
  cutSelectedText(options = {}) {
    if (!this.ensureWritable('cutSelectedText', options)) return;
    let maintainClipboard = false;
    this.mutateSelectedText(selection => {
      if (selection.isEmpty()) {
        selection.selectLine();
        selection.cut(maintainClipboard, true, options.bypassReadOnly);
      } else {
        selection.cut(maintainClipboard, false, options.bypassReadOnly);
      }
      maintainClipboard = true;
    });
  }

  // Essential: For each selection, replace the selected text with the contents of
  // the clipboard.
  //
  // If the clipboard contains the same number of selections as the current
  // editor, each selection will be replaced with the content of the
  // corresponding clipboard selection text.
  //
  // * `options` (optional) See {Selection::insertText}.
  pasteText(options = {}) {
    if (!this.ensureWritable('parseText', options)) return;
    options = Object.assign({}, options);
    let {
      text: clipboardText,
      metadata
    } = this.constructor.clipboard.readWithMetadata();
    if (!this.emitWillInsertTextEvent(clipboardText)) return false;

    if (!metadata) metadata = {};
    if (options.autoIndent == null)
      options.autoIndent = this.shouldAutoIndentOnPaste();

    this.mutateSelectedText((selection, index) => {
      let fullLine, indentBasis, text;
      if (
        metadata.selections &&
        metadata.selections.length === this.getSelections().length
      ) {
        ({ text, indentBasis, fullLine } = metadata.selections[index]);
      } else {
        ({ indentBasis, fullLine } = metadata);
        text = clipboardText;
      }

      if (
        indentBasis != null &&
        (text.includes('\n') ||
          !selection.cursor.hasPrecedingCharactersOnLine())
      ) {
        options.indentBasis = indentBasis;
      } else {
        options.indentBasis = null;
      }

      let range;
      if (fullLine && selection.isEmpty()) {
        const oldPosition = selection.getBufferRange().start;
        selection.setBufferRange([[oldPosition.row, 0], [oldPosition.row, 0]]);
        range = selection.insertText(text, options);
        const newPosition = oldPosition.translate([1, 0]);
        selection.setBufferRange([newPosition, newPosition]);
      } else {
        range = selection.insertText(text, options);
      }

      this.emitter.emit('did-insert-text', { text, range });
    });
  }

  // Essential: For each selection, if the selection is empty, cut all characters
  // of the containing screen line following the cursor. Otherwise cut the selected
  // text.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
  cutToEndOfLine(options = {}) {
    if (!this.ensureWritable('cutToEndOfLine', options)) return;
    let maintainClipboard = false;
    this.mutateSelectedText(selection => {
      selection.cutToEndOfLine(maintainClipboard, options);
      maintainClipboard = true;
    });
  }

  // Essential: For each selection, if the selection is empty, cut all characters
  // of the containing buffer line following the cursor. Otherwise cut the
  // selected text.
  //
  // * `options` (optional) {Object}
  //   * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
  cutToEndOfBufferLine(options = {}) {
    if (!this.ensureWritable('cutToEndOfBufferLine', options)) return;
    let maintainClipboard = false;
    this.mutateSelectedText(selection => {
      selection.cutToEndOfBufferLine(maintainClipboard, options);
      maintainClipboard = true;
    });
  }

  /*
  Section: Folds
  */

  // Essential: Fold the most recent cursor's row based on its indentation level.
  //
  // The fold will extend from the nearest preceding line with a lower
  // indentation level up to the nearest following row with a lower indentation
  // level.
  foldCurrentRow() {
    const { row } = this.getCursorBufferPosition();
    const languageMode = this.buffer.getLanguageMode();
    const range =
      languageMode.getFoldableRangeContainingPoint &&
      languageMode.getFoldableRangeContainingPoint(
        Point(row, Infinity),
        this.getTabLength()
      );
    if (range) return this.displayLayer.foldBufferRange(range);
  }

  // Essential: Unfold the most recent cursor's row by one level.
  unfoldCurrentRow() {
    const { row } = this.getCursorBufferPosition();
    return this.displayLayer.destroyFoldsContainingBufferPositions(
      [Point(row, Infinity)],
      false
    );
  }

  // Essential: Fold the given row in buffer coordinates based on its indentation
  // level.
  //
  // If the given row is foldable, the fold will begin there. Otherwise, it will
  // begin at the first foldable row preceding the given row.
  //
  // * `bufferRow` A {Number}.
  foldBufferRow(bufferRow) {
    let position = Point(bufferRow, Infinity);
    const languageMode = this.buffer.getLanguageMode();
    while (true) {
      const foldableRange =
        languageMode.getFoldableRangeContainingPoint &&
        languageMode.getFoldableRangeContainingPoint(
          position,
          this.getTabLength()
        );
      if (foldableRange) {
        const existingFolds = this.displayLayer.foldsIntersectingBufferRange(
          Range(foldableRange.start, foldableRange.start)
        );
        if (existingFolds.length === 0) {
          this.displayLayer.foldBufferRange(foldableRange);
        } else {
          const firstExistingFoldRange = this.displayLayer.bufferRangeForFold(
            existingFolds[0]
          );
          if (firstExistingFoldRange.start.isLessThan(position)) {
            position = Point(firstExistingFoldRange.start.row, 0);
            continue;
          }
        }
      }
      break;
    }
  }

  // Essential: Unfold all folds containing the given row in buffer coordinates.
  //
  // * `bufferRow` A {Number}
  unfoldBufferRow(bufferRow) {
    const position = Point(bufferRow, Infinity);
    return this.displayLayer.destroyFoldsContainingBufferPositions([position]);
  }

  // Extended: For each selection, fold the rows it intersects.
  foldSelectedLines() {
    for (let selection of this.selections) {
      selection.fold();
    }
  }

  // Extended: Fold all foldable lines.
  foldAll() {
    const languageMode = this.buffer.getLanguageMode();
    const foldableRanges =
      languageMode.getFoldableRanges &&
      languageMode.getFoldableRanges(this.getTabLength());
    this.displayLayer.destroyAllFolds();
    for (let range of foldableRanges || []) {
      this.displayLayer.foldBufferRange(range);
    }
  }

  // Extended: Unfold all existing folds.
  unfoldAll() {
    const result = this.displayLayer.destroyAllFolds();
    if (result.length > 0) this.scrollToCursorPosition();
    return result;
  }

  // Extended: Fold all foldable lines at the given indent level.
  //
  // * `level` A {Number} starting at 0.
  foldAllAtIndentLevel(level) {
    const languageMode = this.buffer.getLanguageMode();
    const foldableRanges =
      languageMode.getFoldableRangesAtIndentLevel &&
      languageMode.getFoldableRangesAtIndentLevel(level, this.getTabLength());
    this.displayLayer.destroyAllFolds();
    for (let range of foldableRanges || []) {
      this.displayLayer.foldBufferRange(range);
    }
  }

  // Extended: Determine whether the given row in buffer coordinates is foldable.
  //
  // A *foldable* row is a row that *starts* a row range that can be folded.
  //
  // * `bufferRow` A {Number}
  //
  // Returns a {Boolean}.
  isFoldableAtBufferRow(bufferRow) {
    const languageMode = this.buffer.getLanguageMode();
    return (
      languageMode.isFoldableAtRow && languageMode.isFoldableAtRow(bufferRow)
    );
  }

  // Extended: Determine whether the given row in screen coordinates is foldable.
  //
  // A *foldable* row is a row that *starts* a row range that can be folded.
  //
  // * `bufferRow` A {Number}
  //
  // Returns a {Boolean}.
  isFoldableAtScreenRow(screenRow) {
    return this.isFoldableAtBufferRow(this.bufferRowForScreenRow(screenRow));
  }

  // Extended: Fold the given buffer row if it isn't currently folded, and unfold
  // it otherwise.
  toggleFoldAtBufferRow(bufferRow) {
    if (this.isFoldedAtBufferRow(bufferRow)) {
      return this.unfoldBufferRow(bufferRow);
    } else {
      return this.foldBufferRow(bufferRow);
    }
  }

  // Extended: Determine whether the most recently added cursor's row is folded.
  //
  // Returns a {Boolean}.
  isFoldedAtCursorRow() {
    return this.isFoldedAtBufferRow(this.getCursorBufferPosition().row);
  }

  // Extended: Determine whether the given row in buffer coordinates is folded.
  //
  // * `bufferRow` A {Number}
  //
  // Returns a {Boolean}.
  isFoldedAtBufferRow(bufferRow) {
    const range = Range(
      Point(bufferRow, 0),
      Point(bufferRow, this.buffer.lineLengthForRow(bufferRow))
    );
    return this.displayLayer.foldsIntersectingBufferRange(range).length > 0;
  }

  // Extended: Determine whether the given row in screen coordinates is folded.
  //
  // * `screenRow` A {Number}
  //
  // Returns a {Boolean}.
  isFoldedAtScreenRow(screenRow) {
    return this.isFoldedAtBufferRow(this.bufferRowForScreenRow(screenRow));
  }

  // Creates a new fold between two row numbers.
  //
  // startRow - The row {Number} to start folding at
  // endRow - The row {Number} to end the fold
  //
  // Returns the new {Fold}.
  foldBufferRowRange(startRow, endRow) {
    return this.foldBufferRange(
      Range(Point(startRow, Infinity), Point(endRow, Infinity))
    );
  }

  foldBufferRange(range) {
    return this.displayLayer.foldBufferRange(range);
  }

  // Remove any {Fold}s found that intersect the given buffer range.
  destroyFoldsIntersectingBufferRange(bufferRange) {
    return this.displayLayer.destroyFoldsIntersectingBufferRange(bufferRange);
  }

  // Remove any {Fold}s found that contain the given array of buffer positions.
  destroyFoldsContainingBufferPositions(bufferPositions, excludeEndpoints) {
    return this.displayLayer.destroyFoldsContainingBufferPositions(
      bufferPositions,
      excludeEndpoints
    );
  }

  /*
  Section: Gutters
  */

  // Essential: Add a custom {Gutter}.
  //
  // * `options` An {Object} with the following fields:
  //   * `name` (required) A unique {String} to identify this gutter.
  //   * `priority` (optional) A {Number} that determines stacking order between
  //       gutters. Lower priority items are forced closer to the edges of the
  //       window. (default: -100)
  //   * `visible` (optional) {Boolean} specifying whether the gutter is visible
  //       initially after being created. (default: true)
  //   * `type` (optional) {String} specifying the type of gutter to create. `'decorated'`
  //       gutters are useful as a destination for decorations created with {Gutter::decorateMarker}.
  //       `'line-number'` gutters.
  //   * `class` (optional) {String} added to the CSS classnames of the gutter's root DOM element.
  //   * `labelFn` (optional) {Function} called by a `'line-number'` gutter to generate the label for each line number
  //       element. Should return a {String} that will be used to label the corresponding line.
  //     * `lineData` an {Object} containing information about each line to label.
  //       * `bufferRow` {Number} indicating the zero-indexed buffer index of this line.
  //       * `screenRow` {Number} indicating the zero-indexed screen index.
  //       * `foldable` {Boolean} that is `true` if a fold may be created here.
  //       * `softWrapped` {Boolean} if this screen row is the soft-wrapped continuation of the same buffer row.
  //       * `maxDigits` {Number} the maximum number of digits necessary to represent any known screen row.
  //   * `onMouseDown` (optional) {Function} to be called when a mousedown event is received by a line-number
  //        element within this `type: 'line-number'` {Gutter}. If unspecified, the default behavior is to select the
  //        clicked buffer row.
  //     * `lineData` an {Object} containing information about the line that's being clicked.
  //       * `bufferRow` {Number} of the originating line element
  //       * `screenRow` {Number}
  //   * `onMouseMove` (optional) {Function} to be called when a mousemove event occurs on a line-number element within
  //        within this `type: 'line-number'` {Gutter}.
  //     * `lineData` an {Object} containing information about the line that's being clicked.
  //       * `bufferRow` {Number} of the originating line element
  //       * `screenRow` {Number}
  //
  // Returns the newly-created {Gutter}.
  addGutter(options) {
    return this.gutterContainer.addGutter(options);
  }

  // Essential: Get this editor's gutters.
  //
  // Returns an {Array} of {Gutter}s.
  getGutters() {
    return this.gutterContainer.getGutters();
  }

  getLineNumberGutter() {
    return this.lineNumberGutter;
  }

  // Essential: Get the gutter with the given name.
  //
  // Returns a {Gutter}, or `null` if no gutter exists for the given name.
  gutterWithName(name) {
    return this.gutterContainer.gutterWithName(name);
  }

  /*
  Section: Scrolling the TextEditor
  */

  // Essential: Scroll the editor to reveal the most recently added cursor if it is
  // off-screen.
  //
  // * `options` (optional) {Object}
  //   * `center` Center the editor around the cursor if possible. (default: true)
  scrollToCursorPosition(options) {
    this.getLastCursor().autoscroll({
      center: options && options.center !== false
    });
  }

  // Essential: Scrolls the editor to the given buffer position.
  //
  // * `bufferPosition` An object that represents a buffer position. It can be either
  //   an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
  // * `options` (optional) {Object}
  //   * `center` Center the editor around the position if possible. (default: false)
  scrollToBufferPosition(bufferPosition, options) {
    return this.scrollToScreenPosition(
      this.screenPositionForBufferPosition(bufferPosition),
      options
    );
  }

  // Essential: Scrolls the editor to the given screen position.
  //
  // * `screenPosition` An object that represents a screen position. It can be either
  //    an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
  // * `options` (optional) {Object}
  //   * `center` Center the editor around the position if possible. (default: false)
  scrollToScreenPosition(screenPosition, options) {
    this.scrollToScreenRange(
      new Range(screenPosition, screenPosition),
      options
    );
  }

  scrollToTop() {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::scrollToTop instead.'
    );
    this.getElement().scrollToTop();
  }

  scrollToBottom() {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::scrollToTop instead.'
    );
    this.getElement().scrollToBottom();
  }

  scrollToScreenRange(screenRange, options = {}) {
    if (options.clip !== false) screenRange = this.clipScreenRange(screenRange);
    const scrollEvent = { screenRange, options };
    if (this.component) this.component.didRequestAutoscroll(scrollEvent);
    this.emitter.emit('did-request-autoscroll', scrollEvent);
  }

  getHorizontalScrollbarHeight() {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::getHorizontalScrollbarHeight instead.'
    );
    return this.getElement().getHorizontalScrollbarHeight();
  }

  getVerticalScrollbarWidth() {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::getVerticalScrollbarWidth instead.'
    );
    return this.getElement().getVerticalScrollbarWidth();
  }

  pageUp() {
    this.moveUp(this.getRowsPerPage());
  }

  pageDown() {
    this.moveDown(this.getRowsPerPage());
  }

  selectPageUp() {
    this.selectUp(this.getRowsPerPage());
  }

  selectPageDown() {
    this.selectDown(this.getRowsPerPage());
  }

  // Returns the number of rows per page
  getRowsPerPage() {
    if (this.component) {
      const clientHeight = this.component.getScrollContainerClientHeight();
      const lineHeight = this.component.getLineHeight();
      return Math.max(1, Math.ceil(clientHeight / lineHeight));
    } else {
      return 1;
    }
  }

  /*
  Section: Config
  */

  // Experimental: Is auto-indentation enabled for this editor?
  //
  // Returns a {Boolean}.
  shouldAutoIndent() {
    return this.autoIndent;
  }

  // Experimental: Is auto-indentation on paste enabled for this editor?
  //
  // Returns a {Boolean}.
  shouldAutoIndentOnPaste() {
    return this.autoIndentOnPaste;
  }

  // Experimental: Does this editor allow scrolling past the last line?
  //
  // Returns a {Boolean}.
  getScrollPastEnd() {
    if (this.getAutoHeight()) {
      return false;
    } else {
      return this.scrollPastEnd;
    }
  }

  // Experimental: How fast does the editor scroll in response to mouse wheel
  // movements?
  //
  // Returns a positive {Number}.
  getScrollSensitivity() {
    return this.scrollSensitivity;
  }

  // Experimental: Does this editor show cursors while there is a selection?
  //
  // Returns a positive {Boolean}.
  getShowCursorOnSelection() {
    return this.showCursorOnSelection;
  }

  // Experimental: Are line numbers enabled for this editor?
  //
  // Returns a {Boolean}
  doesShowLineNumbers() {
    return this.showLineNumbers;
  }

  // Experimental: Get the time interval within which text editing operations
  // are grouped together in the editor's undo history.
  //
  // Returns the time interval {Number} in milliseconds.
  getUndoGroupingInterval() {
    return this.undoGroupingInterval;
  }

  // Experimental: Get the characters that are *not* considered part of words,
  // for the purpose of word-based cursor movements.
  //
  // Returns a {String} containing the non-word characters.
  getNonWordCharacters(position) {
    const languageMode = this.buffer.getLanguageMode();
    return (
      (languageMode.getNonWordCharacters &&
        languageMode.getNonWordCharacters(position || Point(0, 0))) ||
      DEFAULT_NON_WORD_CHARACTERS
    );
  }

  /*
  Section: Event Handlers
  */

  handleLanguageModeChange() {
    this.unfoldAll();
    if (this.languageModeSubscription) {
      this.languageModeSubscription.dispose();
      this.disposables.remove(this.languageModeSubscription);
    }
    const languageMode = this.buffer.getLanguageMode();

    if (
      this.component &&
      this.component.visible &&
      languageMode.startTokenizing
    ) {
      languageMode.startTokenizing();
    }
    this.languageModeSubscription =
      languageMode.onDidTokenize &&
      languageMode.onDidTokenize(() => {
        this.emitter.emit('did-tokenize');
      });
    if (this.languageModeSubscription)
      this.disposables.add(this.languageModeSubscription);
    this.emitter.emit('did-change-grammar', languageMode.grammar);
  }

  /*
  Section: TextEditor Rendering
  */

  // Get the Element for the editor.
  getElement() {
    if (!this.component) {
      if (!TextEditorComponent)
        TextEditorComponent = require('./text-editor-component');
      if (!TextEditorElement)
        TextEditorElement = require('./text-editor-element');
      this.component = new TextEditorComponent({
        model: this,
        updatedSynchronously: TextEditorElement.prototype.updatedSynchronously,
        initialScrollTopRow: this.initialScrollTopRow,
        initialScrollLeftColumn: this.initialScrollLeftColumn
      });
    }
    return this.component.element;
  }

  getAllowedLocations() {
    return ['center'];
  }

  // Essential: Retrieves the greyed out placeholder of a mini editor.
  //
  // Returns a {String}.
  getPlaceholderText() {
    return this.placeholderText;
  }

  // Essential: Set the greyed out placeholder of a mini editor. Placeholder text
  // will be displayed when the editor has no content.
  //
  // * `placeholderText` {String} text that is displayed when the editor has no content.
  setPlaceholderText(placeholderText) {
    this.updatePlaceholderText(placeholderText, true);
  }

  pixelPositionForBufferPosition(bufferPosition) {
    Grim.deprecate(
      'This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead'
    );
    return this.getElement().pixelPositionForBufferPosition(bufferPosition);
  }

  pixelPositionForScreenPosition(screenPosition) {
    Grim.deprecate(
      'This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead'
    );
    return this.getElement().pixelPositionForScreenPosition(screenPosition);
  }

  getVerticalScrollMargin() {
    const maxScrollMargin = Math.floor(
      (this.height / this.getLineHeightInPixels() - 1) / 2
    );
    return Math.min(this.verticalScrollMargin, maxScrollMargin);
  }

  setVerticalScrollMargin(verticalScrollMargin) {
    this.verticalScrollMargin = verticalScrollMargin;
    return this.verticalScrollMargin;
  }

  getHorizontalScrollMargin() {
    return Math.min(
      this.horizontalScrollMargin,
      Math.floor((this.width / this.getDefaultCharWidth() - 1) / 2)
    );
  }
  setHorizontalScrollMargin(horizontalScrollMargin) {
    this.horizontalScrollMargin = horizontalScrollMargin;
    return this.horizontalScrollMargin;
  }

  getLineHeightInPixels() {
    return this.lineHeightInPixels;
  }
  setLineHeightInPixels(lineHeightInPixels) {
    this.lineHeightInPixels = lineHeightInPixels;
    return this.lineHeightInPixels;
  }

  getKoreanCharWidth() {
    return this.koreanCharWidth;
  }
  getHalfWidthCharWidth() {
    return this.halfWidthCharWidth;
  }
  getDoubleWidthCharWidth() {
    return this.doubleWidthCharWidth;
  }
  getDefaultCharWidth() {
    return this.defaultCharWidth;
  }

  ratioForCharacter(character) {
    if (isKoreanCharacter(character)) {
      return this.getKoreanCharWidth() / this.getDefaultCharWidth();
    } else if (isHalfWidthCharacter(character)) {
      return this.getHalfWidthCharWidth() / this.getDefaultCharWidth();
    } else if (isDoubleWidthCharacter(character)) {
      return this.getDoubleWidthCharWidth() / this.getDefaultCharWidth();
    } else {
      return 1;
    }
  }

  setDefaultCharWidth(
    defaultCharWidth,
    doubleWidthCharWidth,
    halfWidthCharWidth,
    koreanCharWidth
  ) {
    if (doubleWidthCharWidth == null) {
      doubleWidthCharWidth = defaultCharWidth;
    }
    if (halfWidthCharWidth == null) {
      halfWidthCharWidth = defaultCharWidth;
    }
    if (koreanCharWidth == null) {
      koreanCharWidth = defaultCharWidth;
    }
    if (
      defaultCharWidth !== this.defaultCharWidth ||
      (doubleWidthCharWidth !== this.doubleWidthCharWidth &&
        halfWidthCharWidth !== this.halfWidthCharWidth &&
        koreanCharWidth !== this.koreanCharWidth)
    ) {
      this.defaultCharWidth = defaultCharWidth;
      this.doubleWidthCharWidth = doubleWidthCharWidth;
      this.halfWidthCharWidth = halfWidthCharWidth;
      this.koreanCharWidth = koreanCharWidth;
      if (this.isSoftWrapped()) {
        this.displayLayer.reset({
          softWrapColumn: this.getSoftWrapColumn()
        });
      }
    }
    return defaultCharWidth;
  }

  setHeight(height) {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::setHeight instead.'
    );
    this.getElement().setHeight(height);
  }

  getHeight() {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::getHeight instead.'
    );
    return this.getElement().getHeight();
  }

  getAutoHeight() {
    return this.autoHeight != null ? this.autoHeight : true;
  }

  getAutoWidth() {
    return this.autoWidth != null ? this.autoWidth : false;
  }

  setWidth(width) {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::setWidth instead.'
    );
    this.getElement().setWidth(width);
  }

  getWidth() {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::getWidth instead.'
    );
    return this.getElement().getWidth();
  }

  // Use setScrollTopRow instead of this method
  setFirstVisibleScreenRow(screenRow) {
    this.setScrollTopRow(screenRow);
  }

  getFirstVisibleScreenRow() {
    return this.getElement().component.getFirstVisibleRow();
  }

  getLastVisibleScreenRow() {
    return this.getElement().component.getLastVisibleRow();
  }

  getVisibleRowRange() {
    return [this.getFirstVisibleScreenRow(), this.getLastVisibleScreenRow()];
  }

  // Use setScrollLeftColumn instead of this method
  setFirstVisibleScreenColumn(column) {
    return this.setScrollLeftColumn(column);
  }

  getFirstVisibleScreenColumn() {
    return this.getElement().component.getFirstVisibleColumn();
  }

  getScrollTop() {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::getScrollTop instead.'
    );
    return this.getElement().getScrollTop();
  }

  setScrollTop(scrollTop) {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::setScrollTop instead.'
    );
    this.getElement().setScrollTop(scrollTop);
  }

  getScrollBottom() {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::getScrollBottom instead.'
    );
    return this.getElement().getScrollBottom();
  }

  setScrollBottom(scrollBottom) {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::setScrollBottom instead.'
    );
    this.getElement().setScrollBottom(scrollBottom);
  }

  getScrollLeft() {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::getScrollLeft instead.'
    );
    return this.getElement().getScrollLeft();
  }

  setScrollLeft(scrollLeft) {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::setScrollLeft instead.'
    );
    this.getElement().setScrollLeft(scrollLeft);
  }

  getScrollRight() {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::getScrollRight instead.'
    );
    return this.getElement().getScrollRight();
  }

  setScrollRight(scrollRight) {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::setScrollRight instead.'
    );
    this.getElement().setScrollRight(scrollRight);
  }

  getScrollHeight() {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::getScrollHeight instead.'
    );
    return this.getElement().getScrollHeight();
  }

  getScrollWidth() {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::getScrollWidth instead.'
    );
    return this.getElement().getScrollWidth();
  }

  getMaxScrollTop() {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::getMaxScrollTop instead.'
    );
    return this.getElement().getMaxScrollTop();
  }

  getScrollTopRow() {
    return this.getElement().component.getScrollTopRow();
  }

  setScrollTopRow(scrollTopRow) {
    this.getElement().component.setScrollTopRow(scrollTopRow);
  }

  getScrollLeftColumn() {
    return this.getElement().component.getScrollLeftColumn();
  }

  setScrollLeftColumn(scrollLeftColumn) {
    this.getElement().component.setScrollLeftColumn(scrollLeftColumn);
  }

  intersectsVisibleRowRange(startRow, endRow) {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.'
    );
    return this.getElement().intersectsVisibleRowRange(startRow, endRow);
  }

  selectionIntersectsVisibleRowRange(selection) {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::selectionIntersectsVisibleRowRange instead.'
    );
    return this.getElement().selectionIntersectsVisibleRowRange(selection);
  }

  screenPositionForPixelPosition(pixelPosition) {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::screenPositionForPixelPosition instead.'
    );
    return this.getElement().screenPositionForPixelPosition(pixelPosition);
  }

  pixelRectForScreenRange(screenRange) {
    Grim.deprecate(
      'This is now a view method. Call TextEditorElement::pixelRectForScreenRange instead.'
    );
    return this.getElement().pixelRectForScreenRange(screenRange);
  }

  /*
  Section: Utility
  */

  inspect() {
    return `<TextEditor ${this.id}>`;
  }

  emitWillInsertTextEvent(text) {
    let result = true;
    const cancel = () => {
      result = false;
    };
    this.emitter.emit('will-insert-text', { cancel, text });
    return result;
  }

  /*
  Section: Language Mode Delegated Methods
  */

  suggestedIndentForBufferRow(bufferRow, options) {
    const languageMode = this.buffer.getLanguageMode();
    return (
      languageMode.suggestedIndentForBufferRow &&
      languageMode.suggestedIndentForBufferRow(
        bufferRow,
        this.getTabLength(),
        options
      )
    );
  }

  // Given a buffer row, indent it.
  //
  // * bufferRow - The row {Number}.
  // * options - An options {Object} to pass through to {TextEditor::setIndentationForBufferRow}.
  autoIndentBufferRow(bufferRow, options) {
    const indentLevel = this.suggestedIndentForBufferRow(bufferRow, options);
    return this.setIndentationForBufferRow(bufferRow, indentLevel, options);
  }

  // Indents all the rows between two buffer row numbers.
  //
  // * startRow - The row {Number} to start at
  // * endRow - The row {Number} to end at
  autoIndentBufferRows(startRow, endRow) {
    let row = startRow;
    while (row <= endRow) {
      this.autoIndentBufferRow(row);
      row++;
    }
  }

  autoDecreaseIndentForBufferRow(bufferRow) {
    const languageMode = this.buffer.getLanguageMode();
    const indentLevel =
      languageMode.suggestedIndentForEditedBufferRow &&
      languageMode.suggestedIndentForEditedBufferRow(
        bufferRow,
        this.getTabLength()
      );
    if (indentLevel != null)
      this.setIndentationForBufferRow(bufferRow, indentLevel);
  }

  toggleLineCommentForBufferRow(row) {
    this.toggleLineCommentsForBufferRows(row, row);
  }

  toggleLineCommentsForBufferRows(start, end, options = {}) {
    const languageMode = this.buffer.getLanguageMode();
    let { commentStartString, commentEndString } =
      (languageMode.commentStringsForPosition &&
        languageMode.commentStringsForPosition(new Point(start, 0))) ||
      {};
    if (!commentStartString) return;
    commentStartString = commentStartString.trim();

    if (commentEndString) {
      commentEndString = commentEndString.trim();
      const startDelimiterColumnRange = columnRangeForStartDelimiter(
        this.buffer.lineForRow(start),
        commentStartString
      );
      if (startDelimiterColumnRange) {
        const endDelimiterColumnRange = columnRangeForEndDelimiter(
          this.buffer.lineForRow(end),
          commentEndString
        );
        if (endDelimiterColumnRange) {
          this.buffer.transact(() => {
            this.buffer.delete([
              [end, endDelimiterColumnRange[0]],
              [end, endDelimiterColumnRange[1]]
            ]);
            this.buffer.delete([
              [start, startDelimiterColumnRange[0]],
              [start, startDelimiterColumnRange[1]]
            ]);
          });
        }
      } else {
        this.buffer.transact(() => {
          const indentLength = this.buffer.lineForRow(start).match(/^\s*/)[0]
            .length;
          this.buffer.insert([start, indentLength], commentStartString + ' ');
          this.buffer.insert(
            [end, this.buffer.lineLengthForRow(end)],
            ' ' + commentEndString
          );

          // Prevent the cursor from selecting / passing the delimiters
          // See https://github.com/atom/atom/pull/17519
          if (options.correctSelection && options.selection) {
            const endLineLength = this.buffer.lineLengthForRow(end);
            const oldRange = options.selection.getBufferRange();
            if (oldRange.isEmpty()) {
              if (oldRange.start.column === endLineLength) {
                const endCol = endLineLength - commentEndString.length - 1;
                options.selection.setBufferRange(
                  [[end, endCol], [end, endCol]],
                  { autoscroll: false }
                );
              }
            } else {
              const startDelta =
                oldRange.start.column === indentLength
                  ? [0, commentStartString.length + 1]
                  : [0, 0];
              const endDelta =
                oldRange.end.column === endLineLength
                  ? [0, -commentEndString.length - 1]
                  : [0, 0];
              options.selection.setBufferRange(
                oldRange.translate(startDelta, endDelta),
                { autoscroll: false }
              );
            }
          }
        });
      }
    } else {
      let hasCommentedLines = false;
      let hasUncommentedLines = false;
      for (let row = start; row <= end; row++) {
        const line = this.buffer.lineForRow(row);
        if (NON_WHITESPACE_REGEXP.test(line)) {
          if (columnRangeForStartDelimiter(line, commentStartString)) {
            hasCommentedLines = true;
          } else {
            hasUncommentedLines = true;
          }
        }
      }

      const shouldUncomment = hasCommentedLines && !hasUncommentedLines;

      if (shouldUncomment) {
        for (let row = start; row <= end; row++) {
          const columnRange = columnRangeForStartDelimiter(
            this.buffer.lineForRow(row),
            commentStartString
          );
          if (columnRange)
            this.buffer.delete([[row, columnRange[0]], [row, columnRange[1]]]);
        }
      } else {
        let minIndentLevel = Infinity;
        let minBlankIndentLevel = Infinity;
        for (let row = start; row <= end; row++) {
          const line = this.buffer.lineForRow(row);
          const indentLevel = this.indentLevelForLine(line);
          if (NON_WHITESPACE_REGEXP.test(line)) {
            if (indentLevel < minIndentLevel) minIndentLevel = indentLevel;
          } else {
            if (indentLevel < minBlankIndentLevel)
              minBlankIndentLevel = indentLevel;
          }
        }
        minIndentLevel = Number.isFinite(minIndentLevel)
          ? minIndentLevel
          : Number.isFinite(minBlankIndentLevel)
          ? minBlankIndentLevel
          : 0;

        const indentString = this.buildIndentString(minIndentLevel);
        for (let row = start; row <= end; row++) {
          const line = this.buffer.lineForRow(row);
          if (NON_WHITESPACE_REGEXP.test(line)) {
            const indentColumn = columnForIndentLevel(
              line,
              minIndentLevel,
              this.getTabLength()
            );
            this.buffer.insert(
              Point(row, indentColumn),
              commentStartString + ' '
            );
          } else {
            this.buffer.setTextInRange(
              new Range(new Point(row, 0), new Point(row, Infinity)),
              indentString + commentStartString + ' '
            );
          }
        }
      }
    }
  }

  rowRangeForParagraphAtBufferRow(bufferRow) {
    if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(bufferRow)))
      return;

    const languageMode = this.buffer.getLanguageMode();
    const isCommented = languageMode.isRowCommented(bufferRow);

    let startRow = bufferRow;
    while (startRow > 0) {
      if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(startRow - 1)))
        break;
      if (languageMode.isRowCommented(startRow - 1) !== isCommented) break;
      startRow--;
    }

    let endRow = bufferRow;
    const rowCount = this.getLineCount();
    while (endRow + 1 < rowCount) {
      if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(endRow + 1)))
        break;
      if (languageMode.isRowCommented(endRow + 1) !== isCommented) break;
      endRow++;
    }

    return new Range(
      new Point(startRow, 0),
      new Point(endRow, this.buffer.lineLengthForRow(endRow))
    );
  }
};

function columnForIndentLevel(line, indentLevel, tabLength) {
  let column = 0;
  let indentLength = 0;
  const goalIndentLength = indentLevel * tabLength;
  while (indentLength < goalIndentLength) {
    const char = line[column];
    if (char === '\t') {
      indentLength += tabLength - (indentLength % tabLength);
    } else if (char === ' ') {
      indentLength++;
    } else {
      break;
    }
    column++;
  }
  return column;
}

function columnRangeForStartDelimiter(line, delimiter) {
  const startColumn = line.search(NON_WHITESPACE_REGEXP);
  if (startColumn === -1) return null;
  if (!line.startsWith(delimiter, startColumn)) return null;

  let endColumn = startColumn + delimiter.length;
  if (line[endColumn] === ' ') endColumn++;
  return [startColumn, endColumn];
}

function columnRangeForEndDelimiter(line, delimiter) {
  let startColumn = line.lastIndexOf(delimiter);
  if (startColumn === -1) return null;

  const endColumn = startColumn + delimiter.length;
  if (NON_WHITESPACE_REGEXP.test(line.slice(endColumn))) return null;
  if (line[startColumn - 1] === ' ') startColumn--;
  return [startColumn, endColumn];
}

class ChangeEvent {
  constructor({ oldRange, newRange }) {
    this.oldRange = oldRange;
    this.newRange = newRange;
  }

  get start() {
    return this.newRange.start;
  }

  get oldExtent() {
    return this.oldRange.getExtent();
  }

  get newExtent() {
    return this.newRange.getExtent();
  }
}
