{View, $, $$$} = require './space-pen-extensions'
GutterView = require './gutter-view'
{Point, Range} = require 'text-buffer'
Editor = require './editor'
CursorView = require './cursor-view'
SelectionView = require './selection-view'
fs = require 'fs-plus'
_ = require 'underscore-plus'
TextBuffer = require 'text-buffer'

MeasureRange = document.createRange()
TextNodeFilter = { acceptNode: -> NodeFilter.FILTER_ACCEPT }
NoScope = ['no-scope']
LongLineLength = 1000

# Public: Represents the entire visual pane in Atom.
#
# The EditorView manages the {Editor}, which manages the file buffers.
#
# ## Requiring in packages
#
# ```coffee
#   {EditorView} = require 'atom'
# ```
module.exports =
class EditorView extends View
  @characterWidthCache: {}
  @configDefaults:
    fontFamily: ''
    fontSize: 16
    showInvisibles: false
    showIndentGuide: false
    showLineNumbers: true
    autoIndent: true
    normalizeIndentOnPaste: true
    nonWordCharacters: "./\\()\"':,.;<>~!@#$%^&*|+=[]{}`~?-"
    preferredLineLength: 80
    tabLength: 2
    softWrap: false
    softTabs: true
    softWrapAtPreferredLineLength: false

  @nextEditorId: 1

  @content: (params) ->
    attributes = { class: @classes(params), tabindex: -1 }
    _.extend(attributes, params.attributes) if params.attributes
    @div attributes, =>
      @subview 'gutter', new GutterView
      @div class: 'scroll-view', outlet: 'scrollView', =>
        @div class: 'overlayer', outlet: 'overlayer'
        @div class: 'lines', outlet: 'renderedLines'
        @div class: 'underlayer', outlet: 'underlayer', =>
          @input class: 'hidden-input', outlet: 'hiddenInput'
      @div class: 'vertical-scrollbar', outlet: 'verticalScrollbar', =>
        @div outlet: 'verticalScrollbarContent'

  @classes: ({mini} = {}) ->
    classes = ['editor', 'editor-colors']
    classes.push 'mini' if mini
    classes.join(' ')

  vScrollMargin: 2
  hScrollMargin: 10
  lineHeight: null
  charWidth: null
  charHeight: null
  cursorViews: null
  selectionViews: null
  lineCache: null
  isFocused: false
  editor: null
  attached: false
  lineOverdraw: 10
  pendingChanges: null
  newCursors: null
  newSelections: null
  redrawOnReattach: false
  bottomPaddingInLines: 10

  # The constructor for setting up an `EditorView` instance.
  #
  # editorOrOptions - Either an {Editor}, or an object with one property, `mini`.
  #                   If `mini` is `true`, a "miniature" `Editor` is constructed.
  #                   Typically, this is ideal for scenarios where you need an Atom editor,
  #                   but without all the chrome, like scrollbars, gutter, _e.t.c._.
  #
  initialize: (editorOrOptions) ->
    if editorOrOptions instanceof Editor
      editor = editorOrOptions
    else
      {editor, @mini, placeholderText} = editorOrOptions ? {}

    @id = EditorView.nextEditorId++
    @lineCache = []
    @configure()
    @bindKeys()
    @handleEvents()
    @handleInputEvents()
    @cursorViews = []
    @selectionViews = []
    @pendingChanges = []
    @newCursors = []
    @newSelections = []

    @setPlaceholderText(placeholderText) if placeholderText

    if editor?
      @edit(editor)
    else if @mini
      @edit(new Editor
        buffer: new TextBuffer
        softWrap: false
        tabLength: 2
        softTabs: true
      )
    else
      throw new Error("Must supply an Editor or mini: true")

  # Sets up the core Atom commands.
  #
  # Some commands are excluded from mini-editors.
  bindKeys: ->
    editorBindings =
      'core:move-left': => @editor.moveCursorLeft()
      'core:move-right': => @editor.moveCursorRight()
      'core:select-left': => @editor.selectLeft()
      'core:select-right': => @editor.selectRight()
      'core:select-all': => @editor.selectAll()
      'core:backspace': => @editor.backspace()
      'core:delete': => @editor.delete()
      'core:undo': => @editor.undo()
      'core:redo': => @editor.redo()
      'core:cut': => @editor.cutSelectedText()
      'core:copy': => @editor.copySelectedText()
      'core:paste': => @editor.pasteText()
      'editor:move-to-previous-word': => @editor.moveCursorToPreviousWord()
      'editor:select-word': => @editor.selectWord()
      'editor:consolidate-selections': (event) => @consolidateSelections(event)
      'editor:backspace-to-beginning-of-word': => @editor.backspaceToBeginningOfWord()
      'editor:backspace-to-beginning-of-line': => @editor.backspaceToBeginningOfLine()
      'editor:delete-to-end-of-word': => @editor.deleteToEndOfWord()
      'editor:delete-line': => @editor.deleteLine()
      'editor:cut-to-end-of-line': => @editor.cutToEndOfLine()
      'editor:move-to-beginning-of-screen-line': => @editor.moveCursorToBeginningOfScreenLine()
      'editor:move-to-beginning-of-line': => @editor.moveCursorToBeginningOfLine()
      'editor:move-to-end-of-screen-line': => @editor.moveCursorToEndOfScreenLine()
      'editor:move-to-end-of-line': => @editor.moveCursorToEndOfLine()
      'editor:move-to-first-character-of-line': => @editor.moveCursorToFirstCharacterOfLine()
      'editor:move-to-beginning-of-word': => @editor.moveCursorToBeginningOfWord()
      'editor:move-to-end-of-word': => @editor.moveCursorToEndOfWord()
      'editor:move-to-beginning-of-next-word': => @editor.moveCursorToBeginningOfNextWord()
      'editor:move-to-previous-word-boundary': => @editor.moveCursorToPreviousWordBoundary()
      'editor:move-to-next-word-boundary': => @editor.moveCursorToNextWordBoundary()
      'editor:select-to-end-of-line': => @editor.selectToEndOfLine()
      'editor:select-to-beginning-of-line': => @editor.selectToBeginningOfLine()
      'editor:select-to-end-of-word': => @editor.selectToEndOfWord()
      'editor:select-to-beginning-of-word': => @editor.selectToBeginningOfWord()
      'editor:select-to-beginning-of-next-word': => @editor.selectToBeginningOfNextWord()
      'editor:select-to-next-word-boundary': => @editor.selectToNextWordBoundary()
      'editor:select-to-previous-word-boundary': => @editor.selectToPreviousWordBoundary()
      'editor:select-to-first-character-of-line': => @editor.selectToFirstCharacterOfLine()
      'editor:select-line': => @editor.selectLine()
      'editor:transpose': => @editor.transpose()
      'editor:upper-case': => @editor.upperCase()
      'editor:lower-case': => @editor.lowerCase()

    unless @mini
      _.extend editorBindings,
        'core:move-up': => @editor.moveCursorUp()
        'core:move-down': => @editor.moveCursorDown()
        'core:move-to-top': => @editor.moveCursorToTop()
        'core:move-to-bottom': => @editor.moveCursorToBottom()
        'core:page-down': => @pageDown()
        'core:page-up': => @pageUp()
        'core:select-up': => @editor.selectUp()
        'core:select-down': => @editor.selectDown()
        'core:select-to-top': => @editor.selectToTop()
        'core:select-to-bottom': => @editor.selectToBottom()
        'editor:indent': => @editor.indent()
        'editor:auto-indent': => @editor.autoIndentSelectedRows()
        'editor:indent-selected-rows': => @editor.indentSelectedRows()
        'editor:outdent-selected-rows': => @editor.outdentSelectedRows()
        'editor:newline': => @editor.insertNewline()
        'editor:newline-below': => @editor.insertNewlineBelow()
        'editor:newline-above': => @editor.insertNewlineAbove()
        'editor:add-selection-below': => @editor.addSelectionBelow()
        'editor:add-selection-above': => @editor.addSelectionAbove()
        'editor:split-selections-into-lines': => @editor.splitSelectionsIntoLines()
        'editor:toggle-soft-tabs': => @toggleSoftTabs()
        'editor:toggle-soft-wrap': => @toggleSoftWrap()
        'editor:fold-all': => @editor.foldAll()
        'editor:unfold-all': => @editor.unfoldAll()
        'editor:fold-current-row': => @editor.foldCurrentRow()
        'editor:unfold-current-row': => @editor.unfoldCurrentRow()
        'editor:fold-selection': => @editor.foldSelectedLines()
        'editor:fold-at-indent-level-1': => @editor.foldAllAtIndentLevel(0)
        'editor:fold-at-indent-level-2': => @editor.foldAllAtIndentLevel(1)
        'editor:fold-at-indent-level-3': => @editor.foldAllAtIndentLevel(2)
        'editor:fold-at-indent-level-4': => @editor.foldAllAtIndentLevel(3)
        'editor:fold-at-indent-level-5': => @editor.foldAllAtIndentLevel(4)
        'editor:fold-at-indent-level-6': => @editor.foldAllAtIndentLevel(5)
        'editor:fold-at-indent-level-7': => @editor.foldAllAtIndentLevel(6)
        'editor:fold-at-indent-level-8': => @editor.foldAllAtIndentLevel(7)
        'editor:fold-at-indent-level-9': => @editor.foldAllAtIndentLevel(8)
        'editor:toggle-line-comments': => @toggleLineCommentsInSelection()
        'editor:log-cursor-scope': => @logCursorScope()
        'editor:checkout-head-revision': => @checkoutHead()
        'editor:copy-path': => @copyPathToClipboard()
        'editor:move-line-up': => @editor.moveLineUp()
        'editor:move-line-down': => @editor.moveLineDown()
        'editor:duplicate-line': => @editor.duplicateLine()
        'editor:join-lines': => @editor.joinLines()
        'editor:toggle-indent-guide': => atom.config.toggle('editor.showIndentGuide')
        'editor:toggle-line-numbers': =>  atom.config.toggle('editor.showLineNumbers')
        'editor:scroll-to-cursor': => @scrollToCursorPosition()

    documentation = {}
    for name, method of editorBindings
      do (name, method) =>
        @command name, (e) -> method(e); false

  # Public: Get the underlying editor model for this view.
  #
  # Returns an {Editor}.
  getEditor: ->
    @editor

  # {Delegates to: Editor.getText}
  getText: ->
    @editor.getText()

  # {Delegates to: Editor.setText}
  setText: (text) ->
    @editor.setText(text)

  # {Delegates to: Editor.insertText}
  insertText: (text, options) ->
    @editor.insertText(text, options)

  setHeightInLines: (heightInLines)->
    heightInLines ?= @calculateHeightInLines()
    @heightInLines = heightInLines if heightInLines

  # {Delegates to: Editor.setEditorWidthInChars}
  setWidthInChars: (widthInChars) ->
    widthInChars ?= @calculateWidthInChars()
    @editor.setEditorWidthInChars(widthInChars) if widthInChars

  # Public: Emulates the "page down" key, where the last row of a buffer scrolls
  # to become the first.
  pageDown: ->
    newScrollTop = @scrollTop() + @scrollView[0].clientHeight
    @editor.moveCursorDown(@getPageRows())
    @scrollTop(newScrollTop,  adjustVerticalScrollbar: true)

  # Public: Emulates the "page up" key, where the frst row of a buffer scrolls
  # to become the last.
  pageUp: ->
    newScrollTop = @scrollTop() - @scrollView[0].clientHeight
    @editor.moveCursorUp(@getPageRows())
    @scrollTop(newScrollTop,  adjustVerticalScrollbar: true)

  # Public: Gets the number of actual page rows existing in an editor.
  #
  # Returns a {Number}.
  getPageRows: ->
    Math.max(1, Math.ceil(@scrollView[0].clientHeight / @lineHeight))

  # Public: Set whether invisible characters are shown.
  #
  # showInvisibles - A {Boolean} which, if `true`, show invisible characters.
  setShowInvisibles: (showInvisibles) ->
    return if showInvisibles == @showInvisibles
    @showInvisibles = showInvisibles
    @resetDisplay()

  # Public: Defines which characters are invisible.
  #
  # invisibles - An {Object} defining the invisible characters:
  #   :eol   - The end of line invisible {String} (default: `\u00ac`).
  #   :space - The space invisible {String} (default: `\u00b7`).
  #   :tab   - The tab invisible {String} (default: `\u00bb`).
  #   :cr    - The carriage return invisible {String} (default: `\u00a4`).
  setInvisibles: (@invisibles={}) ->
    _.defaults @invisibles,
      eol: '\u00ac'
      space: '\u00b7'
      tab: '\u00bb'
      cr: '\u00a4'
    @resetDisplay()

  # Public: Sets whether you want to show the indentation guides.
  #
  # showIndentGuide - A {Boolean} you can set to `true` if you want to see the
  #                   indentation guides.
  setShowIndentGuide: (showIndentGuide) ->
    return if showIndentGuide == @showIndentGuide
    @showIndentGuide = showIndentGuide
    @resetDisplay()

  # Public: Set the text to appear in the editor when it is empty.
  #
  # This only affects mini editors.
  #
  # placeholderText - A {String} of text to display when empty.
  setPlaceholderText: (placeholderText) ->
    return unless @mini
    @placeholderText = placeholderText
    @requestDisplayUpdate()

  getPlaceholderText: ->
    @placeholderText

  # Checkout the HEAD revision of this editor's file.
  checkoutHead: ->
    if path = @editor.getPath()
      atom.project.getRepo()?.checkoutHead(path)

  configure: ->
    @subscribe atom.config.observe 'editor.showLineNumbers', (showLineNumbers) => @gutter.setShowLineNumbers(showLineNumbers)
    @subscribe atom.config.observe 'editor.showInvisibles', (showInvisibles) => @setShowInvisibles(showInvisibles)
    @subscribe atom.config.observe 'editor.showIndentGuide', (showIndentGuide) => @setShowIndentGuide(showIndentGuide)
    @subscribe atom.config.observe 'editor.invisibles', (invisibles) => @setInvisibles(invisibles)
    @subscribe atom.config.observe 'editor.fontSize', (fontSize) => @setFontSize(fontSize)
    @subscribe atom.config.observe 'editor.fontFamily', (fontFamily) => @setFontFamily(fontFamily)

  handleEvents: ->
    @on 'focus', =>
      @hiddenInput.focus()
      false

    @hiddenInput.on 'focus', =>
      @bringHiddenInputIntoView()
      @isFocused = true
      @addClass 'is-focused'

    @hiddenInput.on 'focusout', =>
      @bringHiddenInputIntoView()
      @isFocused = false
      @removeClass 'is-focused'

    @underlayer.on 'mousedown', (e) =>
      @renderedLines.trigger(e)
      false if @isFocused

    @overlayer.on 'mousedown', (e) =>
      @overlayer.hide()
      clickedElement = document.elementFromPoint(e.pageX, e.pageY)
      @overlayer.show()
      e.target = clickedElement
      $(clickedElement).trigger(e)
      false if @isFocused

    @renderedLines.on 'mousedown', '.fold.line', (e) =>
      id = $(e.currentTarget).attr('fold-id')
      marker = @editor.displayBuffer.getMarker(id)
      @editor.setCursorBufferPosition(marker.getBufferRange().start)
      @editor.destroyFoldWithId(id)
      false

    @gutter.on 'mousedown', '.foldable .icon-right', (e) =>
      bufferRow = $(e.target).parent().data('bufferRow')
      @editor.toggleFoldAtBufferRow(bufferRow)
      false

    @renderedLines.on 'mousedown', (e) =>
      clickCount = e.originalEvent.detail

      screenPosition = @screenPositionFromMouseEvent(e)
      if clickCount == 1
        if e.metaKey
          @editor.addCursorAtScreenPosition(screenPosition)
        else if e.shiftKey
          @editor.selectToScreenPosition(screenPosition)
        else
          @editor.setCursorScreenPosition(screenPosition)
      else if clickCount == 2
        @editor.selectWord() unless e.shiftKey
      else if clickCount == 3
        @editor.selectLine() unless e.shiftKey

      @selectOnMousemoveUntilMouseup() unless e.ctrlKey or e.originalEvent.which > 1

    unless @mini
      @scrollView.on 'mousewheel', (e) =>
        if delta = e.originalEvent.wheelDeltaY
          @scrollTop(@scrollTop() - delta)
          false

    @verticalScrollbar.on 'scroll', =>
      @scrollTop(@verticalScrollbar.scrollTop(), adjustVerticalScrollbar: false)

    @scrollView.on 'scroll', =>
      if @scrollLeft() == 0
        @gutter.removeClass('drop-shadow')
      else
        @gutter.addClass('drop-shadow')

    # Listen for overflow events to detect when the editor's width changes
    # to update the soft wrap column.
    updateWidthInChars = _.debounce((=> @setWidthInChars()), 100)
    @scrollView.on 'overflowchanged', =>
      updateWidthInChars() if @[0].classList.contains('soft-wrap')

  handleInputEvents: ->
    @on 'cursor:moved', =>
      return unless @isFocused
      cursorView = @getCursorView()

      if cursorView.isVisible()
        # This is an order of magnitude faster than checking .offset().
        style = cursorView[0].style
        @hiddenInput[0].style.top = style.top
        @hiddenInput[0].style.left = style.left

    selectedText = null
    @hiddenInput.on 'compositionstart', =>
      selectedText = @editor.getSelectedText()
      @hiddenInput.css('width', '100%')
    @hiddenInput.on 'compositionupdate', (e) =>
      @editor.insertText(e.originalEvent.data, {select: true, undo: 'skip'})
    @hiddenInput.on 'compositionend', =>
      @editor.insertText(selectedText, {select: true, undo: 'skip'})
      @hiddenInput.css('width', '1px')

    lastInput = ''
    @on "textInput", (e) =>
      # Work around of the accented character suggestion feature in OS X.
      selectedLength = @hiddenInput[0].selectionEnd - @hiddenInput[0].selectionStart
      if selectedLength is 1 and lastInput is @hiddenInput.val()
        @editor.selectLeft()

      lastInput = e.originalEvent.data
      @editor.insertText(lastInput)

      if lastInput is ' '
        true # Prevents parent elements from scrolling when a space is typed
      else
        @hiddenInput.val(lastInput)
        false

  bringHiddenInputIntoView: ->
    @hiddenInput.css(top: @scrollTop(), left: @scrollLeft())

  selectOnMousemoveUntilMouseup: ->
    lastMoveEvent = null
    moveHandler = (event = lastMoveEvent) =>
      if event
        @editor.selectToScreenPosition(@screenPositionFromMouseEvent(event))
        lastMoveEvent = event

    $(document).on "mousemove.editor-#{@id}", moveHandler
    interval = setInterval(moveHandler, 20)

    $(document).one "mouseup.editor-#{@id}", =>
      clearInterval(interval)
      $(document).off 'mousemove', moveHandler
      @editor.mergeIntersectingSelections(isReversed: @editor.getLastSelection().isReversed())
      @editor.finalizeSelections()
      @syncCursorAnimations()

  afterAttach: (onDom) ->
    return unless onDom

    # TODO: Remove this guard when we understand why this is happening
    unless @editor.isAlive()
      if atom.isReleasedVersion()
        return
      else
        throw new Error("Assertion failure: EditorView is getting attached to a dead editor. Why?")

    @redraw() if @redrawOnReattach
    return if @attached
    @attached = true
    @calculateDimensions()
    @setWidthInChars()
    @subscribe $(window), "resize.editor-#{@id}", =>
      @setHeightInLines()
      @setWidthInChars()
      @updateLayerDimensions()
      @requestDisplayUpdate()
    @focus() if @isFocused

    if pane = @getPane()
      @active = @is(pane.activeView)
      @subscribe pane, 'pane:active-item-changed', (event, item) =>
        wasActive = @active
        @active = @is(pane.activeView)
        @redraw() if @active and not wasActive

    @resetDisplay()

    @trigger 'editor:attached', [this]

  edit: (editor) ->
    return if editor is @editor

    if @editor
      @saveScrollPositionForEditor()
      @editor.off(".editor")

    @editor = editor

    return unless @editor?

    @editor.setVisible(true)

    @editor.on "destroyed", =>
      @remove()

    @editor.on "contents-conflicted.editor", =>
      @showBufferConflictAlert(@editor)

    @editor.on "path-changed.editor", =>
      @editor.reloadGrammar()
      @trigger 'editor:path-changed'

    @editor.on "grammar-changed.editor", =>
      @trigger 'editor:grammar-changed'

    @editor.on 'selection-added.editor', (selection) =>
      @newCursors.push(selection.cursor)
      @newSelections.push(selection)
      @requestDisplayUpdate()

    @editor.on 'screen-lines-changed.editor', (e) =>
      @handleScreenLinesChange(e)

    @editor.on 'scroll-top-changed.editor', (scrollTop) =>
      @scrollTop(scrollTop)

    @editor.on 'scroll-left-changed.editor', (scrollLeft) =>
      @scrollLeft(scrollLeft)

    @editor.on 'soft-wrap-changed.editor', (softWrap) =>
      @setSoftWrap(softWrap)

    @trigger 'editor:path-changed'
    @resetDisplay()

    if @attached and @editor.buffer.isInConflict()
      _.defer => @showBufferConflictAlert(@editor) # Display after editor has a chance to display

  getModel: ->
    @editor

  setModel: (editor) ->
    @edit(editor)

  showBufferConflictAlert: (editor) ->
    atom.confirm
      message: editor.getPath()
      detailedMessage: "Has changed on disk. Do you want to reload it?"
      buttons:
        Reload: -> editor.getBuffer().reload()
        Cancel: null

  scrollTop: (scrollTop, options={}) ->
    return @cachedScrollTop or 0 unless scrollTop?
    maxScrollTop = @verticalScrollbar.prop('scrollHeight') - @verticalScrollbar.height()
    scrollTop = Math.floor(Math.max(0, Math.min(maxScrollTop, scrollTop)))
    return if scrollTop == @cachedScrollTop
    @cachedScrollTop = scrollTop

    @updateDisplay() if @attached

    @renderedLines.css('top', -scrollTop)
    @underlayer.css('top', -scrollTop)
    @overlayer.css('top', -scrollTop)
    @gutter.lineNumbers.css('top', -scrollTop)

    if options?.adjustVerticalScrollbar ? true
      @verticalScrollbar.scrollTop(scrollTop)
    @editor.setScrollTop(@scrollTop())

  scrollBottom: (scrollBottom) ->
    if scrollBottom?
      @scrollTop(scrollBottom - @scrollView.height())
    else
      @scrollTop() + @scrollView.height()

  scrollLeft: (scrollLeft) ->
    if scrollLeft?
      @scrollView.scrollLeft(scrollLeft)
      @editor.setScrollLeft(@scrollLeft())
    else
      @scrollView.scrollLeft()

  scrollRight: (scrollRight) ->
    if scrollRight?
      @scrollView.scrollRight(scrollRight)
      @editor.setScrollLeft(@scrollLeft())
    else
      @scrollView.scrollRight()

  # Public: Scrolls the editor to the bottom.
  scrollToBottom: ->
    @scrollBottom(@editor.getScreenLineCount() * @lineHeight)

  # Public: Scrolls the editor to the position of the most recently added
  # cursor.
  #
  # The editor is also centered.
  scrollToCursorPosition: ->
    @scrollToBufferPosition(@editor.getCursorBufferPosition(), center: true)

  # Public: Scrolls the editor to the given buffer position.
  #
  # bufferPosition - An object that represents a buffer position. It can be either
  #                  an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
  # options - A hash matching the options available to {::scrollToPixelPosition}
  scrollToBufferPosition: (bufferPosition, options) ->
    @scrollToPixelPosition(@pixelPositionForBufferPosition(bufferPosition), options)

  # Public: Scrolls the editor to the given screen position.
  #
  # screenPosition - An object that represents a buffer position. It can be either
  #                  an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
  # options - A hash matching the options available to {::scrollToPixelPosition}
  scrollToScreenPosition: (screenPosition, options) ->
    @scrollToPixelPosition(@pixelPositionForScreenPosition(screenPosition), options)

  # Public: Scrolls the editor to the given pixel position.
  #
  # pixelPosition - An object that represents a pixel position. It can be either
  #                 an {Object} (`{row, column}`), {Array} (`[row, column]`), or
  #                 {Point}.
  # options - A hash with the following keys:
  #   :center - if `true`, the position is scrolled such that it's in
  #             the center of the editor
  scrollToPixelPosition: (pixelPosition, options) ->
    return unless @attached
    @scrollVertically(pixelPosition, options)
    @scrollHorizontally(pixelPosition)

  # Public: Highlight all the folds within the given buffer range.
  #
  # "Highlighting" essentially just adds the `fold-selected` class to the line's
  # DOM element.
  #
  # bufferRange - The {Range} to check.
  highlightFoldsContainingBufferRange: (bufferRange) ->
    screenLines = @editor.linesForScreenRows(@firstRenderedScreenRow, @lastRenderedScreenRow)
    for screenLine, i in screenLines
      if fold = screenLine.fold
        screenRow = @firstRenderedScreenRow + i
        element = @lineElementForScreenRow(screenRow)

        if bufferRange.intersectsWith(fold.getBufferRange())
          element.addClass('fold-selected')
        else
          element.removeClass('fold-selected')

  saveScrollPositionForEditor: ->
    if @attached
      @editor.setScrollTop(@scrollTop())
      @editor.setScrollLeft(@scrollLeft())

  # Public: Toggle soft tabs on the edit session.
  toggleSoftTabs: ->
    @editor.setSoftTabs(not @editor.getSoftTabs())

  # Public: Toggle soft wrap on the edit session.
  toggleSoftWrap: ->
    @setWidthInChars()
    @editor.setSoftWrap(not @editor.getSoftWrap())

  calculateWidthInChars: ->
    Math.floor(@scrollView.width() / @charWidth)

  calculateHeightInLines: ->
    Math.ceil($(window).height() / @lineHeight)

  # Public: Enables/disables soft wrap on the editor.
  #
  # softWrap - A {Boolean} which, if `true`, enables soft wrap
  setSoftWrap: (softWrap) ->
    if softWrap
      @addClass 'soft-wrap'
      @scrollLeft(0)
    else
      @removeClass 'soft-wrap'

  # Public: Sets the font size for the editor.
  #
  # fontSize - A {Number} indicating the font size in pixels.
  setFontSize: (fontSize) ->
    @css('font-size', "#{fontSize}px")

    @clearCharacterWidthCache()

    if @isOnDom()
      @redraw()
    else
      @redrawOnReattach = @attached

  # Public: Retrieves the font size for the editor.
  #
  # Returns a {Number} indicating the font size in pixels.
  getFontSize: ->
    parseInt(@css("font-size"))

  # Public: Sets the font family for the editor.
  #
  # fontFamily - A {String} identifying the CSS `font-family`.
  setFontFamily: (fontFamily='') ->
    @css('font-family', fontFamily)

    @clearCharacterWidthCache()

    @redraw()

  # Public: Gets the font family for the editor.
  #
  # Returns a {String} identifying the CSS `font-family`.
  getFontFamily: -> @css("font-family")

  # Public: Redraw the editor
  redraw: ->
    return unless @hasParent()
    return unless @attached
    @redrawOnReattach = false
    @calculateDimensions()
    @updatePaddingOfRenderedLines()
    @updateLayerDimensions()
    @requestDisplayUpdate()

  # Public: Split the editor view left.
  splitLeft: ->
    pane = @getPane()
    pane?.splitLeft(pane?.copyActiveItem()).activeView

  # Public: Split the editor view right.
  splitRight: ->
    pane = @getPane()
    pane?.splitRight(pane?.copyActiveItem()).activeView

  # Public: Split the editor view up.
  splitUp: ->
    pane = @getPane()
    pane?.splitUp(pane?.copyActiveItem()).activeView

  # Public: Split the editor view down.
  splitDown: ->
    pane = @getPane()
    pane?.splitDown(pane?.copyActiveItem()).activeView

  # Public: Get this view's pane.
  #
  # Returns a {Pane}.
  getPane: ->
    @parent('.item-views').parents('.pane').view()

  remove: (selector, keepData) ->
    return super if keepData or @removed
    super
    atom.workspaceView?.focus()

  beforeRemove: ->
    @trigger 'editor:will-be-removed'
    @removed = true
    @editor?.destroy()
    $(window).off(".editor-#{@id}")
    $(document).off(".editor-#{@id}")

  getCursorView: (index) ->
    index ?= @cursorViews.length - 1
    @cursorViews[index]

  getCursorViews: ->
    new Array(@cursorViews...)

  addCursorView: (cursor, options) ->
    cursorView = new CursorView(cursor, this, options)
    @cursorViews.push(cursorView)
    @overlayer.append(cursorView)
    cursorView

  removeCursorView: (cursorView) ->
    _.remove(@cursorViews, cursorView)

  getSelectionView: (index) ->
    index ?= @selectionViews.length - 1
    @selectionViews[index]

  getSelectionViews: ->
    new Array(@selectionViews...)

  addSelectionView: (selection) ->
    selectionView = new SelectionView({editorView: this, selection})
    @selectionViews.push(selectionView)
    @underlayer.append(selectionView)
    selectionView

  removeSelectionView: (selectionView) ->
    _.remove(@selectionViews, selectionView)

  removeAllCursorAndSelectionViews: ->
    cursorView.remove() for cursorView in @getCursorViews()
    selectionView.remove() for selectionView in @getSelectionViews()

  appendToLinesView: (view) ->
    @overlayer.append(view)

  # Scrolls the editor vertically to a given position.
  scrollVertically: (pixelPosition, {center}={}) ->
    scrollViewHeight = @scrollView.height()
    scrollTop = @scrollTop()
    scrollBottom = scrollTop + scrollViewHeight

    if center
      unless scrollTop < pixelPosition.top < scrollBottom
        @scrollTop(pixelPosition.top - (scrollViewHeight / 2))
    else
      linesInView = @scrollView.height() / @lineHeight
      maxScrollMargin = Math.floor((linesInView - 1) / 2)
      scrollMargin = Math.min(@vScrollMargin, maxScrollMargin)
      margin = scrollMargin * @lineHeight
      desiredTop = pixelPosition.top - margin
      desiredBottom = pixelPosition.top + @lineHeight + margin
      if desiredBottom > scrollBottom
        @scrollTop(desiredBottom - scrollViewHeight)
      else if desiredTop < scrollTop
        @scrollTop(desiredTop)

  # Scrolls the editor horizontally to a given position.
  scrollHorizontally: (pixelPosition) ->
    return if @editor.getSoftWrap()

    charsInView = @scrollView.width() / @charWidth
    maxScrollMargin = Math.floor((charsInView - 1) / 2)
    scrollMargin = Math.min(@hScrollMargin, maxScrollMargin)
    margin = scrollMargin * @charWidth
    desiredRight = pixelPosition.left + @charWidth + margin
    desiredLeft = pixelPosition.left - margin

    if desiredRight > @scrollRight()
      @scrollRight(desiredRight)
    else if desiredLeft < @scrollLeft()
      @scrollLeft(desiredLeft)
    @saveScrollPositionForEditor()

  calculateDimensions: ->
    fragment = $('<div class="line" style="position: absolute; visibility: hidden;"><span>x</span></div>')
    @renderedLines.append(fragment)

    lineRect = fragment[0].getBoundingClientRect()
    charRect = fragment.find('span')[0].getBoundingClientRect()
    @lineHeight = lineRect.height
    @charWidth = charRect.width
    @charHeight = charRect.height
    fragment.remove()
    @setHeightInLines()

  updateLayerDimensions: ->
    height = @lineHeight * @editor.getScreenLineCount()
    unless @layerHeight == height
      @layerHeight = height
      @underlayer.height(@layerHeight)
      @renderedLines.height(@layerHeight)
      @overlayer.height(@layerHeight)
      @verticalScrollbarContent.height(@layerHeight)
      @scrollBottom(height) if @scrollBottom() > height

    minWidth = Math.max(@charWidth * @editor.getMaxScreenLineLength() + 20, @scrollView.width())
    unless @layerMinWidth == minWidth
      @renderedLines.css('min-width', minWidth)
      @underlayer.css('min-width', minWidth)
      @overlayer.css('min-width', minWidth)
      @layerMinWidth = minWidth
      @trigger 'editor:min-width-changed'

  # Override for speed. The base function checks computedStyle, unnecessary here.
  isHidden: ->
    style = this[0].style
    if style.display == 'none' or not @isOnDom()
      true
    else
      false

  clearRenderedLines: ->
    @renderedLines.empty()
    @firstRenderedScreenRow = null
    @lastRenderedScreenRow = null

  resetDisplay: ->
    return unless @attached

    @clearRenderedLines()
    @removeAllCursorAndSelectionViews()
    editorScrollTop = @editor.getScrollTop() ? 0
    editorScrollLeft = @editor.getScrollLeft() ? 0
    @updateLayerDimensions()
    @scrollTop(editorScrollTop)
    @scrollLeft(editorScrollLeft)
    @setSoftWrap(@editor.getSoftWrap())
    @newCursors = @editor.getCursors()
    @newSelections = @editor.getSelections()
    @updateDisplay(suppressAutoScroll: true)

  requestDisplayUpdate: ->
    return if @pendingDisplayUpdate
    return unless @isVisible()
    @pendingDisplayUpdate = true
    setImmediate =>
      @updateDisplay()
      @pendingDisplayUpdate = false

  updateDisplay: (options={}) ->
    return unless @attached and @editor
    return if @editor.isDestroyed()
    unless @isOnDom() and @isVisible()
      @redrawOnReattach = true
      return

    @updateRenderedLines()
    @updatePlaceholderText()
    @highlightCursorLine()
    @updateCursorViews()
    @updateSelectionViews()
    @autoscroll(options)
    @trigger 'editor:display-updated'

  updateCursorViews: ->
    if @newCursors.length > 0
      @addCursorView(cursor) for cursor in @newCursors when not cursor.destroyed
      @syncCursorAnimations()
      @newCursors = []

    for cursorView in @getCursorViews()
      if cursorView.needsRemoval
        cursorView.remove()
      else if @shouldUpdateCursor(cursorView)
        cursorView.updateDisplay()

  shouldUpdateCursor: (cursorView) ->
    return false unless cursorView.needsUpdate

    pos = cursorView.getScreenPosition()
    pos.row >= @firstRenderedScreenRow and pos.row <= @lastRenderedScreenRow

  updateSelectionViews: ->
    if @newSelections.length > 0
      @addSelectionView(selection) for selection in @newSelections when not selection.destroyed
      @newSelections = []

    for selectionView in @getSelectionViews()
      if selectionView.needsRemoval
        selectionView.remove()
      else if @shouldUpdateSelection(selectionView)
        selectionView.updateDisplay()

  shouldUpdateSelection: (selectionView) ->
    screenRange = selectionView.getScreenRange()
    startRow = screenRange.start.row
    endRow = screenRange.end.row
    (startRow >= @firstRenderedScreenRow and startRow <= @lastRenderedScreenRow) or # startRow in range
      (endRow >= @firstRenderedScreenRow and endRow <= @lastRenderedScreenRow) or # endRow in range
      (startRow <= @firstRenderedScreenRow and endRow >= @lastRenderedScreenRow) # selection surrounds the rendered items

  syncCursorAnimations: ->
    for cursorView in @getCursorViews()
      do (cursorView) -> cursorView.resetBlinking()

  autoscroll: (options={}) ->
    for cursorView in @getCursorViews()
      if !options.suppressAutoScroll and cursorView.needsAutoscroll()
        @scrollToPixelPosition(cursorView.getPixelPosition())
      cursorView.clearAutoscroll()

    for selectionView in @getSelectionViews()
      if !options.suppressAutoScroll and selectionView.needsAutoscroll()
        @scrollToPixelPosition(selectionView.getCenterPixelPosition(), center: true)
        selectionView.highlight()
      selectionView.clearAutoscroll()

  updatePlaceholderText: ->
    return unless @mini
    if (not @placeholderText) or @editor.getText()
      @find('.placeholder-text').remove()
    else if @placeholderText and not @editor.getText()
      element = @find('.placeholder-text')
      if element.length
        element.text(@placeholderText)
      else
        @underlayer.append($('<span/>', class: 'placeholder-text', text: @placeholderText))

  updateRenderedLines: ->
    firstVisibleScreenRow = @getFirstVisibleScreenRow()
    lastScreenRowToRender = firstVisibleScreenRow + @heightInLines - 1
    lastScreenRow = @editor.getLastScreenRow()

    if @firstRenderedScreenRow? and firstVisibleScreenRow >= @firstRenderedScreenRow and lastScreenRowToRender <= @lastRenderedScreenRow
      renderFrom = Math.min(lastScreenRow, @firstRenderedScreenRow)
      renderTo = Math.min(lastScreenRow, @lastRenderedScreenRow)
    else
      renderFrom = Math.min(lastScreenRow, Math.max(0, firstVisibleScreenRow - @lineOverdraw))
      renderTo = Math.min(lastScreenRow, lastScreenRowToRender + @lineOverdraw)

    if @pendingChanges.length == 0 and @firstRenderedScreenRow and @firstRenderedScreenRow <= renderFrom and renderTo <= @lastRenderedScreenRow
      return

    changes = @pendingChanges
    intactRanges = @computeIntactRanges(renderFrom, renderTo)

    @gutter.updateLineNumbers(changes, renderFrom, renderTo)

    @clearDirtyRanges(intactRanges)
    @fillDirtyRanges(intactRanges, renderFrom, renderTo)
    @firstRenderedScreenRow = renderFrom
    @lastRenderedScreenRow = renderTo
    @updateLayerDimensions()
    @updatePaddingOfRenderedLines()

  computeSurroundingEmptyLineChanges: (change) ->
    emptyLineChanges = []

    if change.bufferDelta?
      afterStart = change.end + change.bufferDelta + 1
      if @editor.lineForBufferRow(afterStart) is ''
        afterEnd = afterStart
        afterEnd++ while @editor.lineForBufferRow(afterEnd + 1) is ''
        emptyLineChanges.push({start: afterStart, end: afterEnd, screenDelta: 0})

      beforeEnd = change.start - 1
      if @editor.lineForBufferRow(beforeEnd) is ''
        beforeStart = beforeEnd
        beforeStart-- while @editor.lineForBufferRow(beforeStart - 1) is ''
        emptyLineChanges.push({start: beforeStart, end: beforeEnd, screenDelta: 0})

    emptyLineChanges

  computeIntactRanges: (renderFrom, renderTo) ->
    return [] if !@firstRenderedScreenRow? and !@lastRenderedScreenRow?

    intactRanges = [{start: @firstRenderedScreenRow, end: @lastRenderedScreenRow, domStart: 0}]

    if not @mini and @showIndentGuide
      emptyLineChanges = []
      for change in @pendingChanges
        emptyLineChanges.push(@computeSurroundingEmptyLineChanges(change)...)
      @pendingChanges.push(emptyLineChanges...)

    for change in @pendingChanges
      newIntactRanges = []
      for range in intactRanges
        if change.end < range.start and change.screenDelta != 0
          newIntactRanges.push(
            start: range.start + change.screenDelta
            end: range.end + change.screenDelta
            domStart: range.domStart
          )
        else if change.end < range.start or change.start > range.end
          newIntactRanges.push(range)
        else
          if change.start > range.start
            newIntactRanges.push(
              start: range.start
              end: change.start - 1
              domStart: range.domStart)
          if change.end < range.end
            newIntactRanges.push(
              start: change.end + change.screenDelta + 1
              end: range.end + change.screenDelta
              domStart: range.domStart + change.end + 1 - range.start
            )
      intactRanges = newIntactRanges

    @truncateIntactRanges(intactRanges, renderFrom, renderTo)

    @pendingChanges = []

    intactRanges

  truncateIntactRanges: (intactRanges, renderFrom, renderTo) ->
    i = 0
    while i < intactRanges.length
      range = intactRanges[i]
      if range.start < renderFrom
        range.domStart += renderFrom - range.start
        range.start = renderFrom
      if range.end > renderTo
        range.end = renderTo
      if range.start >= range.end
        intactRanges.splice(i--, 1)
      i++
    intactRanges.sort (a, b) -> a.domStart - b.domStart

  clearDirtyRanges: (intactRanges) ->
    if intactRanges.length == 0
      @renderedLines[0].innerHTML = ''
    else if currentLine = @renderedLines[0].firstChild
      domPosition = 0
      for intactRange in intactRanges
        while intactRange.domStart > domPosition
          currentLine = @clearLine(currentLine)
          domPosition++
        for i in [intactRange.start..intactRange.end]
          currentLine = currentLine.nextSibling
          domPosition++
      while currentLine
        currentLine = @clearLine(currentLine)

  clearLine: (lineElement) ->
    next = lineElement.nextSibling
    @renderedLines[0].removeChild(lineElement)
    next

  fillDirtyRanges: (intactRanges, renderFrom, renderTo) ->
    i = 0
    nextIntact = intactRanges[i]
    currentLine = @renderedLines[0].firstChild

    row = renderFrom
    while row <= renderTo
      if row == nextIntact?.end + 1
        nextIntact = intactRanges[++i]

      if !nextIntact or row < nextIntact.start
        if nextIntact
          dirtyRangeEnd = nextIntact.start - 1
        else
          dirtyRangeEnd = renderTo

        for lineElement in @buildLineElementsForScreenRows(row, dirtyRangeEnd)
          @renderedLines[0].insertBefore(lineElement, currentLine)
          row++
      else
        currentLine = currentLine.nextSibling
        row++

  updatePaddingOfRenderedLines: ->
    paddingTop = @firstRenderedScreenRow * @lineHeight
    @renderedLines.css('padding-top', paddingTop)
    @gutter.lineNumbers.css('padding-top', paddingTop)

    paddingBottom = (@editor.getLastScreenRow() - @lastRenderedScreenRow) * @lineHeight
    @renderedLines.css('padding-bottom', paddingBottom)
    @gutter.lineNumbers.css('padding-bottom', paddingBottom)

  # Public: Retrieves the number of the row that is visible and currently at the
  # top of the editor.
  #
  # Returns a {Number}.
  getFirstVisibleScreenRow: ->
    screenRow = Math.floor(@scrollTop() / @lineHeight)
    screenRow = 0 if isNaN(screenRow)
    screenRow

  # Public: Retrieves the number of the row that is visible and currently at the
  # bottom of the editor.
  #
  # Returns a {Number}.
  getLastVisibleScreenRow: ->
    calculatedRow = Math.ceil((@scrollTop() + @scrollView.height()) / @lineHeight) - 1
    screenRow = Math.max(0, Math.min(@editor.getScreenLineCount() - 1, calculatedRow))
    screenRow = 0 if isNaN(screenRow)
    screenRow

  # Public: Given a row number, identifies if it is currently visible.
  #
  # row - A row {Number} to check
  #
  # Returns a {Boolean}.
  isScreenRowVisible: (row) ->
    @getFirstVisibleScreenRow() <= row <= @getLastVisibleScreenRow()

  handleScreenLinesChange: (change) ->
    @pendingChanges.push(change)
    @requestDisplayUpdate()

  buildLineElementForScreenRow: (screenRow) ->
    @buildLineElementsForScreenRows(screenRow, screenRow)[0]

  buildLineElementsForScreenRows: (startRow, endRow) ->
    div = document.createElement('div')
    div.innerHTML = @htmlForScreenRows(startRow, endRow)
    new Array(div.children...)

  htmlForScreenRows: (startRow, endRow) ->
    htmlLines = ''
    screenRow = startRow
    for line in @editor.linesForScreenRows(startRow, endRow)
      htmlLines += @htmlForScreenLine(line, screenRow++)
    htmlLines

  htmlForScreenLine: (screenLine, screenRow) ->
    { tokens, text, lineEnding, fold, isSoftWrapped } =  screenLine
    if fold
      attributes = { class: 'fold line', 'fold-id': fold.id }
    else
      attributes = { class: 'line' }

    invisibles = @invisibles if @showInvisibles
    eolInvisibles = @getEndOfLineInvisibles(screenLine)
    htmlEolInvisibles = @buildHtmlEndOfLineInvisibles(screenLine)

    indentation = EditorView.buildIndentation(screenRow, @editor)

    EditorView.buildLineHtml({tokens, text, lineEnding, fold, isSoftWrapped, invisibles, eolInvisibles, htmlEolInvisibles, attributes, @showIndentGuide, indentation, @editor, @mini})

  @buildIndentation: (screenRow, editor) ->
    bufferRow = editor.bufferPositionForScreenPosition([screenRow]).row
    bufferLine = editor.lineForBufferRow(bufferRow)
    if bufferLine is ''
      indentation = 0
      nextRow = screenRow + 1
      while nextRow < editor.getBuffer().getLineCount()
        bufferRow = editor.bufferPositionForScreenPosition([nextRow]).row
        bufferLine = editor.lineForBufferRow(bufferRow)
        if bufferLine isnt ''
          indentation = Math.ceil(editor.indentLevelForLine(bufferLine))
          break
        nextRow++

      previousRow = screenRow - 1
      while previousRow >= 0
        bufferRow = editor.bufferPositionForScreenPosition([previousRow]).row
        bufferLine = editor.lineForBufferRow(bufferRow)
        if bufferLine isnt ''
          indentation = Math.max(indentation, Math.ceil(editor.indentLevelForLine(bufferLine)))
          break
        previousRow--

      indentation
    else
      Math.ceil(editor.indentLevelForLine(bufferLine))

  buildHtmlEndOfLineInvisibles: (screenLine) ->
    invisibles = []
    for invisible in @getEndOfLineInvisibles(screenLine)
      invisibles.push("<span class='invisible-character'>#{invisible}</span>")
    invisibles.join('')

  getEndOfLineInvisibles: (screenLine) ->
    return [] unless @showInvisibles and @invisibles
    return [] if @mini or screenLine.isSoftWrapped()

    invisibles = []
    invisibles.push(@invisibles.cr) if @invisibles.cr and screenLine.lineEnding is '\r\n'
    invisibles.push(@invisibles.eol) if @invisibles.eol
    invisibles

  lineElementForScreenRow: (screenRow) ->
    @renderedLines.children(":eq(#{screenRow - @firstRenderedScreenRow})")

  toggleLineCommentsInSelection: ->
    @editor.toggleLineCommentsInSelection()

  # Public: Converts a buffer position to a pixel position.
  #
  # position - An object that represents a buffer position. It can be either
  #            an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
  #
  # Returns an object with two values: `top` and `left`, representing the pixel positions.
  pixelPositionForBufferPosition: (position) ->
    @pixelPositionForScreenPosition(@editor.screenPositionForBufferPosition(position))

  # Public: Converts a screen position to a pixel position.
  #
  # position - An object that represents a screen position. It can be either
  #            an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
  #
  # Returns an object with two values: `top` and `left`, representing the pixel positions.
  pixelPositionForScreenPosition: (position) ->
    return { top: 0, left: 0 } unless @isOnDom() and @isVisible()
    {row, column} = Point.fromObject(position)
    actualRow = Math.floor(row)

    lineElement = existingLineElement = @lineElementForScreenRow(actualRow)[0]
    unless existingLineElement
      lineElement = @buildLineElementForScreenRow(actualRow)
      @renderedLines.append(lineElement)
    left = @positionLeftForLineAndColumn(lineElement, actualRow, column)
    unless existingLineElement
      @renderedLines[0].removeChild(lineElement)
    { top: row * @lineHeight, left }

  positionLeftForLineAndColumn: (lineElement, screenRow, screenColumn) ->
    return 0 if screenColumn == 0

    tokenizedLine = @editor.displayBuffer.lineForRow(screenRow)

    left = 0
    index = 0
    for token in tokenizedLine.tokens
      for char in token.value
        return left if index >= screenColumn

        val = @getCharacterWidthCache(token.scopes, char)
        if val?
          left += val
        else
          return @measureToColumn(lineElement, tokenizedLine, screenColumn)

        index++
    left

  measureToColumn: (lineElement, tokenizedLine, screenColumn) ->
    left = oldLeft = index = 0
    iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, TextNodeFilter)

    returnLeft = null

    offsetLeft = @scrollView.offset().left
    paddingLeft = parseInt(@scrollView.css('padding-left'))

    while textNode = iterator.nextNode()
      content = textNode.textContent

      for char, i in content
        # Don't continue caching long lines :racehorse:
        break if index > LongLineLength and screenColumn < index

        # Dont return right away, finish caching the whole line
        returnLeft = left if index == screenColumn
        oldLeft = left

        scopes = tokenizedLine.tokenAtBufferColumn(index)?.scopes
        cachedCharWidth = @getCharacterWidthCache(scopes, char)

        if cachedCharWidth?
          left = oldLeft + cachedCharWidth
        else
          # i + 1 to measure to the end of the current character
          MeasureRange.setEnd(textNode, i + 1)
          MeasureRange.collapse()
          rects = MeasureRange.getClientRects()
          return 0 if rects.length == 0
          left = rects[0].left - Math.floor(offsetLeft) + Math.floor(@scrollLeft()) - paddingLeft

          if scopes?
            cachedCharWidth = left - oldLeft
            @setCharacterWidthCache(scopes, char, cachedCharWidth)

        # Assume all the characters are the same width when dealing with long
        # lines :racehorse:
        return screenColumn * cachedCharWidth if index > LongLineLength

        index++

    returnLeft ? left

  getCharacterWidthCache: (scopes, char) ->
    scopes ?= NoScope
    obj = @constructor.characterWidthCache
    for scope in scopes
      obj = obj[scope]
      return null unless obj?
    obj[char]

  setCharacterWidthCache: (scopes, char, val) ->
    scopes ?= NoScope
    obj = @constructor.characterWidthCache
    for scope in scopes
      obj[scope] ?= {}
      obj = obj[scope]
    obj[char] = val

  clearCharacterWidthCache: ->
    @constructor.characterWidthCache = {}

  pixelOffsetForScreenPosition: (position) ->
    {top, left} = @pixelPositionForScreenPosition(position)
    offset = @renderedLines.offset()
    {top: top + offset.top, left: left + offset.left}

  screenPositionFromMouseEvent: (e) ->
    { pageX, pageY } = e
    offset = @scrollView.offset()

    editorRelativeTop = pageY - offset.top + @scrollTop()
    row = Math.floor(editorRelativeTop / @lineHeight)
    column = 0

    if pageX > offset.left and lineElement = @lineElementForScreenRow(row)[0]
      range = document.createRange()
      iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, acceptNode: -> NodeFilter.FILTER_ACCEPT)
      while node = iterator.nextNode()
        range.selectNodeContents(node)
        column += node.textContent.length
        {left, right} = range.getClientRects()[0]
        break if left <= pageX <= right

      if node
        for characterPosition in [node.textContent.length...0]
          range.setStart(node, characterPosition - 1)
          range.setEnd(node, characterPosition)
          {left, right, width} = range.getClientRects()[0]
          break if left <= pageX - width / 2 <= right
          column--

      range.detach()

    new Point(row, column)

  # Highlights the current line the cursor is on.
  highlightCursorLine: ->
    return if @mini

    @highlightedLine?.removeClass('cursor-line')
    if @editor.getSelection().isEmpty()
      @highlightedLine = @lineElementForScreenRow(@editor.getCursorScreenRow())
      @highlightedLine.addClass('cursor-line')
    else
      @highlightedLine = null

  # Copies the current file path to the native clipboard.
  copyPathToClipboard: ->
    path = @editor.getPath()
    atom.clipboard.write(path) if path?

  @buildLineHtml: ({tokens, text, lineEnding, fold, isSoftWrapped, invisibles, eolInvisibles, htmlEolInvisibles, attributes, showIndentGuide, indentation, editor, mini}) ->
    scopeStack = []
    line = []

    attributePairs = ''
    attributePairs += " #{attributeName}=\"#{value}\"" for attributeName, value of attributes
    line.push("<div #{attributePairs}>")

    if text == ''
      html = @buildEmptyLineHtml(showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, editor, mini)
      line.push(html) if html
    else
      firstNonWhitespacePosition = text.search(/\S/)
      firstTrailingWhitespacePosition = text.search(/\s*$/)
      lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0
      position = 0
      for token in tokens
        @updateScopeStack(line, scopeStack, token.scopes)
        hasLeadingWhitespace =  position < firstNonWhitespacePosition
        hasTrailingWhitespace = position + token.value.length > firstTrailingWhitespacePosition
        hasIndentGuide = not mini and showIndentGuide and (hasLeadingWhitespace or lineIsWhitespaceOnly)
        line.push(token.getValueAsHtml({invisibles, hasLeadingWhitespace, hasTrailingWhitespace, hasIndentGuide}))
        position += token.value.length

    @popScope(line, scopeStack) while scopeStack.length > 0
    line.push(htmlEolInvisibles) unless text == ''
    line.push("<span class='fold-marker'/>") if fold

    line.push('</div>')
    line.join('')

  @updateScopeStack: (line, scopeStack, desiredScopes) ->
    excessScopes = scopeStack.length - desiredScopes.length
    if excessScopes > 0
      @popScope(line, scopeStack) while excessScopes--

    # pop until common prefix
    for i in [scopeStack.length..0]
      break if _.isEqual(scopeStack[0...i], desiredScopes[0...i])
      @popScope(line, scopeStack)

    # push on top of common prefix until scopeStack == desiredScopes
    for j in [i...desiredScopes.length]
      @pushScope(line, scopeStack, desiredScopes[j])

    null

  @pushScope: (line, scopeStack, scope) ->
    scopeStack.push(scope)
    line.push("<span class=\"#{scope.replace(/\./g, ' ')}\">")

  @popScope: (line, scopeStack) ->
    scopeStack.pop()
    line.push("</span>")

  @buildEmptyLineHtml: (showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, editor, mini) ->
    indentCharIndex = 0
    if not mini and showIndentGuide
      if indentation > 0
        tabLength = editor.getTabLength()
        indentGuideHtml = ''
        for level in [0...indentation]
          indentLevelHtml = "<span class='indent-guide'>"
          for characterPosition in [0...tabLength]
            if invisible = eolInvisibles[indentCharIndex++]
              indentLevelHtml += "<span class='invisible-character'>#{invisible}</span>"
            else
              indentLevelHtml += ' '
          indentLevelHtml += "</span>"
          indentGuideHtml += indentLevelHtml

        while indentCharIndex < eolInvisibles.length
          indentGuideHtml += "<span class='invisible-character'>#{eolInvisibles[indentCharIndex++]}</span>"

        return indentGuideHtml

    if htmlEolInvisibles.length > 0
      htmlEolInvisibles
    else
      '&nbsp;'

  replaceSelectedText: (replaceFn) ->
    selection = @editor.getSelection()
    return false if selection.isEmpty()

    text = replaceFn(@editor.getTextInRange(selection.getBufferRange()))
    return false if text is null or text is undefined

    @editor.insertText(text, select: true)
    true

  consolidateSelections: (e) -> e.abortKeyBinding() unless @editor.consolidateSelections()

  logCursorScope: ->
    console.log @editor.getCursorScopes()

  logScreenLines: (start, end) ->
    @editor.logScreenLines(start, end)

  logRenderedLines: ->
    @renderedLines.find('.line').each (n) ->
      console.log n, $(this).text()
