text: 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);
    }
  
  
    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
      });
    }
  
  
    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);
    }
  
  
    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);
    }
  
  
    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();
    }
  }
  