]> git.gir.st - VimFx.git/blob - extension/lib/selection.coffee
Change license to MIT
[VimFx.git] / extension / lib / selection.coffee
1 # This file helps dealing with text selection: Querying it, modifying it and
2 # moving its caret.
3
4 FORWARD = true
5 BACKWARD = false
6
7 class SelectionManager
8 constructor: (@window) ->
9 @selection = @window.getSelection()
10 @nsISelectionController = @window
11 .QueryInterface(Ci.nsIInterfaceRequestor)
12 .getInterface(Ci.nsIWebNavigation)
13 .QueryInterface(Ci.nsIInterfaceRequestor)
14 .getInterface(Ci.nsISelectionDisplay)
15 .QueryInterface(Ci.nsISelectionController)
16
17 @FORWARD = FORWARD
18 @BACKWARD = BACKWARD
19
20 enableCaret: ->
21 @nsISelectionController.setCaretEnabled(true)
22 @nsISelectionController.setCaretReadOnly(false)
23 @nsISelectionController.setCaretVisibilityDuringSelection(true)
24
25 collapse: ->
26 return if @selection.isCollapsed
27 direction = @getDirection()
28 if direction == FORWARD
29 @selection.collapseToEnd()
30 else
31 @selection.collapseToStart()
32
33 moveCaretThrowing: (method, direction, select = true) ->
34 @nsISelectionController[method](direction, select)
35
36 moveCaret: (args...) ->
37 try
38 @moveCaretThrowing(args...)
39 catch error
40 return error
41 return null
42
43 # The simplest way to measure selection length is
44 # `selection.toString().length`. However, `selection.toString()` collapses
45 # whitespace even in `<pre>` elements, so it is sadly not reliable. Instead we
46 # have to measure client rects.
47 getSelectionLength: ->
48 width = 0
49 numRects = 0
50 for index in [0...@selection.rangeCount] by 1
51 for rect in @selection.getRangeAt(index).getClientRects()
52 width += rect.width
53 numRects += 1
54 return [width, numRects]
55
56 getDirection: (directionIfCollapsed = FORWARD) ->
57 # The “test for newlines” trick used in `@reverseDirection` should _not_ be
58 # used here. If it were, selecting the newline(s) between two paragraphs and
59 # then `@collapse()`ing that selection might move the caret.
60 return directionIfCollapsed if @selection.isCollapsed
61
62 # Creating backwards ranges is not supported. When trying to do so,
63 # `range.toString()` returns the empty string.
64 range = @window.document.createRange()
65 range.setStart(@selection.anchorNode, @selection.anchorOffset)
66 range.setEnd(@selection.focusNode, @selection.focusOffset)
67 return range.toString() != ''
68
69 reverseDirection: ->
70 # If the caret is at the end of a paragraph, or at the start of the
71 # paragraph, and the newline(s) between those paragraphs happen to be
72 # selected, it _looks_ as if `selection.isCollapsed` should be `true`, but
73 # it isn't because of said (virtual) newline characters. If so, the below
74 # algorithm might move the caret from the start of a paragraph to the end of
75 # the previous paragraph, etc. So don’t do anything if the selection is
76 # empty or newlines only.
77 return if /^\n*$/.test(@selection.toString())
78
79 direction = @getDirection()
80
81 range = @selection.getRangeAt(0)
82 edge = if direction == FORWARD then 'start' else 'end'
83 {"#{edge}Container": edgeElement, "#{edge}Offset": edgeOffset} = range
84 range.collapse(not direction)
85 @selection.removeAllRanges()
86 @selection.addRange(range)
87 @selection.extend(edgeElement, edgeOffset)
88
89 # When going from backward to forward the caret might end up at the line
90 # _after_ the selection if the selection ends at the end of a line, which
91 # looks a bit odd. This adjusts that case.
92 if direction == BACKWARD
93 [oldWidth] = @getSelectionLength()
94 @moveCaret('characterMove', BACKWARD)
95 [newWidth] = @getSelectionLength()
96 unless newWidth == oldWidth
97 @moveCaret('characterMove', FORWARD)
98
99 wordMoveAdjusted: (direction, select = true) ->
100 selectionDirection = @getDirection(direction)
101
102 try
103 if (select and selectionDirection != direction) or
104 (not select and direction == FORWARD)
105 @_wordMoveAdjusted(direction)
106 else
107 @moveCaretThrowing('wordMove', direction, select)
108 catch error
109 throw error unless error.name == 'NS_ERROR_FAILURE'
110 @collapse() unless select
111 return error
112
113 unless select
114 # When at the very end of the document `@_wordMoveAdjusted(FORWARD)` might
115 # end up moving the caret _backward!_ If so, move the caret back.
116 @moveCaret('wordMove', direction) if @getDirection(direction) != direction
117 @collapse()
118
119 return null
120
121 _wordMoveAdjusted: (direction) ->
122 [oldWidth, oldNumRects] = @getSelectionLength()
123
124 # Do the old “two steps forward and one step back” trick to avoid the
125 # selection ending with whitespace. (Vice versa for backwards selections.)
126 @moveCaretThrowing('wordMove', direction)
127 @moveCaretThrowing('wordMove', direction)
128 @moveCaretThrowing('wordMove', not direction)
129
130 [newWidth, newNumRects] = @getSelectionLength()
131
132 # However, in some cases the above can result in the caret not moving at
133 # all. If so, go _three_ steps forward and one back. (Again, vice versa for
134 # backwards selections.)
135 if oldNumRects == newNumRects and oldWidth == newWidth
136 @moveCaretThrowing('wordMove', direction)
137 @moveCaretThrowing('wordMove', direction)
138 @moveCaretThrowing('wordMove', direction)
139 @moveCaretThrowing('wordMove', not direction)
140
141 [newWidth, newNumRects] = @getSelectionLength()
142
143 # Finally, if everything else failed to move the caret (such as when being
144 # one word from the end of the document), simply move one step.
145 if oldNumRects == newNumRects and oldWidth == newWidth
146 @moveCaretThrowing('wordMove', direction)
147
148 module.exports = SelectionManager
Imprint / Impressum