_ = require 'underscore-plus'
path = require 'path'
Serializable = require 'serializable'
Delegator = require 'delegato'
{Model} = require 'theorist'
{Point, Range} = require 'text-buffer'
LanguageMode = require './language-mode'
DisplayBuffer = require './display-buffer'
Cursor = require './cursor'
Selection = require './selection'
TextMateScopeSelector = require('first-mate').ScopeSelector

# Public: 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. If you're
# interested in the visual appearance of editors, use {EditorView} instead.
#
# 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 Editor Instances
#
# The easiest way to get hold of `Editor` objects is by registering a callback
# with `::eachEditor` 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.
#
# ```coffeescript
#   atom.workspace.eachEditor (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.
#
# ## Common Tasks
#
# This is a subset of methods on this class. Refer to the complete summary for
# its full capabilities.
#
# ### Cursors
# - {::setCursorBufferPosition}
# - {::setCursorScreenPosition}
# - {::moveCursorUp}
# - {::moveCursorDown}
# - {::moveCursorLeft}
# - {::moveCursorRight}
# - {::moveCursorToBeginningOfWord}
# - {::moveCursorToEndOfWord}
# - {::moveCursorToPreviousWordBoundary}
# - {::moveCursorToNextWordBoundary}
# - {::moveCursorToBeginningOfNextWord}
# - {::moveCursorToBeginningOfLine}
# - {::moveCursorToEndOfLine}
# - {::moveCursorToFirstCharacterOfLine}
# - {::moveCursorToTop}
# - {::moveCursorToBottom}
#
# ### Selections
# - {::getSelectedBufferRange}
# - {::getSelectedBufferRanges}
# - {::setSelectedBufferRange}
# - {::setSelectedBufferRanges}
# - {::selectUp}
# - {::selectDown}
# - {::selectLeft}
# - {::selectRight}
# - {::selectToBeginningOfWord}
# - {::selectToEndOfWord}
# - {::selectToPreviousWordBoundary}
# - {::selectToNextWordBoundary}
# - {::selectWord}
# - {::selectToBeginningOfLine}
# - {::selectToEndOfLine}
# - {::selectToFirstCharacterOfLine}
# - {::selectToTop}
# - {::selectToBottom}
# - {::selectAll}
# - {::addSelectionForBufferRange}
# - {::addSelectionAbove}
# - {::addSelectionBelow}
# - {::splitSelectionsIntoLines}
#
# ### Manipulating Text
# - {::getText}
# - {::getSelectedText}
# - {::setText}
# - {::setTextInBufferRange}
# - {::insertText}
# - {::insertNewline}
# - {::insertNewlineAbove}
# - {::insertNewlineBelow}
# - {::backspace}
# - {::backspaceToBeginningOfWord}
# - {::backspaceToBeginningOfLine}
# - {::delete}
# - {::deleteToEndOfWord}
# - {::deleteLine}
# - {::cutSelectedText}
# - {::cutToEndOfLine}
# - {::copySelectedText}
# - {::pasteText}
#
# ### Undo, Redo, and Transactions
# - {::undo}
# - {::redo}
# - {::transact}
# - {::abortTransaction}
#
# ### Markers
# - {::markBufferRange}
# - {::markScreenRange}
# - {::getMarker}
# - {::findMarkers}
module.exports =
class Editor extends Model
  Serializable.includeInto(this)
  atom.deserializers.add(this)
  Delegator.includeInto(this)

  @properties
    scrollTop: 0
    scrollLeft: 0

  deserializing: false
  callDisplayBufferCreatedHook: false
  registerEditor: false
  buffer: null
  languageMode: null
  cursors: null
  selections: null
  suppressSelectionMerging: false

  @delegatesMethods 'suggestedIndentForBufferRow', 'autoIndentBufferRow', 'autoIndentBufferRows',
    'autoDecreaseIndentForBufferRow', 'toggleLineCommentForBufferRow', 'toggleLineCommentsForBufferRows',
    toProperty: 'languageMode'

  constructor: ({@softTabs, initialLine, tabLength, softWrap, @displayBuffer, buffer, registerEditor, suppressCursorCreation}) ->
    super

    @cursors = []
    @selections = []

    @displayBuffer ?= new DisplayBuffer({buffer, tabLength, softWrap})
    @buffer = @displayBuffer.buffer
    @softTabs = @buffer.usesSoftTabs() ? @softTabs ? atom.config.get('editor.softTabs') ? true

    for marker in @findMarkers(@getSelectionMarkerAttributes())
      marker.setAttributes(preserveFolds: true)
      @addSelection(marker)

    @subscribeToBuffer()
    @subscribeToDisplayBuffer()

    if @getCursors().length is 0 and not suppressCursorCreation
      if initialLine
        position = [initialLine, 0]
      else
        position = [0, 0]
      @addCursorAtBufferPosition(position)

    @languageMode = new LanguageMode(this)

    @subscribe @$scrollTop, (scrollTop) => @emit 'scroll-top-changed', scrollTop
    @subscribe @$scrollLeft, (scrollLeft) => @emit 'scroll-left-changed', scrollLeft

    atom.project.addEditor(this) if registerEditor

  serializeParams: ->
    id: @id
    softTabs: @softTabs
    scrollTop: @scrollTop
    scrollLeft: @scrollLeft
    displayBuffer: @displayBuffer.serialize()

  deserializeParams: (params) ->
    params.displayBuffer = DisplayBuffer.deserialize(params.displayBuffer)
    params.registerEditor = true
    params

  subscribeToBuffer: ->
    @buffer.retain()
    @subscribe @buffer, "path-changed", =>
      unless atom.project.getPath()?
        atom.project.setPath(path.dirname(@getPath()))
      @emit "title-changed"
      @emit "path-changed"
    @subscribe @buffer, "contents-modified", => @emit "contents-modified"
    @subscribe @buffer, "contents-conflicted", => @emit "contents-conflicted"
    @subscribe @buffer, "modified-status-changed", => @emit "modified-status-changed"
    @subscribe @buffer, "destroyed", => @destroy()
    @preserveCursorPositionOnBufferReload()

  subscribeToDisplayBuffer: ->
    @subscribe @displayBuffer, 'marker-created', @handleMarkerCreated
    @subscribe @displayBuffer, "changed", (e) => @emit 'screen-lines-changed', e
    @subscribe @displayBuffer, "markers-updated", => @mergeIntersectingSelections()
    @subscribe @displayBuffer, 'grammar-changed', => @handleGrammarChange()
    @subscribe @displayBuffer, 'soft-wrap-changed', (args...) => @emit 'soft-wrap-changed', args...

  getViewClass: ->
    require './editor-view'

  destroyed: ->
    @unsubscribe()
    selection.destroy() for selection in @getSelections()
    @buffer.release()
    @displayBuffer.destroy()
    @languageMode.destroy()
    atom.project?.removeEditor(this)

  # Create an {Editor} with its initial state based on this object
  copy: ->
    tabLength = @getTabLength()
    displayBuffer = @displayBuffer.copy()
    softTabs = @getSoftTabs()
    newEditor = new Editor({@buffer, displayBuffer, tabLength, softTabs, suppressCursorCreation: true})
    newEditor.setScrollTop(@getScrollTop())
    newEditor.setScrollLeft(@getScrollLeft())
    for marker in @findMarkers(editorId: @id)
      marker.copy(editorId: newEditor.id, preserveFolds: true)
    atom.project.addEditor(newEditor)
    newEditor

  # Public: Get the title 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: ->
    if sessionPath = @getPath()
      path.basename(sessionPath)
    else
      'untitled'

  # Public: Get the editor's long title for display in other parts of the UI
  # such as the window title.
  #
  # If the editor's buffer is saved, its long title is formatted as
  # "<filename> - <directory>". If it is unsaved, its title is "untitled"
  #
  # Returns a {String}.
  getLongTitle: ->
    if sessionPath = @getPath()
      fileName = path.basename(sessionPath)
      directory = path.basename(path.dirname(sessionPath))
      "#{fileName} - #{directory}"
    else
      'untitled'

  # Controls visiblity based on the given {Boolean}.
  setVisible: (visible) -> @displayBuffer.setVisible(visible)

  # Called by {EditorView} when the scroll position changes so it can be
  # persisted across reloads.
  setScrollTop: (@scrollTop) -> @scrollTop

  getScrollTop: -> @scrollTop

  # Called by {EditorView} when the scroll position changes so it can be
  # persisted across reloads.
  setScrollLeft: (@scrollLeft) -> @scrollLeft

  getScrollLeft: -> @scrollLeft

  # Set the number of characters that can be displayed horizontally in the
  # editor.
  #
  # editorWidthInChars - A {Number} representing the width of the {EditorView}
  # in characters.
  setEditorWidthInChars: (editorWidthInChars) ->
    @displayBuffer.setEditorWidthInChars(editorWidthInChars)

  # Public: Sets the column at which columsn will soft wrap
  getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn()

  # Public: Returns a {Boolean} indicating whether softTabs are enabled for this
  # editor.
  getSoftTabs: -> @softTabs

  # Public: Enable or disable soft tabs for this editor.
  #
  # softTabs - A {Boolean}
  setSoftTabs: (@softTabs) -> @softTabs

  # Public: Get whether soft wrap is enabled for this editor.
  getSoftWrap: -> @displayBuffer.getSoftWrap()

  # Public: Enable or disable soft wrap for this editor.
  #
  # softWrap - A {Boolean}
  setSoftWrap: (softWrap) -> @displayBuffer.setSoftWrap(softWrap)

  # Public: 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: -> @buildIndentString(1)

  # Public: Get the on-screen length of tab characters.
  #
  # Returns a {Number}.
  getTabLength: -> @displayBuffer.getTabLength()

  # Public: Set the on-screen length of tab characters.
  setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength)

  # Public: 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.
  #
  # For example:
  #   * `[-1, -1]` is converted to `[0, 0]`.
  #   * If the line at row 2 is 10 long, `[2, Infinity]` is converted to
  #     `[2, 10]`.
  #
  # bufferPosition - The {Point} representing the position to clip.
  #
  # Returns a {Point}.
  clipBufferPosition: (bufferPosition) -> @buffer.clipPosition(bufferPosition)

  # Public: 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) -> @buffer.clipRange(range)

  # Public: Get the indentation level of the given a buffer row.
  #
  # Returns 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) ->
    @indentLevelForLine(@lineForBufferRow(bufferRow))

  # Public: 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.
  setIndentationForBufferRow: (bufferRow, newLevel) ->
    currentIndentLength = @lineForBufferRow(bufferRow).match(/^\s*/)[0].length
    newIndentString = @buildIndentString(newLevel)
    @buffer.change([[bufferRow, 0], [bufferRow, currentIndentLength]], newIndentString)

  # Public: Get the indentation level of the given line of text.
  #
  # Returns 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) ->
    if match = line.match(/^[\t ]+/)
      leadingWhitespace = match[0]
      tabCount = leadingWhitespace.match(/\t/g)?.length ? 0
      spaceCount = leadingWhitespace.match(/[ ]/g)?.length ? 0
      tabCount + (spaceCount / @getTabLength())
    else
      0

  # Constructs the string used for tabs.
  buildIndentString: (number) ->
    if @getSoftTabs()
      _.multiplyString(" ", number * @getTabLength())
    else
      _.multiplyString("\t", Math.floor(number))

  # {Delegates to: TextBuffer.save}
  save: -> @buffer.save()

  # {Delegates to: TextBuffer.saveAs}
  saveAs: (path) -> @buffer.saveAs(path)

  # {Delegates to: TextBuffer.getPath}
  getPath: -> @buffer.getPath()

  # Public: Returns a {String} representing the entire contents of the editor.
  getText: -> @buffer.getText()

  # Public: Replaces the entire contents of the buffer with the given {String}.
  setText: (text) -> @buffer.setText(text)

  # Get the text in the given {Range}.
  #
  # Returns a {String}.
  getTextInRange: (range) -> @buffer.getTextInRange(range)

  # Public: Returns a {Number} representing the number of lines in the editor.
  getLineCount: -> @buffer.getLineCount()

  # Retrieves the current {TextBuffer}.
  getBuffer: -> @buffer

  # Public: Retrieves the current buffer's URI.
  getUri: -> @buffer.getUri()

  # {Delegates to: TextBuffer.isRowBlank}
  isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow)

  # Public: Determine if the given row is entirely a comment
  isBufferRowCommented: (bufferRow) ->
    if match = @lineForBufferRow(bufferRow).match(/\S/)
      scopes = @tokenForBufferPosition([bufferRow, match.index]).scopes
      new TextMateScopeSelector('comment.*').matches(scopes)

  # {Delegates to: TextBuffer.nextNonBlankRow}
  nextNonBlankBufferRow: (bufferRow) -> @buffer.nextNonBlankRow(bufferRow)

  # {Delegates to: TextBuffer.getEofPosition}
  getEofBufferPosition: -> @buffer.getEofPosition()

  # Public: Returns a {Number} representing the last zero-indexed buffer row
  # number of the editor.
  getLastBufferRow: -> @buffer.getLastRow()

  # Returns the range for the given buffer row.
  #
  # row - A row {Number}.
  # options - An options hash with an `includeNewline` key.
  #
  # Returns a {Range}.
  bufferRangeForBufferRow: (row, options) -> @buffer.rangeForRow(row, options)

  # Public: Returns a {String} representing the contents of the line at the
  # given buffer row.
  #
  # row - A {Number} representing a zero-indexed buffer row.
  lineForBufferRow: (row) -> @buffer.lineForRow(row)

  # Public: Returns a {Number} representing the line length for the given
  # buffer row, exclusive of its line-ending character(s).
  #
  # row - A {Number} indicating the buffer row.
  lineLengthForBufferRow: (row) -> @buffer.lineLengthForRow(row)

  # {Delegates to: TextBuffer.scan}
  scan: (args...) -> @buffer.scan(args...)

  # {Delegates to: TextBuffer.scanInRange}
  scanInBufferRange: (args...) -> @buffer.scanInRange(args...)

  # {Delegates to: TextBuffer.backwardsScanInRange}
  backwardsScanInBufferRange: (args...) -> @buffer.backwardsScanInRange(args...)

  # {Delegates to: TextBuffer.isModified}
  isModified: -> @buffer.isModified()

  # Public: Determine whether the user should be prompted to save before closing
  # this editor.
  shouldPromptToSave: -> @isModified() and not @buffer.hasMultipleEditors()

  # Public: 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 - An options hash for {::clipScreenPosition}.
  #
  # Returns a {Point}.
  screenPositionForBufferPosition: (bufferPosition, options) -> @displayBuffer.screenPositionForBufferPosition(bufferPosition, options)

  # Public: 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 - An options hash for {::clipScreenPosition}.
  #
  # Returns a {Point}.
  bufferPositionForScreenPosition: (screenPosition, options) -> @displayBuffer.bufferPositionForScreenPosition(screenPosition, options)

  # Public: Convert a range in buffer-coordinates to screen-coordinates.
  #
  # Returns a {Range}.
  screenRangeForBufferRange: (bufferRange) -> @displayBuffer.screenRangeForBufferRange(bufferRange)

  # Public: Convert a range in screen-coordinates to buffer-coordinates.
  #
  # Returns a {Range}.
  bufferRangeForScreenRange: (screenRange) -> @displayBuffer.bufferRangeForScreenRange(screenRange)

  # Public: 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.
  #
  # For example:
  #   * `[-1, -1]` is converted to `[0, 0]`.
  #   * If the line at screen row 2 is 10 long, `[2, Infinity]` is converted to
  #     `[2, 10]`.
  #
  # bufferPosition - The {Point} representing the position to clip.
  #
  # Returns a {Point}.
  clipScreenPosition: (screenPosition, options) -> @displayBuffer.clipScreenPosition(screenPosition, options)

  # {Delegates to: DisplayBuffer.lineForRow}
  lineForScreenRow: (row) -> @displayBuffer.lineForRow(row)

  # {Delegates to: DisplayBuffer.linesForRows}
  linesForScreenRows: (start, end) -> @displayBuffer.linesForRows(start, end)

  # {Delegates to: DisplayBuffer.getLineCount}
  getScreenLineCount: -> @displayBuffer.getLineCount()

  # {Delegates to: DisplayBuffer.getMaxLineLength}
  getMaxScreenLineLength: -> @displayBuffer.getMaxLineLength()

  # {Delegates to: DisplayBuffer.getLastRow}
  getLastScreenRow: -> @displayBuffer.getLastRow()

  # {Delegates to: DisplayBuffer.bufferRowsForScreenRows}
  bufferRowsForScreenRows: (startRow, endRow) -> @displayBuffer.bufferRowsForScreenRows(startRow, endRow)

  bufferRowForScreenRow: (row) -> @displayBuffer.bufferRowForScreenRow(row)

  # Public: Get the syntactic scopes for the most the given position in buffer
  # coorditanates.
  #
  # For example, if called with a position inside the parameter list of an
  # anonymous CoffeeScript function, the method returns the following array:
  # `["source.coffee", "meta.inline.function.coffee", "variable.parameter.function.coffee"]`
  #
  # bufferPosition - A {Point} or {Array} of [row, column].
  #
  # Returns an {Array} of {String}s.
  scopesForBufferPosition: (bufferPosition) -> @displayBuffer.scopesForBufferPosition(bufferPosition)

  # Public: 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")`.
  #
  # Returns a {Range}.
  bufferRangeForScopeAtCursor: (selector) ->
    @displayBuffer.bufferRangeForScopeAtPosition(selector, @getCursorBufferPosition())

  # {Delegates to: DisplayBuffer.tokenForBufferPosition}
  tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition)

  # Public: Get the syntactic scopes for the most recently added cursor's
  # position. See {::scopesForBufferPosition} for more information.
  #
  # Returns an {Array} of {String}s.
  getCursorScopes: -> @getCursor().getScopes()

  # Public: For each selection, replace the selected text with the given text.
  #
  # text - A {String} representing the text to insert.
  # options - See {Selection::insertText}.
  insertText: (text, options={}) ->
    options.autoIndentNewline ?= @shouldAutoIndent()
    options.autoDecreaseIndent ?= @shouldAutoIndent()
    @mutateSelectedText (selection) -> selection.insertText(text, options)

  # Public: For each selection, replace the selected text with a newline.
  insertNewline: ->
    @insertText('\n')

  # Public: For each cursor, insert a newline at beginning the following line.
  insertNewlineBelow: ->
    @transact =>
      @moveCursorToEndOfLine()
      @insertNewline()

  # Public: For each cursor, insert a newline at the end of the preceding line.
  insertNewlineAbove: ->
    @transact =>
      onFirstLine = @getCursorBufferPosition().row is 0
      @moveCursorToBeginningOfLine()
      @moveCursorLeft()
      @insertNewline()
      @moveCursorUp() if onFirstLine

  # Indent all lines intersecting selections. See {Selection::indent} for more
  # information.
  indent: (options={})->
    options.autoIndent ?= @shouldAutoIndent()
    @mutateSelectedText (selection) -> selection.indent(options)

  # Public: For each selection, if the selection is empty, delete the character
  # preceding the cursor. Otherwise delete the selected text.
  backspace: ->
    @mutateSelectedText (selection) -> selection.backspace()

  # Public: For each selection, if the selection is empty, delete all characters
  # of the containing word that precede the cursor. Otherwise delete the
  # selected text.
  backspaceToBeginningOfWord: ->
    @mutateSelectedText (selection) -> selection.backspaceToBeginningOfWord()

  # Public: For each selection, if the selection is empty, delete all characters
  # of the containing line that precede the cursor. Otherwise delete the
  # selected text.
  backspaceToBeginningOfLine: ->
    @mutateSelectedText (selection) -> selection.backspaceToBeginningOfLine()

  # Public: For each selection, if the selection is empty, delete the character
  # preceding the cursor. Otherwise delete the selected text.
  delete: ->
    @mutateSelectedText (selection) -> selection.delete()

  # Public: For each selection, if the selection is empty, delete all characters
  # of the containing word following the cursor. Otherwise delete the selected
  # text.
  deleteToEndOfWord: ->
    @mutateSelectedText (selection) -> selection.deleteToEndOfWord()

  # Public: Delete all lines intersecting selections.
  deleteLine: ->
    @mutateSelectedText (selection) -> selection.deleteLine()

  # Public: Indent rows intersecting selections by one level.
  indentSelectedRows: ->
    @mutateSelectedText (selection) -> selection.indentSelectedRows()

  # Public: Outdent rows intersecting selections by one level.
  outdentSelectedRows: ->
    @mutateSelectedText (selection) -> selection.outdentSelectedRows()

  # Public: Toggle line comments for rows intersecting selections.
  #
  # If the current grammar doesn't support comments, does nothing.
  #
  # Returns an {Array} of the commented {Range}s.
  toggleLineCommentsInSelection: ->
    @mutateSelectedText (selection) -> selection.toggleLineComments()

  # Public: Indent rows intersecting selections based on the grammar's suggested
  # indent level.
  autoIndentSelectedRows: ->
    @mutateSelectedText (selection) -> selection.autoIndentSelectedRows()

  # If soft tabs are enabled, convert all hard tabs to soft tabs in the given
  # {Range}.
  normalizeTabsInBufferRange: (bufferRange) ->
    return unless @getSoftTabs()
    @scanInBufferRange /\t/, bufferRange, ({replace}) => replace(@getTabText())

  # Public: For each selection, if the selection is empty, cut all characters
  # of the containing line following the cursor. Otherwise cut the selected
  # text.
  cutToEndOfLine: ->
    maintainClipboard = false
    @mutateSelectedText (selection) ->
      selection.cutToEndOfLine(maintainClipboard)
      maintainClipboard = true

  # Public: For each selection, cut the selected text.
  cutSelectedText: ->
    maintainClipboard = false
    @mutateSelectedText (selection) ->
      selection.cut(maintainClipboard)
      maintainClipboard = true

  # Public: For each selection, copy the selected text.
  copySelectedText: ->
    maintainClipboard = false
    for selection in @getSelections()
      selection.copy(maintainClipboard)
      maintainClipboard = true

  # Public: For each selection, replace the selected text with the contents of
  # the clipboard.
  #
  # options - See {Selection::insertText}.
  pasteText: (options={}) ->
    {text, metadata} = atom.clipboard.readWithMetadata()

    containsNewlines = text.indexOf('\n') isnt -1

    if atom.config.get('editor.normalizeIndentOnPaste') and metadata
      if !@getCursor().hasPrecedingCharactersOnLine() or containsNewlines
        options.indentBasis ?= metadata.indentBasis

    @insertText(text, options)

  # Public: Undo the last change.
  undo: ->
    @getCursor().needsAutoscroll = true
    @buffer.undo(this)

  # Public: Redo the last change.
  redo: ->
    @getCursor().needsAutoscroll = true
    @buffer.redo(this)

  # Public: 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: ->
    bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row
    @foldBufferRow(bufferRow)

  # Public: Unfold the most recent cursor's row by one level.
  unfoldCurrentRow: ->
    bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row
    @unfoldBufferRow(bufferRow)

  # Public: For each selection, fold the rows it intersects.
  foldSelectedLines: ->
    selection.fold() for selection in @getSelections()

  # Public: Fold all foldable lines.
  foldAll: ->
    @languageMode.foldAll()

  # Public: Unfold all existing folds.
  unfoldAll: ->
    @languageMode.unfoldAll()

  # Public: Fold all foldable lines at the given indent level.
  #
  # level - A {Number}.
  foldAllAtIndentLevel: (level) ->
    @languageMode.foldAllAtIndentLevel(level)

  # Public: 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) ->
    @languageMode.foldBufferRow(bufferRow)

  # Public: Unfold all folds containing the given row in buffer coordinates.
  #
  # bufferRow - A {Number}
  unfoldBufferRow: (bufferRow) ->
    @displayBuffer.unfoldBufferRow(bufferRow)

  # Public: 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) ->
    @languageMode.isFoldableAtBufferRow(bufferRow)

  # TODO: Rename to foldRowRange?
  createFold: (startRow, endRow) ->
    @displayBuffer.createFold(startRow, endRow)

  # {Delegates to: DisplayBuffer.destroyFoldWithId}
  destroyFoldWithId: (id) ->
    @displayBuffer.destroyFoldWithId(id)

  # Remove any {Fold}s found that intersect the given buffer row.
  destroyFoldsIntersectingBufferRange: (bufferRange) ->
    for row in [bufferRange.start.row..bufferRange.end.row]
      @unfoldBufferRow(row)

  # Public: Fold the given buffer row if it isn't currently folded, and unfold
  # it otherwise.
  toggleFoldAtBufferRow: (bufferRow) ->
    if @isFoldedAtBufferRow(bufferRow)
      @unfoldBufferRow(bufferRow)
    else
      @foldBufferRow(bufferRow)

  # Public: Determine whether the most recently added cursor's row is folded.
  #
  # Returns a {Boolean}.
  isFoldedAtCursorRow: ->
    @isFoldedAtScreenRow(@getCursorScreenRow())

  # Public: Determine whether the given row in buffer coordinates is folded.
  #
  # bufferRow - A {Number}
  #
  # Returns a {Boolean}.
  isFoldedAtBufferRow: (bufferRow) ->
    @displayBuffer.isFoldedAtBufferRow(bufferRow)

  # Public: Determine whether the given row in screen coordinates is folded.
  #
  # screenRow - A {Number}
  #
  # Returns a {Boolean}.
  isFoldedAtScreenRow: (screenRow) ->
    @displayBuffer.isFoldedAtScreenRow(screenRow)

  # {Delegates to: DisplayBuffer.largestFoldContainingBufferRow}
  largestFoldContainingBufferRow: (bufferRow) ->
    @displayBuffer.largestFoldContainingBufferRow(bufferRow)

  # {Delegates to: DisplayBuffer.largestFoldStartingAtScreenRow}
  largestFoldStartingAtScreenRow: (screenRow) ->
    @displayBuffer.largestFoldStartingAtScreenRow(screenRow)

  # Move lines intersection the most recent selection up by one row in screen
  # coordinates.
  moveLineUp: ->
    selection = @getSelectedBufferRange()
    return if selection.start.row is 0
    lastRow = @buffer.getLastRow()
    return if selection.isEmpty() and selection.start.row is lastRow and @buffer.getLastLine() is ''

    @transact =>
      foldedRows = []
      rows = [selection.start.row..selection.end.row]
      if selection.start.row isnt selection.end.row and selection.end.column is 0
        rows.pop() unless @isFoldedAtBufferRow(selection.end.row)

      # Move line around the fold that is directly above the selection
      precedingScreenRow = @screenPositionForBufferPosition([selection.start.row]).translate([-1])
      precedingBufferRow = @bufferPositionForScreenPosition(precedingScreenRow).row
      if fold = @largestFoldContainingBufferRow(precedingBufferRow)
        insertDelta = fold.getBufferRange().getRowCount()
      else
        insertDelta = 1

      for row in rows
        if fold = @displayBuffer.largestFoldStartingAtBufferRow(row)
          bufferRange = fold.getBufferRange()
          startRow = bufferRange.start.row
          endRow = bufferRange.end.row
          foldedRows.push(startRow - insertDelta)
        else
          startRow = row
          endRow = row

        insertPosition = Point.fromObject([startRow - insertDelta])
        endPosition = Point.min([endRow + 1], @buffer.getEofPosition())
        lines = @buffer.getTextInRange([[startRow], endPosition])
        if endPosition.row is lastRow and endPosition.column > 0 and not @buffer.lineEndingForRow(endPosition.row)
          lines = "#{lines}\n"

        @buffer.deleteRows(startRow, endRow)

        # Make sure the inserted text doesn't go into an existing fold
        if fold = @displayBuffer.largestFoldStartingAtBufferRow(insertPosition.row)
          @unfoldBufferRow(insertPosition.row)
          foldedRows.push(insertPosition.row + endRow - startRow + fold.getBufferRange().getRowCount())

        @buffer.insert(insertPosition, lines)

      # Restore folds that existed before the lines were moved
      for foldedRow in foldedRows when 0 <= foldedRow <= @getLastBufferRow()
        @foldBufferRow(foldedRow)

      @setSelectedBufferRange(selection.translate([-insertDelta]), preserveFolds: true)

  # Move lines intersecting the most recent selection down by one row in screen
  # coordinates.
  moveLineDown: ->
    selection = @getSelectedBufferRange()
    lastRow = @buffer.getLastRow()
    return if selection.end.row is lastRow
    return if selection.end.row is lastRow - 1 and @buffer.getLastLine() is ''

    @transact =>
      foldedRows = []
      rows = [selection.end.row..selection.start.row]
      if selection.start.row isnt selection.end.row and selection.end.column is 0
        rows.shift() unless @isFoldedAtBufferRow(selection.end.row)

      # Move line around the fold that is directly below the selection
      followingScreenRow = @screenPositionForBufferPosition([selection.end.row]).translate([1])
      followingBufferRow = @bufferPositionForScreenPosition(followingScreenRow).row
      if fold = @largestFoldContainingBufferRow(followingBufferRow)
        insertDelta = fold.getBufferRange().getRowCount()
      else
        insertDelta = 1

      for row in rows
        if fold = @displayBuffer.largestFoldStartingAtBufferRow(row)
          bufferRange = fold.getBufferRange()
          startRow = bufferRange.start.row
          endRow = bufferRange.end.row
          foldedRows.push(endRow + insertDelta)
        else
          startRow = row
          endRow = row

        if endRow + 1 is lastRow
          endPosition = [endRow, @buffer.lineLengthForRow(endRow)]
        else
          endPosition = [endRow + 1]
        lines = @buffer.getTextInRange([[startRow], endPosition])
        @buffer.deleteRows(startRow, endRow)

        insertPosition = Point.min([startRow + insertDelta], @buffer.getEofPosition())
        if insertPosition.row is @buffer.getLastRow() and insertPosition.column > 0
          lines = "\n#{lines}"

        # Make sure the inserted text doesn't go into an existing fold
        if fold = @displayBuffer.largestFoldStartingAtBufferRow(insertPosition.row)
          @unfoldBufferRow(insertPosition.row)
          foldedRows.push(insertPosition.row + fold.getBufferRange().getRowCount())

        @buffer.insert(insertPosition, lines)

      # Restore folds that existed before the lines were moved
      for foldedRow in foldedRows when 0 <= foldedRow <= @getLastBufferRow()
        @foldBufferRow(foldedRow)

      @setSelectedBufferRange(selection.translate([insertDelta]), preserveFolds: true)

  # Duplicate the most recent cursor's current line.
  duplicateLine: ->
    return unless @getSelection().isEmpty()

    @transact =>
      cursorPosition = @getCursorBufferPosition()
      cursorRowFolded = @isFoldedAtCursorRow()
      if cursorRowFolded
        screenRow = @screenPositionForBufferPosition(cursorPosition).row
        bufferRange = @bufferRangeForScreenRange([[screenRow], [screenRow + 1]])
      else
        bufferRange = new Range([cursorPosition.row], [cursorPosition.row + 1])

      insertPosition = new Point(bufferRange.end.row)
      if insertPosition.row > @buffer.getLastRow()
        @unfoldCurrentRow() if cursorRowFolded
        @buffer.append("\n#{@getTextInBufferRange(bufferRange)}")
        @foldCurrentRow() if cursorRowFolded
      else
        @buffer.insert(insertPosition, @getTextInBufferRange(bufferRange))

      @setCursorScreenPosition(@getCursorScreenPosition().translate([1]))
      @foldCurrentRow() if cursorRowFolded

  mutateSelectedText: (fn) ->
    @transact => fn(selection) for selection in @getSelections()

  replaceSelectedText: (options={}, fn) ->
    {selectWordIfEmpty} = options
    @mutateSelectedText (selection) ->
      range = selection.getBufferRange()
      if selectWordIfEmpty and selection.isEmpty()
        selection.selectWord()
      text = selection.getText()
      selection.deleteSelectedText()
      selection.insertText(fn(text))
      selection.setBufferRange(range)

  # Public: Get the {DisplayBufferMarker} for the given marker id.
  getMarker: (id) ->
    @displayBuffer.getMarker(id)

  # Public: Get all {DisplayBufferMarker}s.
  getMarkers: ->
    @displayBuffer.getMarkers()

  # Public: Find all {DisplayBufferMarker}s 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.
  findMarkers: (properties) ->
    @displayBuffer.findMarkers(properties)

  # Public: Mark the given range in screen coordinates.
  #
  # range - A {Range} or range-compatible {Array}.
  # options - See {TextBuffer::markRange}.
  #
  # Returns a {DisplayBufferMarker}.
  markScreenRange: (args...) ->
    @displayBuffer.markScreenRange(args...)

  # Public: Mark the given range in buffer coordinates.
  #
  # range - A {Range} or range-compatible {Array}.
  # options - See {TextBuffer::markRange}.
  #
  # Returns a {DisplayBufferMarker}.
  markBufferRange: (args...) ->
    @displayBuffer.markBufferRange(args...)

  # Public: Mark the given position in screen coordinates.
  #
  # position - A {Point} or {Array} of `[row, column]`.
  # options - See {TextBuffer::markRange}.
  #
  # Returns a {DisplayBufferMarker}.
  markScreenPosition: (args...) ->
    @displayBuffer.markScreenPosition(args...)

  # Public: Mark the given position in buffer coordinates.
  #
  # position - A {Point} or {Array} of `[row, column]`.
  # options - See {TextBuffer::markRange}.
  #
  # Returns a {DisplayBufferMarker}.
  markBufferPosition: (args...) ->
    @displayBuffer.markBufferPosition(args...)

  # {Delegates to: DisplayBuffer.destroyMarker}
  destroyMarker: (args...) ->
    @displayBuffer.destroyMarker(args...)

  # Public: Get the number of markers in this editor's buffer.
  #
  # Returns a {Number}.
  getMarkerCount: ->
    @buffer.getMarkerCount()

  # Public: Determine if there are multiple cursors.
  hasMultipleCursors: ->
    @getCursors().length > 1

  # Public: Get an Array of all {Cursor}s.
  getCursors: -> new Array(@cursors...)

  # Public: Get the most recently added {Cursor}.
  getCursor: ->
    _.last(@cursors)

  # Public: Add a cursor at the position in screen coordinates.
  #
  # Returns a {Cursor}.
  addCursorAtScreenPosition: (screenPosition) ->
    @markScreenPosition(screenPosition, @getSelectionMarkerAttributes())
    @getLastSelection().cursor

  # Public: Add a cursor at the given position in buffer coordinates.
  #
  # Returns a {Cursor}.
  addCursorAtBufferPosition: (bufferPosition) ->
    @markBufferPosition(bufferPosition, @getSelectionMarkerAttributes())
    @getLastSelection().cursor

  # Add a cursor based on the given {DisplayBufferMarker}.
  addCursor: (marker) ->
    cursor = new Cursor(editor: this, marker: marker)
    @cursors.push(cursor)
    @emit 'cursor-added', cursor
    cursor

  # Remove the given cursor from this editor.
  removeCursor: (cursor) ->
    _.remove(@cursors, cursor)

  # Add a {Selection} based on the given {DisplayBufferMarker}.
  #
  # marker  - The {DisplayBufferMarker} to highlight
  # options - An {Object} that pertains to the {Selection} constructor.
  #
  # Returns the new {Selection}.
  addSelection: (marker, options={}) ->
    unless marker.getAttributes().preserveFolds
      @destroyFoldsIntersectingBufferRange(marker.getBufferRange())
    cursor = @addCursor(marker)
    selection = new Selection(_.extend({editor: this, marker, cursor}, options))
    @selections.push(selection)
    selectionBufferRange = selection.getBufferRange()
    @mergeIntersectingSelections()
    if selection.destroyed
      for selection in @getSelections()
        if selection.intersectsBufferRange(selectionBufferRange)
          return selection
    else
      @emit 'selection-added', selection
      selection

  # Public: Add a selection for the given range in buffer coordinates.
  #
  # bufferRange - A {Range}
  # options - An options {Object}:
  #   :reversed - A {Boolean} indicating whether to create the selection in a
  #     reversed orientation.
  #
  # Returns the added {Selection}.
  addSelectionForBufferRange: (bufferRange, options={}) ->
    @markBufferRange(bufferRange, _.defaults(@getSelectionMarkerAttributes(), options))
    @getLastSelection()

  # Public: 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 - An options {Object}:
  #   :reversed - A {Boolean} indicating whether to create the selection in a
  #     reversed orientation.
  setSelectedBufferRange: (bufferRange, options) ->
    @setSelectedBufferRanges([bufferRange], options)

  # Public: 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 - An options {Object}:
  #   :reversed - A {Boolean} indicating whether to create the selection in a
  #     reversed orientation.
  setSelectedBufferRanges: (bufferRanges, options={}) ->
    throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length

    selections = @getSelections()
    selection.destroy() for selection in selections[bufferRanges.length...]

    @mergeIntersectingSelections options, =>
      for bufferRange, i in bufferRanges
        bufferRange = Range.fromObject(bufferRange)
        if selections[i]
          selections[i].setBufferRange(bufferRange, options)
        else
          @addSelectionForBufferRange(bufferRange, options)

  # Remove the given selection.
  removeSelection: (selection) ->
    _.remove(@selections, selection)

  # Reduce one or more selections to a single empty selection based on the most
  # recently added cursor.
  clearSelections: ->
    @consolidateSelections()
    @getSelection().clear()

  # Reduce multiple selections to the most recently added selection.
  consolidateSelections: ->
    selections = @getSelections()
    if selections.length > 1
      selection.destroy() for selection in selections[0...-1]
      true
    else
      false

  # Public: Get current {Selection}s.
  #
  # Returns: An {Array} of {Selection}s.
  getSelections: -> new Array(@selections...)

  # Public: Get the most recent {Selection} or the selection at the given
  # index.
  #
  # index - Optional. The index of the selection to return, based on the order
  #   in which the selections were added.
  #
  # Returns a {Selection}.
  # or the  at the specified index.
  getSelection: (index) ->
    index ?= @selections.length - 1
    @selections[index]

  # Public: Get the most recently added {Selection}.
  #
  # Returns a {Selection}.
  getLastSelection: ->
    _.last(@selections)

  # Public: 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: ->
    @getSelections().sort (a, b) -> a.compare(b)

  # Public: Get the last {Selection} based on its position in the buffer.
  #
  # Returns a {Selection}.
  getLastSelectionInBuffer: ->
    _.last(@getSelectionsOrderedByBufferPosition())

  # Public: Determine if a given range in buffer coordinates intersects a
  # selection.
  #
  # bufferRange - A {Range} or range-comptatible {Array}.
  #
  # Returns a {Boolean}.
  selectionIntersectsBufferRange: (bufferRange) ->
    _.any @getSelections(), (selection) ->
      selection.intersectsBufferRange(bufferRange)

  # Public: 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  - 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) ->
    @moveCursors (cursor) -> cursor.setScreenPosition(position, options)

  # Public: Get the position of the most recently added cursor in screen
  # coordinates.
  #
  # Returns a {Point}.
  getCursorScreenPosition: ->
    @getCursor().getScreenPosition()

  # Public: Get the row of the most recently added cursor in screen coordinates.
  #
  # Returns the screen row {Number}.
  getCursorScreenRow: ->
    @getCursor().getScreenRow()

  # Public: 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  - An {Object} combining options for {::clipScreenPosition} with:
  #   :autoscroll - Determines whether the editor scrolls to the new cursor's
  #     position. Defaults to true.
  setCursorBufferPosition: (position, options) ->
    @moveCursors (cursor) -> cursor.setBufferPosition(position, options)

  # Public: Get the position of the most recently added cursor in buffer
  # coordinates.
  #
  # Returns a {Point}.
  getCursorBufferPosition: ->
    @getCursor().getBufferPosition()

  # Public: Get the {Range} of the most recently added selection in screen
  # coordinates.
  #
  # Returns a {Range}.
  getSelectedScreenRange: ->
    @getLastSelection().getScreenRange()

  # Public: Get the {Range} of the most recently added selection in buffer
  # coordinates.
  #
  # Returns a {Range}.
  getSelectedBufferRange: ->
    @getLastSelection().getBufferRange()

  # Public: Get the {Range}s of all selections in buffer coordinates.
  #
  # The ranges are sorted by their position in the buffer.
  #
  # Returns an {Array} of {Range}s.
  getSelectedBufferRanges: ->
    selection.getBufferRange() for selection in @getSelectionsOrderedByBufferPosition()

  # Public: Get the selected text of the most recently added selection.
  #
  # Returns a {String}.
  getSelectedText: ->
    @getLastSelection().getText()

  # Public: Get the text in the given {Range} in buffer coordinates.
  #
  # range - A {Range} or range-compatible {Array}.
  #
  # Returns a {String}.
  getTextInBufferRange: (range) ->
    @buffer.getTextInRange(range)

  # Public: Set the text in the given {Range} in buffer coordinates.
  #
  # range - A {Range} or range-compatible {Array}.
  # text - A {String}
  #
  # Returns the {Range} of the newly-inserted text.
  setTextInBufferRange: (range, text) -> @getBuffer().change(range, text)

  # Public: Get the {Range} of the paragraph surrounding the most recently added
  # cursor.
  #
  # Returns a {Range}.
  getCurrentParagraphBufferRange: ->
    @getCursor().getCurrentParagraphBufferRange()

  # Public: Returns the word surrounding the most recently added cursor.
  #
  # options - See {Cursor::getBeginningOfCurrentWordBufferPosition}.
  getWordUnderCursor: (options) ->
    @getTextInBufferRange(@getCursor().getCurrentWordBufferRange(options))

  # Public: Move every cursor up one row in screen coordinates.
  moveCursorUp: (lineCount) ->
    @moveCursors (cursor) -> cursor.moveUp(lineCount, moveToEndOfSelection: true)

  # Public: Move every cursor down one row in screen coordinates.
  moveCursorDown: (lineCount) ->
    @moveCursors (cursor) -> cursor.moveDown(lineCount, moveToEndOfSelection: true)

  # Public: Move every cursor left one column.
  moveCursorLeft: ->
    @moveCursors (cursor) -> cursor.moveLeft(moveToEndOfSelection: true)

  # Public: Move every cursor right one column.
  moveCursorRight: ->
    @moveCursors (cursor) -> cursor.moveRight(moveToEndOfSelection: true)

  # Public: Move every cursor to the top of the buffer.
  #
  # If there are multiple cursors, they will be merged into a single cursor.
  moveCursorToTop: ->
    @moveCursors (cursor) -> cursor.moveToTop()

  # Public: Move every cursor to the bottom of the buffer.
  #
  # If there are multiple cursors, they will be merged into a single cursor.
  moveCursorToBottom: ->
    @moveCursors (cursor) -> cursor.moveToBottom()

  # Public: Move every cursor to the beginning of its line in screen coordinates.
  moveCursorToBeginningOfScreenLine: ->
    @moveCursors (cursor) -> cursor.moveToBeginningOfScreenLine()

  # Public: Move every cursor to the beginning of its line in buffer coordinates.
  moveCursorToBeginningOfLine: ->
    @moveCursors (cursor) -> cursor.moveToBeginningOfLine()

  # Public: Move every cursor to the first non-whitespace character of its line.
  moveCursorToFirstCharacterOfLine: ->
    @moveCursors (cursor) -> cursor.moveToFirstCharacterOfLine()

  # Public: Move every cursor to the end of its line in screen coordinates.
  moveCursorToEndOfScreenLine: ->
    @moveCursors (cursor) -> cursor.moveToEndOfScreenLine()

  # Public: Move every cursor to the end of its line in buffer coordinates.
  moveCursorToEndOfLine: ->
    @moveCursors (cursor) -> cursor.moveToEndOfLine()

  # Public: Move every cursor to the beginning of its surrounding word.
  moveCursorToBeginningOfWord: ->
    @moveCursors (cursor) -> cursor.moveToBeginningOfWord()

  # Public: Move every cursor to the end of its surrounding word.
  moveCursorToEndOfWord: ->
    @moveCursors (cursor) -> cursor.moveToEndOfWord()

  # Public: Move every cursor to the beginning of the next word.
  moveCursorToBeginningOfNextWord: ->
    @moveCursors (cursor) -> cursor.moveToBeginningOfNextWord()

  # Public: Move every cursor to the previous word boundary.
  moveCursorToPreviousWordBoundary: ->
    @moveCursors (cursor) -> cursor.moveToPreviousWordBoundary()

  # Public: Move every cursor to the next word boundary.
  moveCursorToNextWordBoundary: ->
    @moveCursors (cursor) -> cursor.moveToNextWordBoundary()

  moveCursors: (fn) ->
    fn(cursor) for cursor in @getCursors()
    @mergeCursors()

  # Public: Select from the current cursor position to the given position in
  # screen coordinates.
  #
  # This method may merge selections that end up intesecting.
  #
  # position - An instance of {Point}, with a given `row` and `column`.
  selectToScreenPosition: (position) ->
    lastSelection = @getLastSelection()
    lastSelection.selectToScreenPosition(position)
    @mergeIntersectingSelections(isReversed: lastSelection.isReversed())

  # Public: Move the cursor of each selection one character rightward while
  # preserving the selection's tail position.
  #
  # This method may merge selections that end up intesecting.
  selectRight: ->
    @expandSelectionsForward (selection) => selection.selectRight()

  # Public: Move the cursor of each selection one character leftward while
  # preserving the selection's tail position.
  #
  # This method may merge selections that end up intesecting.
  selectLeft: ->
    @expandSelectionsBackward (selection) => selection.selectLeft()

  # Public: Move the cursor of each selection one character upward while
  # preserving the selection's tail position.
  #
  # This method may merge selections that end up intesecting.
  selectUp: (rowCount) ->
    @expandSelectionsBackward (selection) => selection.selectUp(rowCount)

  # Public: Move the cursor of each selection one character downward while
  # preserving the selection's tail position.
  #
  # This method may merge selections that end up intesecting.
  selectDown: (rowCount) ->
    @expandSelectionsForward (selection) => selection.selectDown(rowCount)

  # Public: 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: ->
    @expandSelectionsBackward (selection) => selection.selectToTop()

  # Public: Select all text in the buffer.
  #
  # This method merges multiple selections into a single selection.
  selectAll: ->
    @expandSelectionsForward (selection) => selection.selectAll()

  # Public: 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: ->
    @expandSelectionsForward (selection) => selection.selectToBottom()

  # Public: 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 intesecting.
  selectToBeginningOfLine: ->
    @expandSelectionsBackward (selection) => selection.selectToBeginningOfLine()

  # Public: 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 intesecting.
  selectToFirstCharacterOfLine: ->
    @expandSelectionsBackward (selection) => selection.selectToFirstCharacterOfLine()

  # Public: 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 intesecting.
  selectToEndOfLine: ->
    @expandSelectionsForward (selection) => selection.selectToEndOfLine()

  # Public: 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 intesecting.
  selectToPreviousWordBoundary: ->
    @expandSelectionsBackward (selection) => selection.selectToPreviousWordBoundary()

  # Public: 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 intesecting.
  selectToNextWordBoundary: ->
    @expandSelectionsForward (selection) => selection.selectToNextWordBoundary()

  # Public: For each cursor, select the containing line.
  #
  # This method merges selections on successive lines.
  selectLine: ->
    @expandSelectionsForward (selection) => selection.selectLine()

  # Public: Add a similarly-shaped selection to the next elibible 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: ->
    @expandSelectionsForward (selection) => selection.addSelectionBelow()

  # Public: Add a similarly-shaped selection to the next elibible 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: ->
    @expandSelectionsBackward (selection) => selection.addSelectionAbove()

  # Public: 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: ->
    for selection in @getSelections()
      range = selection.getBufferRange()
      continue if range.isSingleLine()

      selection.destroy()
      {start, end} = range
      @addSelectionForBufferRange([start, [start.row, Infinity]])
      {row} = start
      while ++row < end.row
        @addSelectionForBufferRange([[row, 0], [row, Infinity]])
      @addSelectionForBufferRange([[end.row, 0], [end.row, end.column]])

  # Public: 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.
  transpose: ->
    @mutateSelectedText (selection) =>
      if selection.isEmpty()
        selection.selectRight()
        text = selection.getText()
        selection.delete()
        selection.cursor.moveLeft()
        selection.insertText text
      else
        selection.insertText selection.getText().split('').reverse().join('')

  # Public: 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.
  upperCase: ->
    @replaceSelectedText selectWordIfEmpty:true, (text) => text.toUpperCase()

  # Public: 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.
  lowerCase: ->
    @replaceSelectedText selectWordIfEmpty:true, (text) => text.toLowerCase()

  # 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.
  joinLines: ->
    @mutateSelectedText (selection) -> selection.joinLines()

  # Public: 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: ->
    @expandSelectionsBackward (selection) => selection.selectToBeginningOfWord()

  # Public: 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: ->
    @expandSelectionsForward (selection) => selection.selectToEndOfWord()

  # Public: 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: ->
    @expandSelectionsForward (selection) => selection.selectToBeginningOfNextWord()

  # Public: Select the word containing each cursor.
  selectWord: ->
    @expandSelectionsForward (selection) => selection.selectWord()

  # Public: Select the range of the given marker if it is valid.
  #
  # marker - A {DisplayBufferMarker}
  #
  # Returns the selected {Range} or `undefined` if the marker is invalid.
  selectMarker: (marker) ->
    if marker.isValid()
      range = marker.getBufferRange()
      @setSelectedBufferRange(range)
      range

  # Merge cursors that have the same screen position
  mergeCursors: ->
    positions = []
    for cursor in @getCursors()
      position = cursor.getBufferPosition().toString()
      if position in positions
        cursor.destroy()
      else
        positions.push(position)

  # Calls the given function with each selection, then merges selections
  expandSelectionsForward: (fn) ->
    @mergeIntersectingSelections =>
      fn(selection) for selection in @getSelections()

  # Calls the given function with each selection, then merges selections in the
  # reversed orientation
  expandSelectionsBackward: (fn) ->
    @mergeIntersectingSelections isReversed: true, =>
      fn(selection) for selection in @getSelections()

  finalizeSelections: ->
    selection.finalize() for selection in @getSelections()

  # Merges intersecting selections. If passed a function, it executes
  # the function with merging suppressed, then merges intersecting selections
  # afterward.
  mergeIntersectingSelections: (args...) ->
    fn = args.pop() if _.isFunction(_.last(args))
    options = args.pop() ? {}

    return fn?() if @suppressSelectionMerging

    if fn?
      @suppressSelectionMerging = true
      result = fn()
      @suppressSelectionMerging = false

    reducer = (disjointSelections, selection) ->
      intersectingSelection = _.find(disjointSelections, (s) -> s.intersectsWith(selection))
      if intersectingSelection?
        intersectingSelection.merge(selection, options)
        disjointSelections
      else
        disjointSelections.concat([selection])

    _.reduce(@getSelections(), reducer, [])

  preserveCursorPositionOnBufferReload: ->
    cursorPosition = null
    @subscribe @buffer, "will-reload", =>
      cursorPosition = @getCursorBufferPosition()
    @subscribe @buffer, "reloaded", =>
      @setCursorBufferPosition(cursorPosition) if cursorPosition
      cursorPosition = null

  # Public: Get the current {Grammar} of this editor.
  getGrammar: ->
    @displayBuffer.getGrammar()

  # Public: Set the current {Grammar} of this editor.
  #
  # Assigning a grammar will cause the editor to re-tokenize based on the new
  # grammar.
  setGrammar: (grammar) ->
    @displayBuffer.setGrammar(grammar)

  # Reload the grammar based on the file name.
  reloadGrammar: ->
    @displayBuffer.reloadGrammar()

  shouldAutoIndent: ->
    atom.config.get("editor.autoIndent")

  # 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.
  #
  # fn - A {Function} to call inside the transaction.
  transact: (fn) -> @buffer.transact(fn)

  # Public: Start an open-ended transaction.
  #
  # Call {::commitTransaction} or {::abortTransaction} to terminate the
  # transaction. If you nest calls to transactions, only the outermost
  # transaction is considered. You must match every begin with a matching
  # commit, but a single call to abort will cancel all nested transactions.
  beginTransaction: -> @buffer.beginTransaction()

  # Public: Commit an open-ended transaction started with {::beginTransaction}
  # and push it to the undo stack.
  #
  # If transactions are nested, only the outermost commit takes effect.
  commitTransaction: -> @buffer.commitTransaction()

  # Public: Abort an open transaction, undoing any operations performed so far
  # within the transaction.
  abortTransaction: -> @buffer.abortTransaction()

  inspect: ->
    "<Editor #{@id}>"

  logScreenLines: (start, end) -> @displayBuffer.logLines(start, end)

  handleGrammarChange: ->
    @unfoldAll()
    @emit 'grammar-changed'

  handleMarkerCreated: (marker) =>
    if marker.matchesAttributes(@getSelectionMarkerAttributes())
      @addSelection(marker)

  getSelectionMarkerAttributes: ->
    type: 'selection', editorId: @id, invalidate: 'never'
