]> git.gir.st - VimFx.git/blob - extension/lib/modes.coffee
Make Caret browsing handling more robust
[VimFx.git] / extension / lib / modes.coffee
1 ###
2 # Copyright Simon Lydell 2014, 2015, 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 defines VimFx’s modes, and their respective commands. The Normal
21 # mode commands are defined in commands.coffee, though.
22
23 {commands, findStorage} = require('./commands')
24 defaults = require('./defaults')
25 help = require('./help')
26 hintsMode = require('./hints-mode')
27 prefs = require('./prefs')
28 SelectionManager = require('./selection')
29 translate = require('./translate')
30 utils = require('./utils')
31
32 {FORWARD, BACKWARD} = SelectionManager
33
34 # Helper to create modes in a DRY way.
35 mode = (modeName, obj, commands = null) ->
36 obj.name = translate("mode.#{modeName}")
37 obj.order = defaults.mode_order[modeName]
38 obj.commands = {}
39 for commandName, fn of commands
40 pref = "mode.#{modeName}.#{commandName}"
41 obj.commands[commandName] = {
42 pref: defaults.BRANCH + pref
43 run: fn
44 category: defaults.categoryMap[pref]
45 description: translate(pref)
46 order: defaults.command_order[pref]
47 }
48 exports[modeName] = obj
49
50
51
52 mode('normal', {
53 onEnter: ({vim, storage}, {returnTo = null} = {}) ->
54 if returnTo
55 storage.returnTo = returnTo
56 else if storage.returnTo
57 vim._enterMode(storage.returnTo)
58 storage.returnTo = null
59
60 onLeave: ({vim}) ->
61 vim._run('clear_inputs')
62
63 onInput: (args, match) ->
64 {vim, storage, event} = args
65 {keyStr} = match
66
67 vim.hideNotification() if match.type in ['none', 'full']
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 vim.isUIEvent(event)
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(event.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 vim._parent.resetCaretBrowsing(true)
128 vim._run('enable_caret')
129
130 listener = ->
131 return unless newVim = vim._parent.getCurrentVim(vim.window)
132 vim._parent.resetCaretBrowsing(
133 if newVim.mode == 'caret' then true else null
134 )
135 vim._parent.on('TabSelect', listener)
136 storage.removeListener = -> vim._parent.off('TabSelect', listener)
137
138 onLeave: ({vim, storage}) ->
139 vim._parent.resetCaretBrowsing()
140 vim._run('clear_selection')
141 storage.removeListener?()
142 storage.removeListener = null
143
144 onInput: (args, match) ->
145 args.vim.hideNotification()
146
147 # In case the user turns Caret Browsing off while in Caret mode.
148 args.vim._parent.resetCaretBrowsing(true)
149
150 switch match.type
151 when 'full'
152 match.command.run(args)
153 return true
154 when 'partial', 'count'
155 return true
156 return false
157
158 }, {
159 # coffeelint: disable=colon_assignment_spacing
160 move_left: helper_move_caret.bind(null, 'characterMove', BACKWARD)
161 move_right: helper_move_caret.bind(null, 'characterMove', FORWARD)
162 move_down: helper_move_caret.bind(null, 'lineMove', FORWARD)
163 move_up: helper_move_caret.bind(null, 'lineMove', BACKWARD)
164 move_word_left: helper_move_caret.bind(null, 'wordMoveAdjusted', BACKWARD)
165 move_word_right: helper_move_caret.bind(null, 'wordMoveAdjusted', FORWARD)
166 move_to_line_start: helper_move_caret.bind(null, 'intraLineMove', BACKWARD)
167 move_to_line_end: helper_move_caret.bind(null, 'intraLineMove', FORWARD)
168 # coffeelint: enable=colon_assignment_spacing
169
170 toggle_selection: ({vim, storage}) ->
171 storage.select = not storage.select
172 if storage.select
173 vim.notify(translate('notification.toggle_selection.enter'))
174 else
175 vim._run('collapse_selection')
176
177 toggle_selection_direction: ({vim}) ->
178 vim._run('toggle_selection_direction')
179
180 copy_selection_and_exit: ({vim}) ->
181 vim._run('get_selection', null, (selection) ->
182 # If the selection consists of newlines only, it _looks_ as if the
183 # selection is collapsed, so don’t try to copy it in that case.
184 if /^\n*$/.test(selection)
185 vim.notify(translate('notification.copy_selection_and_exit.none'))
186 else
187 # Trigger this copying command instead of putting `selection` into the
188 # clipboard, since `window.getSelection().toString()` sadly collapses
189 # whitespace in `<pre>` elements.
190 vim.window.goDoCommand('cmd_copy')
191 vim._enterMode('normal')
192 )
193
194 exit: ({vim}) ->
195 vim._enterMode('normal')
196 })
197
198
199
200 mode('hints', {
201 onEnter: ({vim, storage}, options) ->
202 {
203 markerContainer, callback, matchText = true, count = 1, sleep = -1
204 } = options
205 storage.markerContainer = markerContainer
206 storage.callback = callback
207 storage.matchText = matchText
208 storage.count = count
209 storage.isMatched = {byText: false, byHint: false}
210 storage.skipOnLeaveCleanup = false
211
212 if matchText
213 markerContainer.visualFeedbackUpdater =
214 hintsMode.updateVisualFeedback.bind(null, vim)
215 vim._run('clear_selection')
216
217 if sleep >= 0
218 storage.clearInterval = utils.interval(vim.window, sleep, (next) ->
219 if markerContainer.markers.length == 0
220 next()
221 return
222 vim._send('getMarkableElementsMovements', null, (diffs) ->
223 for {dx, dy}, index in diffs when not (dx == 0 and dy == 0)
224 markerContainer.markerMap[index].updatePosition(dx, dy)
225 next()
226 )
227 )
228
229 onLeave: ({vim, storage}) ->
230 hintsMode.cleanup(vim, storage) unless storage.skipOnLeaveCleanup
231
232 onInput: (args, match) ->
233 {vim, storage} = args
234 {markerContainer, callback} = storage
235
236 switch match.type
237 when 'full'
238 match.command.run(Object.assign({match}, args))
239
240 when 'none', 'count'
241 # Make sure notifications for counts aren’t shown.
242 vim._refreshPersistentNotification()
243
244 {char, isHintChar} = hintsMode.getChar(match, storage)
245 return true unless char
246
247 return true if storage.isMatched.byText and not isHintChar
248
249 visibleMarkers = markerContainer.addChar(char, isHintChar)
250 storage.isMatched = hintsMode.isMatched(visibleMarkers, markerContainer)
251
252 if (storage.isMatched.byHint and isHintChar) or
253 (storage.isMatched.byText and not isHintChar and
254 vim.options['hints.auto_activate'])
255 hintsMode.activateMatch(
256 vim, storage, match, visibleMarkers, callback
257 )
258
259 unless isHintChar
260 vim._parent.ignoreKeyEventsUntilTime =
261 Date.now() + vim.options['hints.timeout']
262
263 return true
264
265 }, {
266 exit: ({vim}) ->
267 vim._enterMode('normal')
268
269 activate_highlighted: ({vim, storage, match}) ->
270 {markerContainer: {markers, highlightedMarkers}, callback} = storage
271 return if highlightedMarkers.length == 0
272
273 for marker in markers when marker.visible
274 marker.hide() unless marker in highlightedMarkers
275
276 hintsMode.activateMatch(
277 vim, storage, match, highlightedMarkers, callback
278 )
279
280 rotate_markers_forward: ({storage}) ->
281 storage.markerContainer.rotateOverlapping(true)
282
283 rotate_markers_backward: ({storage}) ->
284 storage.markerContainer.rotateOverlapping(false)
285
286 delete_char: ({storage}) ->
287 {markerContainer} = storage
288 visibleMarkers = markerContainer.deleteChar()
289 storage.isMatched =
290 hintsMode.isMatched(visibleMarkers or [], markerContainer)
291
292 increase_count: ({storage}) ->
293 storage.count += 1
294 # Uncomment this line if you want to use `gulp hints.html`!
295 # utils.writeToClipboard(storage.markerContainer.container.outerHTML)
296
297 toggle_complementary: ({storage}) ->
298 storage.markerContainer.toggleComplementary()
299 })
300
301
302
303 mode('ignore', {
304 onEnter: ({vim, storage}, {count = null, type = null} = {}) ->
305 storage.count = count
306
307 # Keep last `.type` if no type was given. This is useful when returning to
308 # Ignore mode after runnning the `unquote` command.
309 if type
310 storage.type = type
311 else
312 storage.type ?= 'explicit'
313
314 onLeave: ({vim, storage}) ->
315 unless storage.count? or storage.type == 'focusType'
316 vim._run('blur_active_element')
317
318 onInput: (args, match) ->
319 {vim, storage} = args
320 args.count = 1
321
322 switch storage.count
323 when null
324 switch match.type
325 when 'full'
326 match.command.run(args)
327 return true
328 when 'partial'
329 return true
330
331 # Make sure notifications for counts aren’t shown.
332 vim.hideNotification()
333 return false
334
335 when 1
336 vim._enterMode('normal')
337
338 else
339 storage.count -= 1
340
341 return false
342
343 }, {
344 exit: ({vim, storage}) ->
345 storage.type = null
346 vim._enterMode('normal')
347
348 unquote: ({vim}) ->
349 vim._enterMode('normal', {returnTo: 'ignore'})
350 })
351
352
353
354 mode('find', {
355 onEnter: ->
356
357 onLeave: ({vim}) ->
358 findBar = vim.window.gBrowser.getFindBar()
359 findStorage.lastSearchString = findBar._findField.value
360 findStorage.busy = false
361
362 onInput: (args, match) ->
363 {vim} = args
364 switch
365 when match.type == 'full'
366 args.findBar = args.vim.window.gBrowser.getFindBar()
367 match.command.run(args)
368 return true
369 when match.type == 'partial'
370 return true
371 when vim.focusType != 'findbar'
372 # If we’re in Find mode but the find bar input hasn’t been focused yet,
373 # suppress all input, because we don’t want to trigger Firefox commands,
374 # such as `/` (which opens the Quick Find bar). This happens when
375 # `helper_find_from_top_of_viewport` is slow, or when _Firefox_ is slow,
376 # for example to due to heavy page loading. The following URL is a good
377 # stress test: <https://html.spec.whatwg.org/>
378 findStorage.busy = true
379 return true
380 else
381 # At this point we know for sure that the find bar is not busy anymore.
382 findStorage.busy = false
383 return false
384
385 }, {
386 exit: ({vim, findBar}) ->
387 vim._enterMode('normal')
388 findBar.close()
389 })
390
391
392
393 mode('marks', {
394 onEnter: ({vim, storage}, callback) ->
395 storage.callback = callback
396 storage.timeoutId = vim.window.setTimeout((->
397 vim.hideNotification()
398 vim._enterMode('normal')
399 ), vim.options.timeout)
400
401 onLeave: ({vim, storage}) ->
402 storage.callback = null
403 vim.window.clearTimeout(storage.timeoutId) if storage.timeoutId?
404 storage.timeoutId = null
405
406 onInput: (args, match) ->
407 {vim, storage} = args
408 switch match.type
409 when 'full'
410 match.command.run(args)
411 when 'none', 'count'
412 storage.callback(match.keyStr)
413 vim._enterMode('normal')
414 return true
415
416 }, {
417 exit: ({vim}) ->
418 vim.hideNotification()
419 vim._enterMode('normal')
420 })
Imprint / Impressum