]> git.gir.st - VimFx.git/blob - extension/lib/modes.coffee
Fix missing hint markers when re-entering Hints mode quickly
[VimFx.git] / extension / lib / modes.coffee
1 ###
2 # Copyright Anton Khodakivskiy 2013, 2014.
3 # Copyright Simon Lydell 2013, 2014, 2015, 2016.
4 # Copyright Wang Zhuochun 2014.
5 #
6 # This file is part of VimFx.
7 #
8 # VimFx is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # VimFx is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with VimFx. If not, see <http://www.gnu.org/licenses/>.
20 ###
21
22 # This file defines VimFx’s modes, and their respective commands. The Normal
23 # mode commands are defined in commands.coffee, though.
24
25 {commands, findStorage} = require('./commands')
26 defaults = require('./defaults')
27 help = require('./help')
28 prefs = require('./prefs')
29 SelectionManager = require('./selection')
30 translate = require('./translate')
31 utils = require('./utils')
32
33 {FORWARD, BACKWARD} = SelectionManager
34 CARET_BROWSING_PREF = 'accessibility.browsewithcaret'
35
36 # Helper to create modes in a DRY way.
37 mode = (modeName, obj, commands = null) ->
38 obj.name = translate("mode.#{modeName}")
39 obj.order = defaults.mode_order[modeName]
40 obj.commands = {}
41 for commandName, fn of commands
42 pref = "mode.#{modeName}.#{commandName}"
43 obj.commands[commandName] = {
44 pref: defaults.BRANCH + pref
45 run: fn
46 category: defaults.categoryMap[pref]
47 description: translate(pref)
48 order: defaults.command_order[pref]
49 }
50 exports[modeName] = obj
51
52
53
54 mode('normal', {
55 onEnter: ({vim, storage}, {returnTo = null} = {}) ->
56 if returnTo
57 storage.returnTo = returnTo
58 else if storage.returnTo
59 vim._enterMode(storage.returnTo)
60 storage.returnTo = null
61
62 onLeave: ({vim}) ->
63 vim._run('clear_inputs')
64
65 onInput: (args, match) ->
66 {vim, storage, uiEvent} = args
67 {keyStr} = match
68
69 if match.type == 'none' or
70 (match.likelyConflict and not match.specialKeys['<force>'])
71 match.discard()
72 if storage.returnTo
73 vim._enterMode(storage.returnTo)
74 storage.returnTo = null
75 # If you press `aa` (and `a` is a prefix key, but there’s no `aa`
76 # shortcut), don’t pass the second `a` to the page.
77 return not match.toplevel
78
79 if match.type == 'full'
80 match.command.run(args)
81
82 # If the command changed the mode, wait until coming back from that mode
83 # before switching to `storage.returnTo` if any (see `onEnter` above).
84 if storage.returnTo and vim.mode == 'normal'
85 vim._enterMode(storage.returnTo)
86 storage.returnTo = null
87
88 # At this point the match is either full, partial or part of a count. Then
89 # we always want to suppress, except for one case: The Escape key.
90 return true unless keyStr == '<escape>'
91
92 # Passing Escape through allows for stopping the loading of the page and
93 # closing many custom dialogs (and perhaps other things; Escape is a very
94 # commonly used key).
95 if uiEvent
96 # In browser UI the biggest reasons are allowing to reset the location bar
97 # when blurring it, and closing dialogs such as the “bookmark this page”
98 # dialog (<c-d>). However, an exception is made for the devtools (<c-K>).
99 # There, trying to unfocus the devtools using Escape would annoyingly
100 # open the split console.
101 return utils.isDevtoolsElement(uiEvent.originalTarget)
102 else
103 # In web pages content, an exception is made if an element that VimFx
104 # cares about is focused. That allows for blurring an input in a custom
105 # dialog without closing the dialog too.
106 return vim.focusType != 'none'
107
108 # Note that this special handling of Escape is only used in Normal mode.
109 # There are two reasons we might suppress it in other modes. If some custom
110 # dialog of a website is open, we should be able to cancel hint markers on
111 # it without closing it. Secondly, otherwise cancelling hint markers on
112 # Google causes its search bar to be focused.
113
114 }, commands)
115
116
117
118 helper_move_caret = (method, direction, {vim, storage, count = 1}) ->
119 vim._run('move_caret', {
120 method, direction, select: storage.select
121 count: if method == 'intraLineMove' then 1 else count
122 })
123
124 mode('caret', {
125 onEnter: ({vim, storage}, {select = false} = {}) ->
126 storage.select = select
127 storage.caretBrowsingPref = prefs.root.get(CARET_BROWSING_PREF)
128 prefs.root.set(CARET_BROWSING_PREF, true)
129 vim._run('enable_caret')
130
131 listener = ->
132 return unless newVim = vim._parent.getCurrentVim(vim.window)
133 prefs.root.set(
134 CARET_BROWSING_PREF,
135 if newVim.mode == 'caret' then true else storage.caretBrowsingPref
136 )
137 vim._parent.on('TabSelect', listener)
138 storage.removeListener = -> vim._parent.off('TabSelect', listener)
139
140 onLeave: ({vim, storage}) ->
141 prefs.root.set(CARET_BROWSING_PREF, storage.caretBrowsingPref)
142 vim._run('clear_selection')
143 storage.removeListener?()
144 storage.removeListener = null
145
146 onInput: (args, match) ->
147 if match.type == 'full'
148 match.command.run(args)
149 return true
150 return false
151
152 }, {
153 # coffeelint: disable=colon_assignment_spacing
154 move_left: helper_move_caret.bind(null, 'characterMove', BACKWARD)
155 move_right: helper_move_caret.bind(null, 'characterMove', FORWARD)
156 move_down: helper_move_caret.bind(null, 'lineMove', FORWARD)
157 move_up: helper_move_caret.bind(null, 'lineMove', BACKWARD)
158 move_word_left: helper_move_caret.bind(null, 'wordMoveAdjusted', BACKWARD)
159 move_word_right: helper_move_caret.bind(null, 'wordMoveAdjusted', FORWARD)
160 move_to_line_start: helper_move_caret.bind(null, 'intraLineMove', BACKWARD)
161 move_to_line_end: helper_move_caret.bind(null, 'intraLineMove', FORWARD)
162 # coffeelint: enable=colon_assignment_spacing
163
164 toggle_selection: ({vim, storage}) ->
165 storage.select = not storage.select
166 if storage.select
167 vim.notify(translate('notification.toggle_selection.enter'))
168 else
169 vim._run('collapse_selection')
170
171 toggle_selection_direction: ({vim}) ->
172 vim._run('toggle_selection_direction')
173
174 copy_selection_and_exit: ({vim}) ->
175 vim._run('get_selection', null, (selection) ->
176 # If the selection consists of newlines only, it _looks_ as if the
177 # selection is collapsed, so don’t try to copy it in that case.
178 if /^\n*$/.test(selection)
179 vim.notify(translate('notification.copy_selection_and_exit.none'))
180 else
181 # Trigger this copying command instead of putting `selection` into the
182 # clipboard, since `window.getSelection().toString()` sadly collapses
183 # whitespace in `<pre>` elements.
184 vim.window.goDoCommand('cmd_copy')
185 vim._enterMode('normal')
186 )
187
188 exit: ({vim}) ->
189 vim._enterMode('normal')
190 })
191
192
193
194 mode('hints', {
195 onEnter: ({vim, storage}, options) ->
196 {markerContainer, callback, count = 1, sleep = -1} = options
197 storage.markerContainer = markerContainer
198 storage.callback = callback
199 storage.count = count
200
201 if sleep >= 0
202 storage.clearInterval = utils.interval(vim.window, sleep, (next) ->
203 if markerContainer.markers.length == 0
204 next()
205 return
206 vim._send('getMarkableElementsMovements', null, (diffs) ->
207 for {dx, dy}, index in diffs when not (dx == 0 and dy == 0)
208 markerContainer.markerMap[index].updatePosition(dx, dy)
209 next()
210 )
211 )
212
213 onLeave: ({vim, storage}) ->
214 storage.markerContainer?.remove()
215 storage.clearInterval?()
216 for key of storage
217 storage[key] = null
218 return
219
220 onInput: (args, match) ->
221 {vim, storage} = args
222 {markerContainer, callback} = storage
223
224 if match.type == 'full'
225 match.command.run(args)
226
227 else if match.unmodifiedKey in vim.options.hint_chars
228 matchedMarkers = markerContainer.matchHintChar(match.unmodifiedKey)
229
230 if matchedMarkers.length > 0
231 again = callback(matchedMarkers[0], storage.count, match.keyStr)
232 storage.count -= 1
233
234 if again
235 vim.window.setTimeout((->
236 marker.markMatched(false) for marker in matchedMarkers
237 return
238 ), vim.options.hints_timeout)
239 markerContainer.reset()
240
241 else
242 vim.window.setTimeout((->
243 # Don’t remove the marker container if we have re-entered Hints mode
244 # before the timeout has passed.
245 markerContainer.remove() unless vim.mode == 'hints'
246 ), vim.options.hints_timeout)
247
248 # Prevent `onLeave` from removing the markers immediately.
249 storage.markerContainer = null
250
251 # The callback might have entered another mode. Only go back to Normal
252 # mode if we’re still in Hints mode.
253 vim._enterMode('normal') if vim.mode == 'hints'
254
255 return true
256
257 }, {
258 exit: ({vim}) ->
259 vim._enterMode('normal')
260
261 rotate_markers_forward: ({storage}) ->
262 storage.markerContainer.rotateOverlapping(true)
263
264 rotate_markers_backward: ({storage}) ->
265 storage.markerContainer.rotateOverlapping(false)
266
267 delete_hint_char: ({storage}) ->
268 storage.markerContainer.deleteHintChar()
269
270 increase_count: ({storage}) ->
271 storage.count += 1
272
273 toggle_complementary: ({storage}) ->
274 storage.markerContainer.toggleComplementary()
275 })
276
277
278
279 mode('ignore', {
280 onEnter: ({vim, storage}, {count = null, type = null} = {}) ->
281 storage.count = count
282
283 # Keep last `.type` if no type was given. This is useful when returning to
284 # Ignore mode after runnning the `unquote` command.
285 if type
286 storage.type = type
287 else
288 storage.type ?= 'explicit'
289
290 onLeave: ({vim, storage}) ->
291 unless storage.count? or storage.type == 'focusType'
292 vim._run('blur_active_element')
293
294 onInput: (args, match) ->
295 {vim, storage} = args
296 args.count = 1
297 switch storage.count
298 when null
299 if match.type == 'full'
300 match.command.run(args)
301 return true
302 when 1
303 vim._enterMode('normal')
304 else
305 storage.count -= 1
306 return false
307
308 }, {
309 exit: ({vim, storage}) ->
310 storage.type = null
311 vim._enterMode('normal')
312 unquote: ({vim}) ->
313 vim._enterMode('normal', {returnTo: 'ignore'})
314 })
315
316
317
318 mode('find', {
319 onEnter: ->
320
321 onLeave: ({vim}) ->
322 findBar = vim.window.gBrowser.getFindBar()
323 findStorage.lastSearchString = findBar._findField.value
324
325 onInput: (args, match) ->
326 args.findBar = args.vim.window.gBrowser.getFindBar()
327 if match.type == 'full'
328 match.command.run(args)
329 return true
330 return false
331
332 }, {
333 exit: ({vim, findBar}) ->
334 vim._enterMode('normal')
335 findBar.close()
336 })
337
338
339
340 mode('marks', {
341 onEnter: ({vim, storage}, callback) ->
342 storage.callback = callback
343 storage.timeoutId = vim.window.setTimeout((->
344 vim.hideNotification()
345 vim._enterMode('normal')
346 ), vim.options.timeout)
347
348 onLeave: ({vim, storage}) ->
349 storage.callback = null
350 vim.window.clearTimeout(storage.timeoutId) if storage.timeoutId?
351 storage.timeoutId = null
352
353 onInput: (args, match) ->
354 {vim, storage} = args
355 if match.type == 'full'
356 match.command.run(args)
357 else
358 storage.callback(match.keyStr)
359 vim._enterMode('normal')
360 return true
361 }, {
362 exit: ({vim}) ->
363 vim._enterMode('normal')
364 })
Imprint / Impressum