2 # Copyright Simon Lydell 2014, 2015, 2016.
4 # This file is part of VimFx.
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.
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.
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/>.
20 # This file defines VimFx’s modes, and their respective commands. The Normal
21 # mode commands are defined in commands.coffee, though.
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')
32 {FORWARD, BACKWARD} = SelectionManager
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]
39 for commandName, fn of commands
40 pref = "mode.#{modeName}.#{commandName}"
41 obj.commands[commandName] = {
42 pref: defaults.BRANCH + pref
44 category: defaults.categoryMap[pref]
45 description: translate(pref)
46 order: defaults.command_order[pref]
48 exports[modeName] = obj
53 onEnter: ({vim, storage}, {returnTo = null} = {}) ->
55 storage.returnTo = returnTo
56 else if storage.returnTo
57 vim._enterMode(storage.returnTo)
58 storage.returnTo = null
61 vim._run('clear_inputs')
63 onInput: (args, match) ->
64 {vim, storage, event} = args
66 focusTypeBeforeCommand = vim.focusType
68 vim.hideNotification() if match.type in ['none', 'full']
70 if match.type == 'none' or
71 (match.likelyConflict and not match.specialKeys['<force>'])
74 vim._enterMode(storage.returnTo)
75 storage.returnTo = null
76 # If you press `aa` (and `a` is a prefix key, but there’s no `aa`
77 # shortcut), don’t pass the second `a` to the page.
78 return not match.toplevel
80 if match.type == 'full'
81 match.command.run(args)
83 # If the command changed the mode, wait until coming back from that mode
84 # before switching to `storage.returnTo` if any (see `onEnter` above).
85 if storage.returnTo and vim.mode == 'normal'
86 vim._enterMode(storage.returnTo)
87 storage.returnTo = null
89 # At this point the match is either full, partial or part of a count. Then
90 # we always want to suppress, except for one case: The Escape key.
91 return true unless keyStr == '<escape>'
93 # Passing Escape through allows for stopping the loading of the page and
94 # closing many custom dialogs (and perhaps other things; Escape is a very
96 if vim.isUIEvent(event)
97 # In browser UI the biggest reasons are allowing to reset the location bar
98 # when blurring it, and closing dialogs such as the “bookmark this page”
99 # dialog (<c-d>). However, an exception is made for the devtools (<c-K>).
100 # There, trying to unfocus the devtools using Escape would annoyingly
101 # open the split console.
102 return utils.isDevtoolsElement(event.originalTarget)
104 # In web page content, an exception is made if an element that VimFx
105 # cares about is focused. That allows for blurring an input in a custom
106 # dialog without closing the dialog too. Note that running a command might
107 # change `vim.focusType`, which is why this saved value is used here.
108 return focusTypeBeforeCommand != 'none'
110 # Note that this special handling of Escape is only used in Normal mode.
111 # There are two reasons we might suppress it in other modes. If some custom
112 # dialog of a website is open, we should be able to cancel hint markers on
113 # it without closing it. Secondly, otherwise cancelling hint markers on
114 # Google causes its search bar to be focused.
120 helper_move_caret = (method, direction, {vim, storage, count = 1}) ->
121 vim._run('move_caret', {
122 method, direction, select: storage.select
123 count: if method == 'intraLineMove' then 1 else count
127 onEnter: ({vim, storage}, {select = false} = {}) ->
128 storage.select = select
129 vim._parent.resetCaretBrowsing(true)
130 vim._run('enable_caret')
133 return unless newVim = vim._parent.getCurrentVim(vim.window)
134 vim._parent.resetCaretBrowsing(
135 if newVim.mode == 'caret' then true else null
137 vim._parent.on('TabSelect', listener)
138 storage.removeListener = -> vim._parent.off('TabSelect', listener)
140 onLeave: ({vim, storage}) ->
141 vim._parent.resetCaretBrowsing()
142 vim._run('clear_selection')
143 storage.removeListener?()
144 storage.removeListener = null
146 onInput: (args, match) ->
147 args.vim.hideNotification()
149 # In case the user turns Caret Browsing off while in Caret mode.
150 args.vim._parent.resetCaretBrowsing(true)
154 match.command.run(args)
156 when 'partial', 'count'
161 # coffeelint: disable=colon_assignment_spacing
162 move_left: helper_move_caret.bind(null, 'characterMove', BACKWARD)
163 move_right: helper_move_caret.bind(null, 'characterMove', FORWARD)
164 move_down: helper_move_caret.bind(null, 'lineMove', FORWARD)
165 move_up: helper_move_caret.bind(null, 'lineMove', BACKWARD)
166 move_word_left: helper_move_caret.bind(null, 'wordMoveAdjusted', BACKWARD)
167 move_word_right: helper_move_caret.bind(null, 'wordMoveAdjusted', FORWARD)
168 move_to_line_start: helper_move_caret.bind(null, 'intraLineMove', BACKWARD)
169 move_to_line_end: helper_move_caret.bind(null, 'intraLineMove', FORWARD)
170 # coffeelint: enable=colon_assignment_spacing
172 toggle_selection: ({vim, storage}) ->
173 storage.select = not storage.select
175 vim.notify(translate('notification.toggle_selection.enter'))
177 vim._run('collapse_selection')
179 toggle_selection_direction: ({vim}) ->
180 vim._run('toggle_selection_direction')
182 copy_selection_and_exit: ({vim}) ->
183 vim._run('get_selection', null, (selection) ->
184 # If the selection consists of newlines only, it _looks_ as if the
185 # selection is collapsed, so don’t try to copy it in that case.
186 if /^\n*$/.test(selection)
187 vim.notify(translate('notification.copy_selection_and_exit.none'))
189 # Trigger this copying command instead of putting `selection` into the
190 # clipboard, since `window.getSelection().toString()` sadly collapses
191 # whitespace in `<pre>` elements.
192 vim.window.goDoCommand('cmd_copy')
193 vim._enterMode('normal')
197 vim._enterMode('normal')
203 onEnter: ({vim, storage}, options) ->
205 markerContainer, callback, matchText = true, count = 1, sleep = -1
207 storage.markerContainer = markerContainer
208 storage.callback = callback
209 storage.matchText = matchText
210 storage.count = count
211 storage.isMatched = {byText: false, byHint: false}
212 storage.skipOnLeaveCleanup = false
215 markerContainer.visualFeedbackUpdater =
216 hintsMode.updateVisualFeedback.bind(null, vim)
217 vim._run('clear_selection')
220 storage.clearInterval = utils.interval(vim.window, sleep, (next) ->
221 if markerContainer.markers.length == 0
224 vim._send('getMarkableElementsMovements', null, (diffs) ->
225 for {dx, dy}, index in diffs when not (dx == 0 and dy == 0)
226 markerContainer.markerMap[index].updatePosition(dx, dy)
231 onLeave: ({vim, storage}) ->
232 hintsMode.cleanup(vim, storage) unless storage.skipOnLeaveCleanup
234 onInput: (args, match) ->
235 {vim, storage} = args
236 {markerContainer, callback} = storage
240 match.command.run(Object.assign({match}, args))
243 # Make sure notifications for counts aren’t shown.
244 vim._refreshPersistentNotification()
246 {char, isHintChar} = hintsMode.getChar(match, storage)
247 return true unless char
249 return true if storage.isMatched.byText and not isHintChar
251 visibleMarkers = markerContainer.addChar(char, isHintChar)
252 storage.isMatched = hintsMode.isMatched(visibleMarkers, markerContainer)
254 if (storage.isMatched.byHint and isHintChar) or
255 (storage.isMatched.byText and not isHintChar and
256 vim.options['hints.auto_activate'])
257 hintsMode.activateMatch(
258 vim, storage, match, visibleMarkers, callback
262 vim._parent.ignoreKeyEventsUntilTime =
263 Date.now() + vim.options['hints.timeout']
269 vim._enterMode('normal')
271 activate_highlighted: ({vim, storage, match}) ->
272 {markerContainer: {markers, highlightedMarkers}, callback} = storage
273 return if highlightedMarkers.length == 0
275 for marker in markers when marker.visible
276 marker.hide() unless marker in highlightedMarkers
278 hintsMode.activateMatch(
279 vim, storage, match, highlightedMarkers, callback
282 rotate_markers_forward: ({storage}) ->
283 storage.markerContainer.rotateOverlapping(true)
285 rotate_markers_backward: ({storage}) ->
286 storage.markerContainer.rotateOverlapping(false)
288 delete_char: ({storage}) ->
289 {markerContainer} = storage
290 visibleMarkers = markerContainer.deleteChar()
292 hintsMode.isMatched(visibleMarkers or [], markerContainer)
294 increase_count: ({storage}) ->
296 # Uncomment this line if you want to use `gulp hints.html`!
297 # utils.writeToClipboard(storage.markerContainer.container.outerHTML)
299 toggle_complementary: ({storage}) ->
300 storage.markerContainer.toggleComplementary()
306 onEnter: ({vim, storage}, {count = null, type = null} = {}) ->
307 storage.count = count
309 # Keep last `.type` if no type was given. This is useful when returning to
310 # Ignore mode after runnning the `unquote` command.
314 storage.type ?= 'explicit'
316 onLeave: ({vim, storage}) ->
317 unless storage.count? or storage.type == 'focusType'
318 vim._run('blur_active_element')
320 onInput: (args, match) ->
321 {vim, storage} = args
328 match.command.run(args)
333 # Make sure notifications for counts aren’t shown.
334 vim.hideNotification()
338 vim._enterMode('normal')
346 exit: ({vim, storage}) ->
348 vim._enterMode('normal')
351 vim._enterMode('normal', {returnTo: 'ignore'})
360 findBar = vim.window.gBrowser.getFindBar()
361 findStorage.lastSearchString = findBar._findField.value
362 findStorage.busy = false
364 onInput: (args, match) ->
367 when match.type == 'full'
368 args.findBar = args.vim.window.gBrowser.getFindBar()
369 match.command.run(args)
371 when match.type == 'partial'
373 when vim.focusType != 'findbar'
374 # If we’re in Find mode but the find bar input hasn’t been focused yet,
375 # suppress all input, because we don’t want to trigger Firefox commands,
376 # such as `/` (which opens the Quick Find bar). This happens when
377 # `helper_find_from_top_of_viewport` is slow, or when _Firefox_ is slow,
378 # for example to due to heavy page loading. The following URL is a good
379 # stress test: <https://html.spec.whatwg.org/>
380 findStorage.busy = true
383 # At this point we know for sure that the find bar is not busy anymore.
384 findStorage.busy = false
388 exit: ({vim, findBar}) ->
389 vim._enterMode('normal')
396 onEnter: ({vim, storage}, callback) ->
397 storage.callback = callback
398 storage.timeoutId = vim.window.setTimeout((->
399 vim.hideNotification()
400 vim._enterMode('normal')
401 ), vim.options.timeout)
403 onLeave: ({vim, storage}) ->
404 storage.callback = null
405 vim.window.clearTimeout(storage.timeoutId) if storage.timeoutId?
406 storage.timeoutId = null
408 onInput: (args, match) ->
409 {vim, storage} = args
412 match.command.run(args)
414 storage.callback(match.keyStr)
415 vim._enterMode('normal')
420 vim.hideNotification()
421 vim._enterMode('normal')