const {Emitter, CompositeDisposable} = require('event-kit')
const {File} = require('pathwatcher')
const diff = require('diff')
const _ = require('underscore-plus')
const path = require('path')
const crypto = require('crypto')
const mkdirp = require('mkdirp')
const superstring = require('superstring')
const NativeTextBuffer = superstring.TextBuffer
const Point = require('./point')
const Range = require('./range')
const DefaultHistoryProvider = require('./default-history-provider')
const NullLanguageMode = require('./null-language-mode')
const Marker = require('./marker')
const MarkerLayer = require('./marker-layer')
const DisplayLayer = require('./display-layer')
const {spliceArray, newlineRegex, patchFromChanges, normalizePatchChanges, extentForText, debounce} = require('./helpers')
const {traverse, traversal} = require('./point-helpers')
const Grim = require('grim')

// Extended: A mutable text container with undo/redo support and the ability to
// annotate logical regions in the text.
//
// ## Observing Changes
//
// You can observe changes in a {TextBuffer} using methods like {::onDidChange},
// {::onDidStopChanging}, and {::getChangesSinceCheckpoint}. These methods report
// aggregated buffer updates as arrays of change objects containing the following
// fields: `oldRange`, `newRange`, `oldText`, and `newText`. The `oldText`,
// `newText`, and `newRange` fields are self-explanatory, but the interepretation
// of `oldRange` is more nuanced:
//
// The reported `oldRange` is the range of the replaced text in the original
// contents of the buffer *irrespective of the spatial impact of any other
// reported change*. So, for example, if you wanted to apply all the changes made
// in a transaction to a clone of the observed buffer, the easiest approach would
// be to apply the changes in reverse:
//
// ```js
// buffer1.onDidChange(({changes}) => {
//   for (const {oldRange, newText} of changes.reverse()) {
//     buffer2.setTextInRange(oldRange, newText)
//   }
// })
// ```
//
// If you needed to apply the changes in the forwards order, you would need to
// incorporate the impact of preceding changes into the range passed to
// {::setTextInRange}, as follows:
//
// ```js
// buffer1.onDidChange(({changes}) => {
//   for (const {oldRange, newRange, newText} of changes) {
//     const rangeToReplace = Range(
//       newRange.start,
//       newRange.start.traverse(oldRange.getExtent())
//     )
//     buffer2.setTextInRange(rangeToReplace, newText)
//   }
// })
// ```
class TextBuffer {
  /*
  Section: Construction
  */

  // Public: Create a new buffer with the given params.
  //
  // * `params` {Object} or {String} of text
  //   * `text` The initial {String} text of the buffer.
  //   * `shouldDestroyOnFileDelete` A {Function} that returns a {Boolean}
  //     indicating whether the buffer should be destroyed if its file is
  //     deleted.
  constructor (params) {
    if (params == null) params = {}

    this.refcount = 0
    this.conflict = false
    this.file = null
    this.fileSubscriptions = null
    this.stoppedChangingTimeout = null
    this.emitter = new Emitter()
    this.changesSinceLastStoppedChangingEvent = []
    this.changesSinceLastDidChangeTextEvent = []
    this.id = crypto.randomBytes(16).toString('hex')
    this.buffer = new NativeTextBuffer(typeof params === 'string' ? params : params.text)
    this.debouncedEmitDidStopChangingEvent = debounce(this.emitDidStopChangingEvent.bind(this), this.stoppedChangingDelay)
    this.maxUndoEntries = params.maxUndoEntries != null ? params.maxUndoEntries : this.defaultMaxUndoEntries
    this.setHistoryProvider(new DefaultHistoryProvider(this))
    this.languageMode = new NullLanguageMode()
    this.nextMarkerLayerId = 0
    this.nextDisplayLayerId = 0
    this.defaultMarkerLayer = new MarkerLayer(this, String(this.nextMarkerLayerId++))
    this.displayLayers = {}
    this.markerLayers = {}
    this.markerLayers[this.defaultMarkerLayer.id] = this.defaultMarkerLayer
    this.markerLayersWithPendingUpdateEvents = new Set()
    this.selectionsMarkerLayerIds = new Set()
    this.nextMarkerId = 1
    this.outstandingSaveCount = 0
    this.loadCount = 0
    this.cachedHasAstral = null
    this._emittedWillChangeEvent = false

    this.setEncoding(params.encoding)
    this.setPreferredLineEnding(params.preferredLineEnding)

    this.loaded = false
    this.destroyed = false
    this.transactCallDepth = 0
    this.digestWhenLastPersisted = false

    this.shouldDestroyOnFileDelete = params.shouldDestroyOnFileDelete || (() => false)

    if (params.filePath) {
      this.setPath(params.filePath)
      if (params.load) {
        Grim.deprecate(
          'The `load` option to the TextBuffer constructor is deprecated. ' +
          'Get a loaded buffer using TextBuffer.load(filePath) instead.'
        )
        this.load({internal: true})
      }
    }
  }

  toString () {
    return `<TextBuffer ${this.id}>`
  }

  // Public: Create a new buffer backed by the given file path.
  //
  // * `source` Either a {String} path to a local file or (experimentally) a file
  //   {Object} as described by the {::setFile} method.
  // * `params` An {Object} with the following properties:
  //   * `encoding` (optional) {String} The file's encoding.
  //   * `shouldDestroyOnFileDelete` (optional) A {Function} that returns a
  //     {Boolean} indicating whether the buffer should be destroyed if its file
  //     is deleted.
  //
  // Returns a {Promise} that resolves with a {TextBuffer} instance.
  static load (source, params) {
    const buffer = new TextBuffer(params)
    if (typeof source === 'string') {
      buffer.setPath(source)
    } else {
      buffer.setFile(source)
    }
    return buffer
      .load({clearHistory: true, internal: true, mustExist: params && params.mustExist})
      .then(() => buffer)
      .catch(err => {
        buffer.destroy()
        throw err
      })
  }

  // Public: Create a new buffer backed by the given file path. For better
  // performance, use {TextBuffer.load} instead.
  //
  // * `filePath` The {String} file path.
  // * `params` An {Object} with the following properties:
  //   * `encoding` (optional) {String} The file's encoding.
  //   * `shouldDestroyOnFileDelete` (optional) A {Function} that returns a
  //     {Boolean} indicating whether the buffer should be destroyed if its file
  //     is deleted.
  //
  // Returns a {TextBuffer} instance.
  static loadSync (filePath, params) {
    const buffer = new TextBuffer(params)
    buffer.setPath(filePath)
    try {
      buffer.loadSync({internal: true, mustExist: params && params.mustExist})
    } catch (e) {
      buffer.destroy()
      throw e
    }
    return buffer
  }

  // Public: Restore a {TextBuffer} based on an earlier state created using
  // the {TextBuffer::serialize} method.
  //
  // * `params` An {Object} returned from {TextBuffer::serialize}
  //
  // Returns a {Promise} that resolves with a {TextBuffer} instance.
  static async deserialize (params) {
    if (params.version && params.version !== TextBuffer.version) return

    delete params.load

    let buffer
    if (params.filePath) {
      buffer = await TextBuffer.load(params.filePath, params)
      if (buffer.digestWhenLastPersisted === params.digestWhenLastPersisted) {
        buffer.buffer.deserializeChanges(params.outstandingChanges)
      } else {
        params.history = {}
      }
    } else {
      buffer = new TextBuffer(params)
    }

    buffer.id = params.id
    buffer.preferredLineEnding = params.preferredLineEnding
    buffer.nextMarkerId = params.nextMarkerId
    buffer.nextMarkerLayerId = params.nextMarkerLayerId
    buffer.nextDisplayLayerId = params.nextDisplayLayerId
    buffer.historyProvider.deserialize(params.history, buffer)

    for (const layerId in params.markerLayers) {
      const layerState = params.markerLayers[layerId]
      let layer
      if (layerId === params.defaultMarkerLayerId) {
        buffer.defaultMarkerLayer.id = layerId
        buffer.defaultMarkerLayer.deserialize(layerState)
        layer = buffer.defaultMarkerLayer
      } else {
        layer = MarkerLayer.deserialize(buffer, layerState)
      }
      buffer.markerLayers[layerId] = layer
    }

    for (const layerId in params.displayLayers) {
      const layerState = params.displayLayers[layerId]
      buffer.displayLayers[layerId] = DisplayLayer.deserialize(buffer, layerState)
    }

    return buffer
  }

  // Returns a {String} representing a unique identifier for this {TextBuffer}.
  getId () {
    return this.id
  }

  serialize (options) {
    if (options == null) options = {}
    if (options.markerLayers == null) options.markerLayers = true
    if (options.history == null) options.history = true

    const markerLayers = {}
    if (options.markerLayers) {
      for (const id in this.markerLayers) {
        const layer = this.markerLayers[id]
        if (layer.persistent) {
          markerLayers[id] = layer.serialize()
        }
      }
    }

    const displayLayers = {}
    for (const id in this.displayLayers) {
      const layer = this.displayLayers[id]
      displayLayers[id] = layer.serialize()
    }

    let history = {}
    if (options.history) {
      history = this.historyProvider.serialize(options)
    }

    const result = {
      id: this.getId(),
      version: TextBuffer.version,
      defaultMarkerLayerId: this.defaultMarkerLayer.id,
      markerLayers,
      displayLayers,
      nextMarkerLayerId: this.nextMarkerLayerId,
      nextDisplayLayerId: this.nextDisplayLayerId,
      history,
      encoding: this.getEncoding(),
      preferredLineEnding: this.preferredLineEnding,
      nextMarkerId: this.nextMarkerId
    }

    const filePath = this.getPath()
    if (filePath) {
      if (this.baseTextDigestCache == null) this.baseTextDigestCache = this.buffer.baseTextDigest()
      result.filePath = filePath
      result.digestWhenLastPersisted = this.digestWhenLastPersisted
      result.outstandingChanges = this.buffer.serializeChanges()
    } else {
      result.text = this.getText()
    }

    return result
  }

  /*
  Section: Event Subscription
  */

  // Public: Invoke the given callback synchronously _before_ the content of the
  // buffer changes.
  //
  // Because observers are invoked synchronously, it's important not to perform
  // any expensive operations via this method.
  //
  // * `callback` {Function} to be called when the buffer changes.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onWillChange (callback) {
    return this.emitter.on('will-change', callback)
  }

  // Public: Invoke the given callback synchronously when a transaction finishes
  // with a list of all the changes in the transaction.
  //
  // * `callback` {Function} to be called when a transaction in which textual
  //   changes occurred is completed.
  //   * `event` {Object} with the following keys:
  //     * `oldRange` The smallest combined {Range} containing all of the old text.
  //     * `newRange` The smallest combined {Range} containing all of the new text.
  //     * `changes` {Array} of {Object}s summarizing the aggregated changes
  //       that occurred during the transaction. See *Working With Aggregated
  //       Changes* in the description of the {TextBuffer} class for details.
  //       * `oldRange` The {Range} of the deleted text in the contents of the
  //         buffer as it existed *before* the batch of changes reported by this
  //         event.
  //       * `newRange`: The {Range} of the inserted text in the current contents
  //         of the buffer.
  //       * `oldText`: A {String} representing the deleted text.
  //       * `newText`: A {String} representing the inserted text.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidChange (callback) {
    return this.emitter.on('did-change-text', callback)
  }

  // Public: This is now identical to {::onDidChange}.
  onDidChangeText (callback) {
    return this.onDidChange(callback)
  }

  // Public: Invoke the given callback asynchronously following one or more
  // changes after {::getStoppedChangingDelay} milliseconds elapse without an
  // additional change.
  //
  // This method can be used to perform potentially expensive operations that
  // don't need to be performed synchronously. If you need to run your callback
  // synchronously, use {::onDidChange} instead.
  //
  // * `callback` {Function} to be called when the buffer stops changing.
  //   * `event` {Object} with the following keys:
  //     * `changes` An {Array} containing {Object}s summarizing the aggregated
  //       changes. See *Working With Aggregated Changes* in the description of
  //       the {TextBuffer} class for details.
  //       * `oldRange` The {Range} of the deleted text in the contents of the
  //         buffer as it existed *before* the batch of changes reported by this
  //         event.
  //       * `newRange`: The {Range} of the inserted text in the current contents
  //         of the buffer.
  //       * `oldText`: A {String} representing the deleted text.
  //       * `newText`: A {String} representing the inserted text.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidStopChanging (callback) {
    return this.emitter.on('did-stop-changing', callback)
  }

  // Public: Invoke the given callback when the in-memory contents of the
  // buffer become in conflict with the contents of the file on disk.
  //
  // * `callback` {Function} to be called when the buffer enters conflict.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidConflict (callback) {
    return this.emitter.on('did-conflict', callback)
  }

  // Public: Invoke the given callback if the value of {::isModified} changes.
  //
  // * `callback` {Function} to be called when {::isModified} changes.
  //   * `modified` {Boolean} indicating whether the buffer is modified.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidChangeModified (callback) {
    return this.emitter.on('did-change-modified', callback)
  }

  // Public: Invoke the given callback when all marker `::onDidChange`
  // observers have been notified following a change to the buffer.
  //
  // The order of events following a buffer change is as follows:
  //
  // * The text of the buffer is changed
  // * All markers are updated accordingly, but their `::onDidChange` observers
  //   are not notified.
  // * `TextBuffer::onDidChange` observers are notified.
  // * `Marker::onDidChange` observers are notified.
  // * `TextBuffer::onDidUpdateMarkers` observers are notified.
  //
  // Basically, this method gives you a way to take action after both a buffer
  // change and all associated marker changes.
  //
  // * `callback` {Function} to be called after markers are updated.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidUpdateMarkers (callback) {
    return this.emitter.on('did-update-markers', callback)
  }

  // Public: Invoke the given callback when a marker is created.
  //
  // * `callback` {Function} to be called when a marker is created.
  //   * `marker` {Marker} that was created.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidCreateMarker (callback) {
    return this.emitter.on('did-create-marker', callback)
  }

  // Public: Invoke the given callback when the value of {::getPath} changes.
  //
  // * `callback` {Function} to be called when the path changes.
  //   * `path` {String} representing the buffer's current path on disk.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidChangePath (callback) {
    return this.emitter.on('did-change-path', callback)
  }

  // Public: Invoke the given callback when the value of {::getEncoding} changes.
  //
  // * `callback` {Function} to be called when the encoding changes.
  //   * `encoding` {String} character set encoding of the buffer.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidChangeEncoding (callback) {
    return this.emitter.on('did-change-encoding', callback)
  }

  // Public: Invoke the given callback before the buffer is saved to disk.
  //
  // * `callback` {Function} to be called before the buffer is saved. If this function returns
  //   a {Promise}, then the buffer will not be saved until the promise resolves.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onWillSave (callback) {
    return this.emitter.on('will-save', callback)
  }

  // Public: 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.emitter.on('did-save', callback)
  }

  // Public: Invoke the given callback after the file backing the buffer is
  // deleted.
  //
  // * `callback` {Function} to be called after the buffer is deleted.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidDelete (callback) {
    return this.emitter.on('did-delete', callback)
  }

  // Public: Invoke the given callback before the buffer is reloaded from the
  // contents of its file on disk.
  //
  // * `callback` {Function} to be called before the buffer is reloaded.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onWillReload (callback) {
    return this.emitter.on('will-reload', callback)
  }

  // Public: Invoke the given callback after the buffer is reloaded from the
  // contents of its file on disk.
  //
  // * `callback` {Function} to be called after the buffer is reloaded.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidReload (callback) {
    return this.emitter.on('did-reload', callback)
  }

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

  // Public: Invoke the given callback when there is an error in watching the
  // file.
  //
  // * `callback` {Function} callback
  //   * `errorObject` {Object}
  //     * `error` {Object} the error object
  //     * `handle` {Function} call this to indicate you have handled the error.
  //       The error will not be thrown if this function is called.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onWillThrowWatchError (callback) {
    return this.emitter.on('will-throw-watch-error', callback)
  }

  // Public: Get the number of milliseconds that will elapse without a change
  // before {::onDidStopChanging} observers are invoked following a change.
  //
  // Returns a {Number}.
  getStoppedChangingDelay () {
    return this.stoppedChangingDelay
  }

  /*
  Section: File Details
  */

  // Public: Determine if the in-memory contents of the buffer differ from its
  // contents on disk.
  //
  // If the buffer is unsaved, always returns `true` unless the buffer is empty.
  //
  // Returns a {Boolean}.
  isModified () {
    if (this.file) {
      return !this.file.existsSync() || this.buffer.isModified()
    } else {
      return this.buffer.getLength() > 0
    }
  }

  // Public: Determine if the in-memory contents of the buffer conflict with the
  // on-disk contents of its associated file.
  //
  // Returns a {Boolean}.
  isInConflict () {
    return this.isModified() && this.fileHasChangedSinceLastLoad
  }

  // Public: Get the path of the associated file.
  //
  // Returns a {String}.
  getPath () {
    return this.file ? this.file.getPath() : undefined
  }

  // Public: Set the path for the buffer's associated file.
  //
  // * `filePath` A {String} representing the new file path
  setPath (filePath) {
    if (filePath === this.getPath()) return
    return this.setFile(filePath && new File(filePath))
  }

  // Experimental: Set a custom {File} object as the buffer's backing store.
  //
  // * `file` An {Object} with the following properties:
  //   * `getPath` A {Function} that returns the {String} path to the file.
  //   * `createReadStream` A {Function} that returns a `Readable` stream
  //     that can be used to load the file's content.
  //   * `createWriteStream` A {Function} that returns a `Writable` stream
  //     that can be used to save content to the file.
  //   * `existsSync` A {Function} that returns a {Boolean}, true if the file exists, false otherwise.
  //   * `onDidChange` (optional) A {Function} that invokes its callback argument
  //     when the file changes. The method should return a {Disposable} that
  //     can be used to prevent further calls to the callback.
  //   * `onDidDelete` (optional) A {Function} that invokes its callback argument
  //     when the file is deleted. The method should return a {Disposable} that
  //     can be used to prevent further calls to the callback.
  //   * `onDidRename` (optional) A {Function} that invokes its callback argument
  //     when the file is renamed. The method should return a {Disposable} that
  //     can be used to prevent further calls to the callback.
  setFile (file) {
    if (!this.file && !file) return
    if (file && file.getPath() === this.getPath()) return

    this.file = file
    if (this.file) {
      if (typeof this.file.setEncoding === 'function') {
        this.file.setEncoding(this.getEncoding())
      }
      this.subscribeToFile()
    }

    this.emitter.emit('did-change-path', this.getPath())
  }

  // Public: Sets the character set encoding for this buffer.
  //
  // * `encoding` The {String} encoding to use (default: 'utf8').
  setEncoding (encoding = 'utf8') {
    if (encoding === this.getEncoding()) return

    this.encoding = encoding
    if (this.file) {
      if (typeof this.file.setEncoding === 'function') {
        this.file.setEncoding(encoding)
      }
      this.emitter.emit('did-change-encoding', encoding)
      if (!this.isModified()) {
        this.load({clearHistory: true, internal: true})
      }
    } else {
      this.emitter.emit('did-change-encoding', encoding)
    }
  }

  // Public: Returns the {String} encoding of this buffer.
  getEncoding () {
    return this.encoding || (this.file && this.file.getEncoding())
  }

  setPreferredLineEnding (preferredLineEnding = null) {
    this.preferredLineEnding = preferredLineEnding
  }

  getPreferredLineEnding () {
    return this.preferredLineEnding
  }

  // Public: Get the path of the associated file.
  //
  // Returns a {String}.
  getUri () {
    return this.getPath()
  }

  // Get the basename of the associated file.
  //
  // The basename is the name portion of the file's path, without the containing
  // directories.
  //
  // Returns a {String}.
  getBaseName () {
    return this.file && this.file.getBaseName()
  }

  /*
  Section: Reading Text
  */

  // Public: Determine whether the buffer is empty.
  //
  // Returns a {Boolean}.
  isEmpty () {
    return this.buffer.getLength() === 0
  }

  // Public: Get the entire text of the buffer. Avoid using this unless you know that the
  // buffer's text is reasonably short.
  //
  // Returns a {String}.
  getText () {
    if (this.cachedText == null) this.cachedText = this.buffer.getText()
    return this.cachedText
  }

  getCharacterAtPosition (position) {
    return this.buffer.getCharacterAtPosition(Point.fromObject(position))
  }

  // Public: Get the text in a range.
  //
  // * `range` A {Range}
  //
  // Returns a {String}
  getTextInRange (range) {
    return this.buffer.getTextInRange(Range.fromObject(range))
  }

  // Public: Get the text of all lines in the buffer, without their line endings.
  //
  // Returns an {Array} of {String}s.
  getLines () {
    return this.buffer.getLines()
  }

  // Public: Get the text of the last line of the buffer, without its line
  // ending.
  //
  // Returns a {String}.
  getLastLine () {
    return this.lineForRow(this.getLastRow())
  }

  // Public: Get the text of the line at the given 0-indexed row, without its
  // line ending.
  //
  // * `row` A {Number} representing the row.
  //
  // Returns a {String}.
  lineForRow (row) {
    return this.buffer.lineForRow(row)
  }

  // Public: Get the line ending for the given 0-indexed row.
  //
  // * `row` A {Number} indicating the row.
  //
  // Returns a {String}. The returned newline is represented as a literal string:
  // `'\n'`, `'\r\n'`, or `''` for the last line of the buffer, which
  // doesn't end in a newline.
  lineEndingForRow (row) {
    return this.buffer.lineEndingForRow(row)
  }

  // Public: Get the length of the line for the given 0-indexed row, without its
  // line ending.
  //
  // * `row` A {Number} indicating the row.
  //
  // Returns a {Number}.
  lineLengthForRow (row) {
    return this.buffer.lineLengthForRow(row)
  }

  // Public: Determine if the given row contains only whitespace.
  //
  // * `row` A {Number} representing a 0-indexed row.
  //
  // Returns a {Boolean}.
  isRowBlank (row) {
    return !/\S/.test(this.lineForRow(row))
  }

  // Public: Given a row, find the first preceding row that's not blank.
  //
  // * `startRow` A {Number} identifying the row to start checking at.
  //
  // Returns a {Number} or `null` if there's no preceding non-blank row.
  previousNonBlankRow (startRow) {
    if (startRow === 0) return null
    startRow = Math.min(startRow, this.getLastRow())
    for (let row = startRow - 1; row >= 0; row--) {
      if (!this.isRowBlank(row)) return row
    }
    return null
  }

  // Public: Given a row, find the next row that's not blank.
  //
  // * `startRow` A {Number} identifying the row to start checking at.
  //
  // Returns a {Number} or `null` if there's no next non-blank row.
  nextNonBlankRow (startRow) {
    const lastRow = this.getLastRow()
    if (startRow < lastRow) {
      for (let row = startRow + 1; row <= lastRow; row++) {
        if (!this.isRowBlank(row)) return row
      }
    }
    return null
  }

  // Extended: Return true if the buffer contains any astral-plane Unicode characters that
  // are encoded as surrogate pairs.
  //
  // Returns a {Boolean}.
  hasAstral () {
    if (this.cachedHasAstral !== null) {
      return this.cachedHasAstral
    } else {
      this.cachedHasAstral = this.buffer.hasAstral()
      return this.cachedHasAstral
    }
  }

  /*
  Section: Mutating Text
  */

  // Public: Replace the entire contents of the buffer with the given text.
  //
  // * `text` A {String}
  //
  // Returns a {Range} spanning the new buffer contents.
  setText (text) {
    return this.setTextInRange(this.getRange(), text, {normalizeLineEndings: false})
  }

  // Public: Replace the current buffer contents by applying a diff based on the
  // given text.
  //
  // * `text` A {String} containing the new buffer contents.
  setTextViaDiff (text) {
    const currentText = this.getText()
    if (currentText === text) return

    const computeBufferColumn = function (str) {
      const newlineIndex = str.lastIndexOf('\n')
      if (newlineIndex === -1) {
        return str.length
      } else {
        return str.length - newlineIndex - 1
      }
    }

    this.transact(() => {
      let row = 0
      let column = 0
      const currentPosition = [0, 0]

      const lineDiff = diff.diffLines(currentText, text)
      const changeOptions = {normalizeLineEndings: false}

      for (let change of lineDiff) {
        const lineCount = change.count
        currentPosition[0] = row
        currentPosition[1] = column

        if (change.added) {
          row += lineCount
          column = computeBufferColumn(change.value)
          this.setTextInRange([currentPosition, currentPosition], change.value, changeOptions)
        } else if (change.removed) {
          const endRow = row + lineCount
          const endColumn = column + computeBufferColumn(change.value)
          this.setTextInRange([currentPosition, [endRow, endColumn]], '', changeOptions)
        } else {
          row += lineCount
          column = computeBufferColumn(change.value)
        }
      }
    })
  }

  // Public: Set the text in the given range.
  //
  // * `range` A {Range}
  // * `text` A {String}
  // * `options` (optional) {Object}
  //   * `normalizeLineEndings` (optional) {Boolean} (default: true)
  //   * `undo` (optional) *Deprecated* {String} 'skip' will cause this change
  //     to be grouped with the preceding change for the purposes of undo and
  //     redo. This property is deprecated. Call groupLastChanges() on the
  //     buffer after instead.
  //
  // Returns the {Range} of the inserted text.
  setTextInRange (range, newText, options) {
    let normalizeLineEndings, undo
    if (options) ({normalizeLineEndings, undo} = options)
    if (normalizeLineEndings == null) normalizeLineEndings = true
    if (undo != null) {
      Grim.deprecate('The `undo` option is deprecated. Call groupLastChanges() on the TextBuffer afterward instead.')
    }

    if (this.transactCallDepth === 0) {
      const newRange = this.transact(() => this.setTextInRange(range, newText, {normalizeLineEndings}))
      if (undo === 'skip') this.groupLastChanges()
      return newRange
    }

    const oldRange = this.clipRange(range)
    const oldText = this.getTextInRange(oldRange)

    if (normalizeLineEndings) {
      const normalizedEnding = this.preferredLineEnding ||
        this.lineEndingForRow(oldRange.start.row) ||
        this.lineEndingForRow(oldRange.start.row - 1)
      if (normalizedEnding) {
        newText = newText.replace(newlineRegex, normalizedEnding)
      }
    }

    const change = {
      oldStart: oldRange.start,
      oldEnd: oldRange.end,
      newStart: oldRange.start,
      newEnd: traverse(oldRange.start, extentForText(newText)),
      oldText,
      newText
    }
    const newRange = this.applyChange(change, true)
    if (undo === 'skip') this.groupLastChanges()
    return newRange
  }

  // Public: Insert text at the given position.
  //
  // * `position` A {Point} representing the insertion location. The position is
  //   clipped before insertion.
  // * `text` A {String} representing the text to insert.
  // * `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.
  //
  // Returns the {Range} of the inserted text.
  insert (position, text, options) {
    return this.setTextInRange(new Range(position, position), text, options)
  }

  // Public: Append text to the end of the buffer.
  //
  // * `text` A {String} representing the text text to append.
  // * `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.
  //
  // Returns the {Range} of the inserted text
  append (text, options) {
    return this.insert(this.getEndPosition(), text, options)
  }

  // Applies a change to the buffer based on its old range and new text.
  applyChange (change, pushToHistory = false) {
    const {newStart, newEnd, oldStart, oldEnd, oldText, newText} = change

    const oldExtent = traversal(oldEnd, oldStart)
    const oldRange = Range(newStart, traverse(newStart, oldExtent))
    oldRange.freeze()

    const newExtent = traversal(newEnd, newStart)
    const newRange = Range(newStart, traverse(newStart, newExtent))
    newRange.freeze()

    if (pushToHistory) {
      if (!change.oldExtent) change.oldExtent = oldExtent
      if (!change.newExtent) change.newExtent = newExtent
      if (this.historyProvider) {
        this.historyProvider.pushChange(change)
      }
    }

    const changeEvent = {oldRange, newRange, oldText, newText}
    for (const id in this.displayLayers) {
      const displayLayer = this.displayLayers[id]
      displayLayer.bufferWillChange(changeEvent)
    }

    this.emitWillChangeEvent()
    this.buffer.setTextInRange(oldRange, newText)

    if (this.markerLayers) {
      for (const id in this.markerLayers) {
        const markerLayer = this.markerLayers[id]
        markerLayer.splice(oldRange.start, oldExtent, newExtent)
        this.markerLayersWithPendingUpdateEvents.add(markerLayer)
      }
    }

    this.cachedText = null
    this.changesSinceLastDidChangeTextEvent.push(change)
    this.changesSinceLastStoppedChangingEvent.push(change)
    this.emitDidChangeEvent(changeEvent)
    return newRange
  }

  emitDidChangeEvent (changeEvent) {
    if (!changeEvent.oldRange.isEmpty() || !changeEvent.newRange.isEmpty()) {
      this.languageMode.bufferDidChange(changeEvent)
      for (const id in this.displayLayers) {
        this.displayLayers[id].bufferDidChange(changeEvent)
      }
    }
  }

  // Public: Delete the text in the given range.
  //
  // * `range` A {Range} in which to delete. The range is clipped before deleting.
  //
  // Returns an empty {Range} starting at the start of deleted range.
  delete (range) {
    return this.setTextInRange(range, '')
  }

  // Public: Delete the line associated with a specified 0-indexed row.
  //
  // * `row` A {Number} representing the row to delete.
  //
  // Returns the {Range} of the deleted text.
  deleteRow (row) {
    return this.deleteRows(row, row)
  }

  // Public: Delete the lines associated with the specified 0-indexed row range.
  //
  // If the row range is out of bounds, it will be clipped. If the `startRow` is
  // greater than the `endRow`, they will be reordered.
  //
  // * `startRow` A {Number} representing the first row to delete.
  // * `endRow` A {Number} representing the last row to delete, inclusive.
  //
  // Returns the {Range} of the deleted text.
  deleteRows (startRow, endRow) {
    let endPoint, startPoint
    const lastRow = this.getLastRow()

    if (startRow > endRow) { [startRow, endRow] = [endRow, startRow] }

    if (endRow < 0) {
      return new Range(this.getFirstPosition(), this.getFirstPosition())
    }

    if (startRow > lastRow) {
      return new Range(this.getEndPosition(), this.getEndPosition())
    }

    startRow = Math.max(0, startRow)
    endRow = Math.min(lastRow, endRow)

    if (endRow < lastRow) {
      startPoint = new Point(startRow, 0)
      endPoint = new Point(endRow + 1, 0)
    } else {
      if (startRow === 0) {
        startPoint = new Point(startRow, 0)
      } else {
        startPoint = new Point(startRow - 1, this.lineLengthForRow(startRow - 1))
      }
      endPoint = new Point(endRow, this.lineLengthForRow(endRow))
    }

    return this.delete(new Range(startPoint, endPoint))
  }

  /*
  Section: Markers
  */

  // Public: Create a layer to contain a set of related markers.
  //
  // * `options` (optional) An {Object} contaning the following keys:
  //   * `maintainHistory` (optional) A {Boolean} indicating whether or not the
  //     state of this layer should be restored on undo/redo operations. Defaults
  //     to `false`.
  //   * `persistent` (optional) 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}.
  //   * `role` (optional) A {String} indicating role of this marker layer
  //
  // Returns a {MarkerLayer}.
  addMarkerLayer (options) {
    const layer = new MarkerLayer(this, String(this.nextMarkerLayerId++), options)
    this.markerLayers[layer.id] = layer
    return layer
  }

  // Public: Get a {MarkerLayer} by id.
  //
  // * `id` The id of the marker layer to retrieve.
  //
  // Returns a {MarkerLayer} or `undefined` if no layer exists with the given
  // id.
  getMarkerLayer (id) {
    return this.markerLayers[id]
  }

  // Public: Get the default {MarkerLayer}.
  //
  // All {Marker} APIs not tied to an explicit layer interact with this default
  // layer.
  //
  // Returns a {MarkerLayer}.
  getDefaultMarkerLayer () {
    return this.defaultMarkerLayer
  }

  // Public: Create a {Marker} with the given range in the default {MarkerLayer}.
  // 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` (optional) A hash of key-value pairs to associate with the
  //   marker. There are also reserved property names that have marker-specific
  //   meaning.
  //   * `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.
  //   * `exclusive` (optional) {Boolean} indicating whether insertions at the
  //     start or end of the marked range should be interpreted as happening
  //     *outside* the marker. Defaults to `false`, except when using the
  //     `inside` invalidation strategy or when when the marker has no tail, in
  //     which case it defaults to true. Explicitly assigning this option
  //     overrides behavior in all circumstances.
  //
  // Returns a {Marker}.
  markRange (range, properties) {
    return this.defaultMarkerLayer.markRange(range, properties)
  }

  // Public: Create a {Marker} at the given position with no tail in the default
  // marker layer.
  //
  // * `position` {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.
  //   * `exclusive` (optional) {Boolean} indicating whether insertions at the
  //     start or end of the marked range should be interpreted as happening
  //     *outside* the marker. Defaults to `false`, except when using the
  //     `inside` invalidation strategy or when when the marker has no tail, in
  //     which case it defaults to true. Explicitly assigning this option
  //     overrides behavior in all circumstances.
  //
  // Returns a {Marker}.
  markPosition (position, options) {
    return this.defaultMarkerLayer.markPosition(position, options)
  }

  // Public: Get all existing markers on the default marker layer.
  //
  // Returns an {Array} of {Marker}s.
  getMarkers () {
    return this.defaultMarkerLayer.getMarkers()
  }

  // Public: Get an existing marker by its id from the default marker layer.
  //
  // * `id` {Number} id of the marker to retrieve
  //
  // Returns a {Marker}.
  getMarker (id) {
    return this.defaultMarkerLayer.getMarker(id)
  }

  // Public: Find markers conforming to the given parameters in the default
  // marker layer.
  //
  // Markers are sorted based on their position in the buffer. If two markers
  // start at the same position, the larger marker comes first.
  //
  // * `params` A hash of key-value pairs constraining the set of returned markers. You
  //   can query against custom marker properties by listing the desired
  //   key-value pairs here. In addition, the following keys are reserved and
  //   have special semantics:
  //   * `startPosition` Only include markers that start at the given {Point}.
  //   * `endPosition` Only include markers that end at the given {Point}.
  //   * `startsInRange` Only include markers that start inside the given {Range}.
  //   * `endsInRange` Only include markers that end inside the given {Range}.
  //   * `containsPoint` Only include markers that contain the given {Point}, inclusive.
  //   * `containsRange` Only include markers that contain the given {Range}, inclusive.
  //   * `startRow` Only include markers that start at the given row {Number}.
  //   * `endRow` Only include markers that end at the given row {Number}.
  //   * `intersectsRow` Only include markers that intersect the given row {Number}.
  //
  // Returns an {Array} of {Marker}s.
  findMarkers (params) {
    return this.defaultMarkerLayer.findMarkers(params)
  }

  // Public: 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()
  }

  /*
  Section: History
  */

  setHistoryProvider (historyProvider) {
    this.historyProvider = historyProvider
  }

  restoreDefaultHistoryProvider (history) {
    const provider = new DefaultHistoryProvider(this)
    provider.restoreFromSnapshot(history)
    return this.setHistoryProvider(provider)
  }

  getHistory (maxEntries) {
    if (this.transactCallDepth > 0) {
      throw new Error('Cannot build history snapshots within transactions')
    }

    const snapshot = this.historyProvider.getSnapshot(maxEntries)

    const baseTextBuffer = new NativeTextBuffer(this.getText())
    for (let i = snapshot.undoStackChanges.length - 1; i >= 0; i--) {
      const change = snapshot.undoStackChanges[i]
      const newRange = Range(change.newStart, change.newEnd)
      baseTextBuffer.setTextInRange(newRange, change.oldText)
    }

    return {
      baseText: baseTextBuffer.getText(),
      undoStack: snapshot.undoStack,
      redoStack: snapshot.redoStack,
      nextCheckpointId: snapshot.nextCheckpointId
    }
  }

  // Provide fallback in case people are using this renamed private field in packages.
  get history () {
    return this.historyProvider
  }

  // Public: Undo the last operation. If a transaction is in progress, aborts it.
  //
  // * `options` (optional) {Object}
  //   * `selectionsMarkerLayer` (optional)
  //     Restore snapshot of selections marker layer to given selectionsMarkerLayer.
  //
  // Returns a {Boolean} of whether or not a change was made.
  undo (options) {
    const pop = this.historyProvider.undo()
    if (!pop) return false

    this.emitWillChangeEvent()
    this.transactCallDepth++
    try {
      for (const change of pop.textUpdates) {
        this.applyChange(change)
      }
    } finally {
      this.transactCallDepth--
    }
    this.restoreFromMarkerSnapshot(pop.markers, options && options.selectionsMarkerLayer)
    this.emitDidChangeTextEvent()
    this.emitMarkerChangeEvents(pop.markers)
    return true
  }

  // Public: Redo the last operation
  //
  // * `options` (optional) {Object}
  //   * `selectionsMarkerLayer` (optional)
  //     Restore snapshot of selections marker layer to given selectionsMarkerLayer.
  //
  // Returns a {Boolean} of whether or not a change was made.
  redo (options) {
    const pop = this.historyProvider.redo()
    if (!pop) return false

    this.emitWillChangeEvent()
    this.transactCallDepth++
    try {
      for (const change of pop.textUpdates) {
        this.applyChange(change)
      }
    } finally {
      this.transactCallDepth--
    }
    this.restoreFromMarkerSnapshot(pop.markers, options && options.selectionsMarkerLayer)
    this.emitDidChangeTextEvent()
    this.emitMarkerChangeEvents(pop.markers)
    return true
  }

  // Public: 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.
  //
  // * `options` (optional) {Object}
  //   * `groupingInterval` (optional) The {Number} of milliseconds for which this
  //     transaction should be considered 'open for grouping' after it begins. If
  //     a transaction with a positive `groupingInterval` is committed while the
  //     previous transaction is still open for grouping, the two transactions
  //     are merged with respect to undo and redo.
  //   * `selectionsMarkerLayer` (optional)
  //     When provided, skip taking snapshot for other selections markerLayers except given one.
  // * `groupingInterval` (optional) The {Number} of milliseconds for which this
  //   transaction should be considered 'open for grouping' after it begins. If a
  //   transaction with a positive `groupingInterval` is committed while the previous
  //   transaction is still open for grouping, the two transactions are merged with
  //   respect to undo and redo.
  // * `fn` A {Function} to call inside the transaction.
  transact (options, fn) {
    let groupingInterval, result, selectionsMarkerLayer
    if (typeof options === 'function') {
      fn = options
      groupingInterval = 0
    } else if (typeof options === 'object') {
      ({groupingInterval, selectionsMarkerLayer} = options)
      if (groupingInterval == null) { groupingInterval = 0 }
    } else {
      groupingInterval = options
    }

    const checkpointBefore = this.historyProvider.createCheckpoint({
      markers: this.createMarkerSnapshot(selectionsMarkerLayer),
      isBarrier: true
    })

    try {
      this.transactCallDepth++
      result = fn()
    } catch (exception) {
      this.revertToCheckpoint(checkpointBefore, {deleteCheckpoint: true})
      if (!(exception instanceof TransactionAbortedError)) throw exception
      return
    } finally {
      this.transactCallDepth--
    }

    if (this.isDestroyed()) return result
    const endMarkerSnapshot = this.createMarkerSnapshot(selectionsMarkerLayer)
    this.historyProvider.groupChangesSinceCheckpoint(checkpointBefore, {
      markers: endMarkerSnapshot,
      deleteCheckpoint: true
    })
    this.historyProvider.applyGroupingInterval(groupingInterval)
    this.historyProvider.enforceUndoStackSizeLimit()
    this.emitDidChangeTextEvent()
    this.emitMarkerChangeEvents(endMarkerSnapshot)
    return result
  }

  // Public: Abort the currently running transaction
  //
  // Only intended to be called within the `fn` option to {::transact}
  abortTransaction () {
    throw new TransactionAbortedError('Transaction aborted.')
  }

  // Public: Clear the undo stack.
  clearUndoStack () {
    return this.historyProvider.clearUndoStack()
  }

  // Public: Create a pointer to the current state of the buffer for use
  // with {::revertToCheckpoint} and {::groupChangesSinceCheckpoint}.
  //
  // * `options` (optional) {Object}
  //   * `selectionsMarkerLayer` (optional)
  //     When provided, skip taking snapshot for other selections markerLayers except given one.
  //
  // Returns a checkpoint id value.
  createCheckpoint (options) {
    return this.historyProvider.createCheckpoint({markers: this.createMarkerSnapshot(options != null ? options.selectionsMarkerLayer : undefined), isBarrier: false})
  }

  // Public: 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` {Number} id of the checkpoint to revert to.
  // * `options` (optional) {Object}
  //   * `selectionsMarkerLayer` (optional)
  //     Restore snapshot of selections marker layer to given selectionsMarkerLayer.
  //
  // Returns a {Boolean} indicating whether the operation succeeded.
  revertToCheckpoint (checkpoint, options) {
    const truncated = this.historyProvider.revertToCheckpoint(checkpoint)
    if (!truncated) return false
    this.emitWillChangeEvent()
    this.transactCallDepth++
    try {
      for (const change of truncated.textUpdates) {
        this.applyChange(change)
      }
    } finally {
      this.transactCallDepth--
    }
    this.restoreFromMarkerSnapshot(truncated.markers, options && options.selectionsMarkerLayer)
    this.emitDidChangeTextEvent()
    this.emitter.emit('did-update-markers')
    this.emitMarkerChangeEvents(truncated.markers)
    return true
  }

  // Public: 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` {Number} id of the checkpoint to group changes since.
  // * `options` (optional) {Object}
  //   * `selectionsMarkerLayer` (optional)
  //     When provided, skip taking snapshot for other selections markerLayers except given one.
  //
  // Returns a {Boolean} indicating whether the operation succeeded.
  groupChangesSinceCheckpoint (checkpoint, options) {
    return this.historyProvider.groupChangesSinceCheckpoint(checkpoint, {
      markers: this.createMarkerSnapshot(options && options.selectionsMarkerLayer),
      deleteCheckpoint: false
    })
  }

  // Public: Group the last two text changes for purposes of undo/redo.
  //
  // This operation will only succeed if there are two changes on the undo
  // stack. It will not group past the beginning of an open transaction.
  //
  // Returns a {Boolean} indicating whether the operation succeeded.
  groupLastChanges () {
    return this.historyProvider.groupLastChanges()
  }

  // Public: Returns a list of changes since the given checkpoint.
  //
  // If the given checkpoint is no longer present in the undo history, this
  // method will return an empty {Array}.
  //
  // * `checkpoint` {Number} id of the checkpoint to get changes since.
  //
  // Returns an {Array} of {Object}s with the following fields that summarize
  //  the aggregated changes since the checkpoint. See *Working With Aggregated
  // Changes* in the description of the {TextBuffer} class for details.
  // * `oldRange` The {Range} of the deleted text in the text as it existed when
  //   the checkpoint was created.
  // * `newRange`: The {Range} of the inserted text in the current text.
  // * `oldText`: A {String} representing the deleted text.
  // * `newText`: A {String} representing the inserted text.
  getChangesSinceCheckpoint (checkpoint) {
    const changes = this.historyProvider.getChangesSinceCheckpoint(checkpoint)
    if (changes) {
      return normalizePatchChanges(changes)
    } else {
      return []
    }
  }

  /*
  Section: Search And Replace
  */

  // Public: Scan regular expression matches in the entire buffer, calling the
  // given iterator function on each match.
  //
  // If you're programmatically modifying the results, you may want to try
  // {::backwardsScan} 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 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.
  //   * `leadingContextLines` An {Array} with `leadingContextLineCount` lines before the match.
  //   * `trailingContextLines` An {Array} with `trailingContextLineCount` lines after the match.
  scan (regex, options = {}, iterator) {
    if (_.isFunction(options)) {
      iterator = options
      options = {}
    }

    return this.scanInRange(regex, this.getRange(), options, iterator)
  }

  // Public: Scan regular expression matches in the entire buffer in reverse
  // order, calling the given iterator function on each match.
  //
  // * `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 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.
  //   * `leadingContextLines` An {Array} with `leadingContextLineCount` lines before the match.
  //   * `trailingContextLines` An {Array} with `trailingContextLineCount` lines after the match.
  backwardsScan (regex, options = {}, iterator) {
    if (_.isFunction(options)) {
      iterator = options
      options = {}
    }

    return this.backwardsScanInRange(regex, this.getRange(), options, iterator)
  }

  // Public: 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.
  // * `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.
  // * `callback` 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.
  //   * `leadingContextLines` An {Array} with `leadingContextLineCount` lines before the match.
  //   * `trailingContextLines` An {Array} with `trailingContextLineCount` lines after the match.
  scanInRange (regex, range, options = {}, callback, reverse = false) {
    if (_.isFunction(options)) {
      reverse = callback
      callback = options
      options = {}
    }

    range = this.clipRange(range)
    const matchRanges = this.findAllInRangeSync(regex, range)
    let startIndex = 0
    let endIndex = matchRanges.length
    let increment = 1
    let previousRow = -1
    let replacementColumnDelta = 0
    if (reverse) {
      startIndex = matchRanges.length - 1
      endIndex = -1
      increment = -1
    }

    for (let i = startIndex; i !== endIndex; i += increment) {
      const matchRange = matchRanges[i]
      if (range.end.isEqual(matchRange.start) && (range.end.column > 0)) continue
      if (matchRange.start.row !== previousRow) {
        replacementColumnDelta = 0
      }
      previousRow = matchRange.start.row
      matchRange.start.column += replacementColumnDelta
      matchRange.end.column += replacementColumnDelta

      const argument = new SearchCallbackArgument(this, Range.fromObject(matchRange), regex, options)
      callback(argument)
      if (argument.stopped || !regex.global) break

      if (!reverse && argument.replacementText != null) {
        replacementColumnDelta +=
          (matchRange.start.column + argument.replacementText.length) -
          matchRange.end.column
      }
    }
  }

  // Public: 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.
  // * `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 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.
  backwardsScanInRange (regex, range, options = {}, iterator) {
    if (_.isFunction(options)) {
      iterator = options
      options = {}
    }

    return this.scanInRange(regex, range, options, iterator, true)
  }

  // Public: Replace all regular expression matches in the entire buffer.
  //
  // * `regex` A {RegExp} representing the matches to be replaced.
  // * `replacementText` A {String} representing the text to replace each match.
  //
  // Returns a {Number} representing the number of replacements made.
  replace (regex, replacementText) {
    const doSave = !this.isModified()
    let replacements = 0

    this.transact(() => {
      return this.scan(regex, function ({matchText, replace}) {
        const text = matchText.replace(regex, replacementText)
        replacementText = text === matchText ? replacementText : text
        replace(replacementText)
        return replacements++
      })
    })

    if (doSave) this.save()

    return replacements
  }

  // Experimental: Asynchronously search the buffer for a given regex.
  //
  // * `regex` A {RegExp} to search for.
  //
  // Returns a {Promise} that resolves with the first {Range} of text that
  // matches the given regex.
  find (regex) { return this.buffer.find(regex) }

  // Experimental: Asynchronously search a given range of the buffer for a given regex.
  //
  // * `regex` A {RegExp} to search for.
  // * `range` A {Range} to search within.
  //
  // Returns a {Promise} that resolves with the first {Range} of text that
  // matches the given regex.
  findInRange (regex, range) { return this.buffer.findInRange(regex, range) }

  // Experimental: Search the buffer for a given regex.
  //
  // * `regex` A {RegExp} to search for.
  //
  // Returns the first {Range} of text that matches the given regex.
  findSync (regex) { return this.buffer.findSync(regex) }

  // Experimental: Search a given range of the buffer for a given regex.
  //
  // * `regex` A {RegExp} to search for.
  // * `range` A {Range} to search within.
  //
  // Returns the first {Range} of text that matches the given regex.
  findInRangeSync (regex, range) { return this.buffer.findInRangeSync(regex, range) }

  // Experimental: Asynchronously search the buffer for a given regex.
  //
  // * `regex` A {RegExp} to search for.
  //
  // Returns a {Promise} that resolves with an {Array} containing every
  // {Range} of text that matches the given regex.
  findAll (regex) { return this.buffer.findAll(regex) }

  // Experimental: Asynchronously search a given range of the buffer for a given regex.
  //
  // * `regex` A {RegExp} to search for.
  // * `range` A {Range} to search within.
  //
  // Returns a {Promise} that resolves with an {Array} containing every
  // {Range} of text that matches the given regex.
  findAllInRange (regex, range) { return this.buffer.findAllInRange(regex, range) }

  // Experimental: Run an regexp search on the buffer
  //
  // * `regex` A {RegExp} to search for.
  //
  // Returns an {Array} containing every {Range} of text that matches the given
  // regex.
  findAllSync (regex) { return this.buffer.findAllSync(regex) }

  // Experimental: Search a given range of the buffer for a given regex.
  //
  // * `regex` A {RegExp} to search for.
  // * `range` A {Range} to search within.
  //
  // Returns an {Array} containing every {Range} of text that matches the given
  // regex.
  findAllInRangeSync (regex, range) { return this.buffer.findAllInRangeSync(regex, range) }

  // Experimental: Search a given range of the buffer for a given regex. Store
  // the matching ranges in the given marker layer.
  //
  // * `markerLayer` A {MarkerLayer} to populate.
  // * `regex` A {RegExp} to search for.
  // * `range` A {Range} to search within.
  //
  // Returns an {Array} of {Marker}s representing the matches.
  findAndMarkAllInRangeSync (markerLayer, regex, range, options = {}) {
    const startId = this.nextMarkerId
    const exclusive = options.invalidate === 'inside' || !options.tailed
    this.nextMarkerId += this.buffer.findAndMarkAllSync(
      markerLayer.index,
      startId,
      exclusive,
      regex,
      Range.fromObject(range)
    )
    const markers = []
    for (let id = startId; id < this.nextMarkerId; id++) {
      const marker = new Marker(id, markerLayer, null, options, true)
      markerLayer.markersById[id] = marker
      markers.push(marker)
    }
    return markers
  }

  // Experimental: Find fuzzy match suggestions in the buffer
  //
  // * `query` A {String} to search for.
  // * `extraWordCharacters` A {String} of additional word characters to use when
  //    deciphering word boundaries
  // * `maxCount` A {Number} that limits the number of matches returned
  //
  // Returns an {Array} containing every {SubsequenceMatch} of text that matches the given
  // query.
  findWordsWithSubsequence (query, extraWordCharacters, maxCount) {
    return this.buffer.findWordsWithSubsequence(query, extraWordCharacters, maxCount)
  }

  // Experimental: Find fuzzy match suggestions in the buffer in a given range
  //
  // * `query` A {String} to search for.
  // * `extraWordCharacters` A {String} of additional word characters to use when
  //    deciphering word boundaries
  // * `maxCount` A {Number} that limits the number of matches returned
  // * `range` A {Range} that specifies the portion of the buffer to search
  //
  // Returns an {Array} containing every {SubsequenceMatch} of text that matches the given
  // query in the given range.
  findWordsWithSubsequenceInRange (query, extraWordCharacters, maxCount, range) {
    return this.buffer.findWordsWithSubsequenceInRange(query, extraWordCharacters, maxCount, range)
  }

  /*
  Section: Buffer Range Details
  */

  // Public: Get the range spanning from `[0, 0]` to {::getEndPosition}.
  //
  // Returns a {Range}.
  getRange () {
    return new Range(this.getFirstPosition(), this.getEndPosition())
  }

  // Public: Get the number of lines in the buffer.
  //
  // Returns a {Number}.
  getLineCount () { return this.buffer.getLineCount() }

  // Public: Get the last 0-indexed row in the buffer.
  //
  // Returns a {Number}.
  getLastRow () {
    return this.getLineCount() - 1
  }

  // Public: Get the first position in the buffer, which is always `[0, 0]`.
  //
  // Returns a {Point}.
  getFirstPosition () {
    return new Point(0, 0)
  }

  // Public: Get the maximal position in the buffer, where new text would be
  // appended.
  //
  // Returns a {Point}.
  getEndPosition () { return Point.fromObject(this.buffer.getExtent()) }

  // Public: Get the length of the buffer's text.
  getLength () { return this.buffer.getLength() }

  // Public: Get the length of the buffer in characters.
  //
  // Returns a {Number}.
  getMaxCharacterIndex () {
    return this.characterIndexForPosition(Point.INFINITY)
  }

  // Public: Get the range for the given row
  //
  // * `row` A {Number} representing a 0-indexed row.
  // * `includeNewline` A {Boolean} indicating whether or not to include the
  //   newline, which results in a range that extends to the start
  //   of the next line. (default: `false`)
  //
  // Returns a {Range}.
  rangeForRow (row, includeNewline) {
    row = Math.max(row, 0)
    row = Math.min(row, this.getLastRow())
    if (includeNewline && row < this.getLastRow()) {
      return new Range(new Point(row, 0), new Point(row + 1, 0))
    } else {
      return new Range(new Point(row, 0), new Point(row, this.lineLengthForRow(row)))
    }
  }

  // Public: Convert a position in the buffer in row/column coordinates to an
  // absolute character offset, inclusive of line ending characters.
  //
  // The position is clipped prior to translating.
  //
  // * `position` A {Point} or point-compatible {Array}.
  //
  // Returns a {Number}.
  characterIndexForPosition (position) {
    return this.buffer.characterIndexForPosition(Point.fromObject(position))
  }

  // Public: Convert an absolute character offset, inclusive of newlines, to a
  // position in the buffer in row/column coordinates.
  //
  // The offset is clipped prior to translating.
  //
  // * `offset` A {Number}.
  //
  // Returns a {Point}.
  positionForCharacterIndex (offset) {
    return Point.fromObject(this.buffer.positionForCharacterIndex(offset))
  }

  // Public: Clip the given range so it starts and ends at valid positions.
  //
  // For example, the position `[1, 100]` is out of bounds if the line at row 1 is
  // only 10 characters long, and it would be clipped to `(1, 10)`.
  //
  // * `range` A {Range} or range-compatible {Array} to clip.
  //
  // Returns the given {Range} if it is already in bounds, or a new clipped
  // {Range} if the given range is out-of-bounds.
  clipRange (range) {
    range = Range.fromObject(range)
    const start = this.clipPosition(range.start)
    const end = this.clipPosition(range.end)
    if (range.start.isEqual(start) && range.end.isEqual(end)) {
      return range
    } else {
      return new Range(start, end)
    }
  }

  // Public: Clip the given point so it is at a valid position in the buffer.
  //
  // For example, the position (1, 100) is out of bounds if the line at row 1 is
  // only 10 characters long, and it would be clipped to (1, 10)
  //
  // * `position` A {Point} or point-compatible {Array}.
  //
  // Returns a new {Point} if the given position is invalid, otherwise returns
  // the given position.
  clipPosition (position, options) {
    position = Point.fromObject(position)
    Point.assertValid(position)
    const {row, column} = position
    if (row < 0) {
      return this.getFirstPosition()
    } else if (row > this.getLastRow()) {
      return this.getEndPosition()
    } else if (column < 0) {
      return Point(row, 0)
    } else {
      const lineLength = this.lineLengthForRow(row)
      if (column >= lineLength && row < this.getLastRow() && options && options.clipDirection === 'forward') {
        return new Point(row + 1, 0)
      } else if (column > lineLength) {
        return new Point(row, lineLength)
      } else {
        return position
      }
    }
  }

  /*
  Section: Buffer Operations
  */

  // Public: Save the buffer.
  //
  // Returns a {Promise} that resolves when the save has completed.
  save () {
    return this.saveTo(this.file)
  }

  // Public: Save the buffer at a specific path.
  //
  // * `filePath` The path to save at.
  //
  // Returns a {Promise} that resolves when the save has completed.
  saveAs (filePath) {
    if (!filePath) throw new Error("Can't save buffer with no file path")
    return this.saveTo(new File(filePath))
  }

  async saveTo (file) {
    if (this.destroyed) throw new Error("Can't save destroyed buffer")
    if (!file) throw new Error("Can't save a buffer with no file")

    const filePath = file.getPath()

    this.outstandingSaveCount++

    try {
      let destination
      if (file instanceof File) {
        await new Promise((resolve, reject) => {
          mkdirp(path.dirname(filePath), error => {
            error ? reject(error) : resolve()
          })
        })
        destination = filePath
      } else {
        destination = file.createWriteStream()
      }

      await this.emitter.emitAsync('will-save', {path: filePath})

      try {
        await this.buffer.save(destination, this.getEncoding())
      } catch (error) {
        if (error.code !== 'EACCES' || destination !== filePath) throw error

        const isWindows = process.platform === 'win32'
        if (isWindows) {
          const winattr = getPromisifiedWinattr()
          const attrs = await winattr.get(filePath)
          if (!attrs.hidden) throw error

          try {
            await winattr.set(filePath, { hidden: false })
            await this.buffer.save(filePath, this.getEncoding())
            await winattr.set(filePath, { hidden: true })
          } catch (_) {
            throw error
          }
        } else {
          const fsAdmin = require('fs-admin')
          try {
            await this.buffer.save(fsAdmin.createWriteStream(filePath), this.getEncoding())
          } catch (_) {
            throw error
          }
        }
      }
    } finally {
      this.outstandingSaveCount--
    }

    this.setFile(file)
    this.fileHasChangedSinceLastLoad = false
    this.digestWhenLastPersisted = this.buffer.baseTextDigest()
    this.loaded = true
    this.emitModifiedStatusChanged(false)
    this.emitter.emit('did-save', {path: filePath})
    return this
  }

  // Public: Reload the file's content from disk.
  //
  // Returns a {Promise} that resolves when the load is complete.
  reload () {
    return this.load({discardChanges: true, internal: true})
  }

  /*
  Section: Display Layers
  */

  addDisplayLayer (params) {
    const id = this.nextDisplayLayerId++
    const displayLayer = new DisplayLayer(id, this, params)
    this.displayLayers[id] = displayLayer
    return displayLayer
  }

  getDisplayLayer (id) {
    return this.displayLayers[id]
  }

  setDisplayLayers (displayLayers) {
    this.displayLayers = displayLayers
     // Used for deserialization
  }

  /*
  Language Modes
  */

  // Experimental: Get the language mode associated with this buffer.
  //
  // Returns a language mode {Object} (See {TextBuffer::setLanguageMode} for its interface).
  getLanguageMode () { return this.languageMode }

  // Experimental: Set the LanguageMode for this buffer.
  //
  // * `languageMode` - an {Object} with the following methods:
  //   * `getLanguageId` - A {Function} that returns a {String} identifying the language.
  //   * `bufferDidChange` - A {Function} that is called whenever the buffer changes.
  //     * `change` An {Object} with the following fields:
  //       * `oldText` {String} The deleted text
  //       * `oldRange` The {Range} of the deleted text before the change took place.
  //       * `newText` {String} The inserted text
  //       * `newRange` The {Range} of the inserted text after the change took place.
  //   * `onDidChangeHighlighting` - A {Function} that takes a callback {Function} and calls it with
  //     a {Range} argument whenever the syntax of a given part of the buffer is updated.
  //   * `buildHighlightIterator` - A function that returns an iterator object with the following
  //     methods:
  //     * `seek` A {Function} that takes a {Point} and resets the iterator to that position.
  //     * `moveToSuccessor` A {Function} that advances the iterator to the next token
  //     * `getPosition` A {Function} that returns a {Point} representing the iterator's current
  //       position in the buffer.
  //     * `getCloseTags` A {Function} that returns an {Array} of {Number}s representing tokens
  //        that end at the current position.
  //     * `getOpenTags` A {Function} that returns an {Array} of {Number}s representing tokens
  //        that begin at the current position.
  setLanguageMode (languageMode) {
    if (languageMode !== this.languageMode) {
      const oldLanguageMode = this.languageMode
      if (typeof this.languageMode.destroy === 'function') {
        this.languageMode.destroy()
      }
      this.languageMode = languageMode || new NullLanguageMode()
      for (const id in this.displayLayers) {
        const displayLayer = this.displayLayers[id]
        displayLayer.bufferDidChangeLanguageMode(languageMode)
      }
      this.emitter.emit('did-change-language-mode', {newMode: this.languageMode, oldMode: oldLanguageMode})
    }
  }

  // Experimental: Call the given callback whenever the buffer's language mode changes.
  //
  // * `callback` - A {Function} to call when the language mode changes.
  //   * `languageMode` - The buffer's new language mode {Object}. See {TextBuffer::setLanguageMode}
  //     for its interface.
  //   * `oldlanguageMode` - The buffer's old language mode {Object}. See {TextBuffer::setLanguageMode}
  //     for its interface.
  //
  // Returns a {Disposable} that can be used to stop the callback from being called.
  onDidChangeLanguageMode (callback) {
    return this.emitter.on('did-change-language-mode', ({newMode, oldMode}) => callback(newMode, oldMode))
  }

  /*
  Section: Private Utility Methods
  */
  registerSelectionsMarkerLayer (markerLayer) {
    return this.selectionsMarkerLayerIds.add(markerLayer.id)
  }

  loadSync (options) {
    if (!options || !options.internal) {
      Grim.deprecate('The .loadSync instance method is deprecated. Create a loaded buffer using TextBuffer.loadSync(filePath) instead.')
    }

    let patch = null
    let checkpoint = null
    try {
      patch = this.buffer.loadSync(
        this.getPath(),
        this.getEncoding(),
        (percentDone, patch) => {
          if (patch && patch.getChangeCount() > 0) {
            checkpoint = this.historyProvider.createCheckpoint({
              markers: this.createMarkerSnapshot(),
              isBarrier: true
            })
            this.emitter.emit('will-reload')
            this.emitWillChangeEvent()
          }
        }
      )
      this.finishLoading(checkpoint, patch, options)
    } catch (error) {
      if ((!options || !options.mustExist) && error.code === 'ENOENT') {
        this.emitter.emit('will-reload')
        if (options && options.discardChanges) this.setText('')
        this.emitter.emit('did-reload')
      } else {
        throw error
      }
    }

    return this
  }

  async load (options) {
    if (!options || !options.internal) {
      Grim.deprecate('The .load instance method is deprecated. Create a loaded buffer using TextBuffer.load(filePath) instead.')
    }

    const source = this.file instanceof File
      ? this.file.getPath()
      : this.file.createReadStream()

    const loadCount = ++this.loadCount

    let checkpoint = null
    let patch
    try {
      patch = await this.buffer.load(
        source,
        {
          encoding: this.getEncoding(),
          force: options && options.discardChanges,
          patch: this.loaded
        },
        (percentDone, patch) => {
          if (this.loadCount > loadCount) return false
          if (patch) {
            if (patch.getChangeCount() > 0) {
              checkpoint = this.historyProvider.createCheckpoint({markers: this.createMarkerSnapshot(), isBarrier: true})
              this.emitter.emit('will-reload')
              return this.emitWillChangeEvent()
            } else if (options && options.discardChanges) {
              return this.emitter.emit('will-reload')
            }
          }
        }
      )
      this.finishLoading(checkpoint, patch, options)
    } catch (error) {
      if ((!options || !options.mustExist) && error.code === 'ENOENT') {
        this.emitter.emit('will-reload')
        if (options && options.discardChanges) this.setText('')
        this.emitter.emit('did-reload')
      } else {
        throw error
      }
    }

    return this
  }

  finishLoading (checkpoint, patch, options) {
    if (this.isDestroyed() || (this.loaded && checkpoint == null && patch != null)) {
      if (options && options.discardChanges) {
        this.emitter.emit('did-reload')
      }
      return
    }

    this.fileHasChangedSinceLastLoad = false
    this.digestWhenLastPersisted = this.buffer.baseTextDigest()
    this.cachedText = null

    if (this.loaded && patch && patch.getChangeCount() > 0) {
      const changes = patch.getChanges()

      if (options && options.clearHistory) {
        this.historyProvider.clearUndoStack()
      } else {
        if (this.historyProvider.pushPatch) {
          this.historyProvider.pushPatch(patch)
        } else {
          this.historyProvider.pushChanges(changes)
        }
      }

      if (changes) {
        this.changesSinceLastDidChangeTextEvent.push(...changes)
        this.changesSinceLastStoppedChangingEvent.push(...changes)
      }

      if (this.markerLayers != null) {
        for (const change of changes) {
          for (const id in this.markerLayers) {
            const markerLayer = this.markerLayers[id]
            markerLayer.splice(
              change.newStart,
              traversal(change.oldEnd, change.oldStart),
              traversal(change.newEnd, change.newStart)
            )
          }
        }
      }

      const markersSnapshot = this.createMarkerSnapshot()
      this.historyProvider.groupChangesSinceCheckpoint(checkpoint, {
        markers: markersSnapshot,
        deleteCheckpoint: true
      })

      this.emitDidChangeEvent(new ChangeEvent(this, changes))
      this.emitDidChangeTextEvent()
      this.emitMarkerChangeEvents(markersSnapshot)
      this.emitModifiedStatusChanged(this.isModified())
    }

    this.loaded = true
    this.emitter.emit('did-reload')
    return this
  }

  destroy () {
    if (this.destroyed) return
    this.destroyed = true
    this.emitter.emit('did-destroy')
    this.emitter.clear()

    if (this.fileSubscriptions != null) {
      this.fileSubscriptions.dispose()
    }
    for (const id in this.markerLayers) {
      const markerLayer = this.markerLayers[id]
      markerLayer.destroy()
    }
    if (this.outstandingSaveCount === 0) {
      this.buffer.reset('')
    } else {
      var subscription = this.onDidSave(() => {
        if (this.outstandingSaveCount === 0) {
          this.buffer.reset('')
          subscription.dispose()
        }
      })
    }

    this.cachedText = null
    if (typeof this.historyProvider.clear === 'function') this.historyProvider.clear()
    if (typeof this.languageMode.destroy === 'function') this.languageMode.destroy()
  }

  isAlive () { return !this.destroyed }

  isDestroyed () { return this.destroyed }

  isRetained () { return this.refcount > 0 }

  retain () {
    this.refcount++
    return this
  }

  release () {
    this.refcount--
    if (this.refcount <= 0) this.destroy()
    return this
  }

  subscribeToFile () {
    if (this.fileSubscriptions) this.fileSubscriptions.dispose()
    this.fileSubscriptions = new CompositeDisposable()

    if (this.file.onDidChange) {
      this.fileSubscriptions.add(this.file.onDidChange(debounce(async () => {
        // On Linux we get change events when the file is deleted. This yields
        // consistent behavior with Mac/Windows.
        if (!this.file.existsSync()) return
        if (this.outstandingSaveCount > 0) return
        this.fileHasChangedSinceLastLoad = true

        if (this.isModified()) {
          const source = this.file instanceof File
            ? this.file.getPath()
            : this.file.createReadStream()
          if (!(await this.buffer.baseTextMatchesFile(source, this.getEncoding()))) {
            this.emitter.emit('did-conflict')
          }
        } else {
          return this.load({internal: true})
        }
      }, this.fileChangeDelay)))
    }

    if (this.file.onDidDelete) {
      this.fileSubscriptions.add(this.file.onDidDelete(() => {
        const modified = this.buffer.isModified()
        this.emitter.emit('did-delete')
        if (!modified && this.shouldDestroyOnFileDelete()) {
          return this.destroy()
        } else {
          return this.emitModifiedStatusChanged(true)
        }
      }))
    }

    if (this.file.onDidRename) {
      this.fileSubscriptions.add(this.file.onDidRename(() => {
        this.emitter.emit('did-change-path', this.getPath())
      }))
    }

    if (this.file.onWillThrowWatchError) {
      this.fileSubscriptions.add(this.file.onWillThrowWatchError(error => {
        this.emitter.emit('will-throw-watch-error', error)
      }))
    }
  }

  createMarkerSnapshot (selectionsMarkerLayer) {
    const snapshot = {}
    for (const markerLayerId in this.markerLayers) {
      const markerLayer = this.markerLayers[markerLayerId]
      if (!markerLayer.maintainHistory) continue
      if (
        selectionsMarkerLayer &&
        markerLayer.getRole() === 'selections' &&
        markerLayerId !== selectionsMarkerLayer.id
      ) continue
      snapshot[markerLayerId] = markerLayer.createSnapshot()
    }
    return snapshot
  }

  restoreFromMarkerSnapshot (snapshot, selectionsMarkerLayer) {
    let selectionsSnapshotId
    if (selectionsMarkerLayer != null) {
      // Do selective selections marker restoration only when snapshot includes single selections snapshot.
      const selectionsSnapshotIds = Object.keys(snapshot).filter(id => this.selectionsMarkerLayerIds.has(id))
      if (selectionsSnapshotIds.length === 1) {
        selectionsSnapshotId = selectionsSnapshotIds[0]
      }
    }

    for (const markerLayerId in snapshot) {
      const layerSnapshot = snapshot[markerLayerId]
      if (markerLayerId === selectionsSnapshotId) {
        this.markerLayers[selectionsMarkerLayer.id].restoreFromSnapshot(
          layerSnapshot,
          markerLayerId !== selectionsMarkerLayer.id
        )
      } else if (this.markerLayers[markerLayerId]) {
        this.markerLayers[markerLayerId].restoreFromSnapshot(layerSnapshot)
      }
    }
  }

  emitMarkerChangeEvents (snapshot) {
    if (this.transactCallDepth === 0) {
      while (this.markerLayersWithPendingUpdateEvents.size > 0) {
        const updatedMarkerLayers = Array.from(this.markerLayersWithPendingUpdateEvents)
        this.markerLayersWithPendingUpdateEvents.clear()
        for (const markerLayer of updatedMarkerLayers) {
          markerLayer.emitUpdateEvent()
          if (markerLayer === this.defaultMarkerLayer) {
            this.emitter.emit('did-update-markers')
          }
        }
      }
    }

    for (const markerLayerId in this.markerLayers) {
      const markerLayer = this.markerLayers[markerLayerId]
      markerLayer.emitChangeEvents(snapshot && snapshot[markerLayerId])
    }
  }

  emitWillChangeEvent () {
    if (!this._emittedWillChangeEvent) {
      this.emitter.emit('will-change')
      this._emittedWillChangeEvent = true
    }
  }

  emitDidChangeTextEvent () {
    this.cachedHasAstral = null
    if (this.transactCallDepth === 0) {
      if (this.changesSinceLastDidChangeTextEvent.length > 0) {
        const compactedChanges = patchFromChanges(this.changesSinceLastDidChangeTextEvent).getChanges()
        this.changesSinceLastDidChangeTextEvent.length = 0
        if (compactedChanges.length > 0) {
          const changeEvent = new ChangeEvent(this, compactedChanges)
          this.languageMode.bufferDidFinishTransaction(changeEvent)
          this.emitter.emit('did-change-text', changeEvent)
        }
        this.debouncedEmitDidStopChangingEvent()
        this._emittedWillChangeEvent = false
      }
      for (const id in this.displayLayers) {
        const displayLayer = this.displayLayers[id]
        displayLayer.emitDeferredChangeEvents()
      }
    }
  }

  // Identifies if the buffer belongs to multiple editors.
  //
  // For example, if the {EditorView} was split.
  //
  // Returns a {Boolean}.
  hasMultipleEditors () { return this.refcount > 1 }

  emitDidStopChangingEvent () {
    if (this.destroyed) return
    const modifiedStatus = this.isModified()
    const compactedChanges = Object.freeze(normalizePatchChanges(
      patchFromChanges(this.changesSinceLastStoppedChangingEvent).getChanges()
    ))
    this.changesSinceLastStoppedChangingEvent.length = 0
    this.emitter.emit('did-stop-changing', {changes: compactedChanges})
    this.emitModifiedStatusChanged(modifiedStatus)
  }

  emitModifiedStatusChanged (modifiedStatus) {
    if (modifiedStatus === this.previousModifiedStatus) return
    this.previousModifiedStatus = modifiedStatus
    return this.emitter.emit('did-change-modified', modifiedStatus)
  }

  logLines (start = 0, end = this.getLastRow()) {
    for (let row = start; row <= end; row++) {
      const line = this.lineForRow(row)
      console.log(row, line, line.length)
    }
  }

  /*
  Section: Private History Delegate Methods
  */

  invertChange (change) {
    return Object.freeze({
      oldRange: change.newRange,
      newRange: change.oldRange,
      oldText: change.newText,
      newText: change.oldText
    })
  }

  serializeChange (change) {
    return {
      oldRange: change.oldRange.serialize(),
      newRange: change.newRange.serialize(),
      oldText: change.oldText,
      newText: change.newText
    }
  }

  deserializeChange (change) {
    return {
      oldRange: Range.deserialize(change.oldRange),
      newRange: Range.deserialize(change.newRange),
      oldText: change.oldText,
      newText: change.newText
    }
  }

  serializeSnapshot (snapshot, options) {
    if (!options.markerLayers) return

    return MarkerLayer.serializeSnapshot(snapshot)
  }

  deserializeSnapshot (snapshot) {
    return MarkerLayer.deserializeSnapshot(snapshot)
  }

  /*
  Section: Private MarkerLayer Delegate Methods
  */

  markerLayerDestroyed (markerLayer) {
    return delete this.markerLayers[markerLayer.id]
  }

  markerCreated (layer, marker) {
    if (layer === this.defaultMarkerLayer) {
      return this.emitter.emit('did-create-marker', marker)
    }
  }

  markersUpdated (layer) {
    if (this.transactCallDepth === 0) {
      layer.emitUpdateEvent()
      if (layer === this.defaultMarkerLayer) {
        return this.emitter.emit('did-update-markers')
      }
    } else {
      return this.markerLayersWithPendingUpdateEvents.add(layer)
    }
  }

  getNextMarkerId () { return this.nextMarkerId++ }
}

Object.assign(TextBuffer, {
  version: 5,
  Point: Point,
  Range: Range,
  newlineRegex: newlineRegex,
  spliceArray: spliceArray
})

TextBuffer.Patch = superstring.Patch

Object.assign(TextBuffer.prototype, {
  stoppedChangingDelay: 300,
  fileChangeDelay: 200,
  backwardsScanChunkSize: 8000,
  defaultMaxUndoEntries: 10000
})

class TransactionAbortedError extends Error {}

class ChangeEvent {
  constructor (buffer, changes) {
    this.changes = Object.freeze(normalizePatchChanges(changes))

    const start = changes[0].oldStart
    const {oldEnd, newEnd} = changes[changes.length - 1]
    this.oldRange = new Range(start, oldEnd).freeze()
    this.newRange = new Range(start, newEnd).freeze()

    let oldText = null
    let newText = null

    Object.defineProperty(this, 'oldText', {
      enumerable: false,
      get () {
        if (oldText == null) {
          const oldBuffer = new NativeTextBuffer(this.newText)
          for (let i = changes.length - 1; i >= 0; i--) {
            const change = changes[i]
            oldBuffer.setTextInRange(
              new Range(
                traversal(change.newStart, start),
                traversal(change.newEnd, start)
              ),
              change.oldText
            )
          }
          oldText = oldBuffer.getText()
        }
        return oldText
      }
    })

    Object.defineProperty(this, 'newText', {
      enumerable: false,
      get () {
        if (newText == null) {
          newText = buffer.getTextInRange(this.newRange)
        }
        return newText
      }
    })
  }

  isEqual (other) {
    return (
      (this.changes.length === other.changes.length) &&
      this.changes.every((change, i) => change.isEqual(other.changes[i])) &&
      this.oldRange.isEqual(other.oldRange) &&
      this.newRange.isEqual(other.newRange)
    )
  }
}

class SearchCallbackArgument {
  get row () {
    return this.range.start.row
  }

  get lineText () {
    return this.buffer.lineForRow(this.range.start.row)
  }

  get lineTextOffset () { return 0 }

  get matchText () {
    return this.buffer.getTextInRange(this.range)
  }

  get match () {
    this.regex.lastIndex = 0
    return this.regex.exec(this.matchText)
  }

  static addContextLines (argument, options) {
    argument.leadingContextLines = []
    let row = Math.max(0, argument.range.start.row - (options.leadingContextLineCount || 0))
    while (row < argument.range.start.row) {
      argument.leadingContextLines.push(argument.buffer.lineForRow(row))
      row += 1
    }

    argument.trailingContextLines = []
    for (let i = 0, end = options.trailingContextLineCount || 0; i < end; i++) {
      row = argument.range.start.row + i + 1
      if (row >= argument.buffer.getLineCount()) break
      argument.trailingContextLines.push(argument.buffer.lineForRow(row))
    }
  }

  constructor (buffer, range, regex, options) {
    this.replace = this.replace.bind(this)
    this.stop = this.stop.bind(this)
    this.buffer = buffer
    this.range = range
    this.regex = regex
    this.stopped = false
    this.replacementText = null
    SearchCallbackArgument.addContextLines(this, options)
  }

  replace (text) {
    this.replacementText = text
    return this.buffer.setTextInRange(this.range, text)
  }

  stop () {
    this.stopped = true
  }
}

let _winattr = null
const getPromisifiedWinattr = function () {
  if (_winattr === null) {
    const { promisify } = require('util')
    const winattr = require('winattr')
    _winattr = {
      set: promisify(winattr.set),
      get: promisify(winattr.get)
    }
  }

  return _winattr
}

module.exports = TextBuffer
