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