       "backward"
     else if range.compareBoundaryPoints(Range.END_TO_END, previousRange) is 1
       "forward"
+
+  domRangesHaveSameCursorPositionForDirection = (range, otherRange, direction) ->
+    rects = range.getClientRects()
+    otherRects = otherRange.getClientRects()
+
+    if rects.length and otherRects.length
+      if direction is "backward"
+        a = rects[0]
+        b = otherRects[0]
+      else
+        a = rects[rects.length - 1]
+        b = otherRects[otherRects.length - 1]
+
+      for prop in ["left", "right", "bottom", "width"]
+        return false if Math.ceil(a[prop]) isnt Math.ceil(b[prop])
+      true
+
+  domSelectionIsUneditable = ->
+    selection = getDOMSelection()
+    focusElement = DOM.findElementForContainerAtOffset(selection.focusNode, selection.focusOffset)
+    not focusElement?.isContentEditable

      else
        textRange.moveStart(granularity, -1)
      textRange.select()
    @selectionDidChange(null, direction)

  lock: ->
    if @lockCount++ is 0


  # Selection observer delegate

  selectionDidChange: (range, previousRange) ->
    direction = getDirectionFromDOMRanges(range, previousRange)

    if direction and @rangeWithinElement(range) and
         (domRangesHaveSameCursorPositionForDirection(range, previousRange, direction) or domSelectionIsUneditable())
      @adjustSelectionInDirectionWithGranularity(direction, "character")
    else
      @updateCurrentLocationRange()

  # Private

  updateCurrentLocationRange: (domRange = getDOMRange()) ->
    locationRange = @createLocationRangeFromDOMRange(domRange)
