2 # Copyright Anton Khodakivskiy 2013, 2014.
3 # Copyright Simon Lydell 2013, 2014, 2015, 2016.
4 # Copyright Wang Zhuochun 2014.
6 # This file is part of VimFx.
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.
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.
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/>.
22 # This file defines VimFx’s modes, and their respective commands. The Normal
23 # mode commands are defined in commands.coffee, though.
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')
33 {FORWARD, BACKWARD} = SelectionManager
34 CARET_BROWSING_PREF = 'accessibility.browsewithcaret'
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]
41 for commandName, fn of commands
42 pref = "mode.#{modeName}.#{commandName}"
43 obj.commands[commandName] = {
44 pref: defaults.BRANCH + pref
46 category: defaults.categoryMap[pref]
47 description: translate(pref)
48 order: defaults.command_order[pref]
50 exports[modeName] = obj
55 onEnter: ({vim, storage}, {returnTo = null} = {}) ->
57 storage.returnTo = returnTo
58 else if storage.returnTo
59 vim._enterMode(storage.returnTo)
60 storage.returnTo = null
63 vim._run('clear_inputs')
65 onInput: (args, match) ->
66 {vim, storage, uiEvent} = args
69 if match.type == 'none' or
70 (match.likelyConflict and not match.specialKeys['<force>'])
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
79 if match.type == 'full'
80 match.command.run(args)
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
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>'
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
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)
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'
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.
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
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')
132 return unless newVim = vim._parent.getCurrentVim(vim.window)
135 if newVim.mode == 'caret' then true else storage.caretBrowsingPref
137 vim._parent.on('TabSelect', listener)
138 storage.removeListener = -> vim._parent.off('TabSelect', listener)
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
146 onInput: (args, match) ->
147 if match.type == 'full'
148 match.command.run(args)
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
164 toggle_selection: ({vim, storage}) ->
165 storage.select = not storage.select
167 vim.notify(translate('notification.toggle_selection.enter'))
169 vim._run('collapse_selection')
171 toggle_selection_direction: ({vim}) ->
172 vim._run('toggle_selection_direction')
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'))
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')
189 vim._enterMode('normal')
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
202 storage.clearInterval = utils.interval(vim.window, sleep, (next) ->
203 if markerContainer.markers.length == 0
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)
213 onLeave: ({vim, storage}) ->
214 storage.markerContainer?.remove()
215 storage.clearInterval?()
220 onInput: (args, match) ->
221 {vim, storage} = args
222 {markerContainer, callback} = storage
224 if match.type == 'full'
225 match.command.run(args)
227 else if match.unmodifiedKey in vim.options.hint_chars
228 matchedMarkers = markerContainer.matchHintChar(match.unmodifiedKey)
230 if matchedMarkers.length > 0
231 # Prevent `onLeave` from removing the markers immediately. (The callback
232 # might enter another mode.)
233 storage.markerContainer = null
235 again = callback(matchedMarkers[0], storage.count, match.keyStr)
239 # Add the container back again.
240 storage.markerContainer = markerContainer
242 vim.window.setTimeout((->
243 marker.markMatched(false) for marker in matchedMarkers
245 ), vim.options.hints_timeout)
246 markerContainer.reset()
249 vim.window.setTimeout((->
250 # Don’t remove the marker container if we have re-entered Hints mode
251 # before the timeout has passed.
252 markerContainer.remove() unless vim.mode == 'hints'
253 ), vim.options.hints_timeout)
255 # The callback might have entered another mode. Only go back to Normal
256 # mode if we’re still in Hints mode.
257 vim._enterMode('normal') if vim.mode == 'hints'
263 vim._enterMode('normal')
265 rotate_markers_forward: ({storage}) ->
266 storage.markerContainer.rotateOverlapping(true)
268 rotate_markers_backward: ({storage}) ->
269 storage.markerContainer.rotateOverlapping(false)
271 delete_hint_char: ({storage}) ->
272 storage.markerContainer.deleteHintChar()
274 increase_count: ({storage}) ->
277 toggle_complementary: ({storage}) ->
278 storage.markerContainer.toggleComplementary()
284 onEnter: ({vim, storage}, {count = null, type = null} = {}) ->
285 storage.count = count
287 # Keep last `.type` if no type was given. This is useful when returning to
288 # Ignore mode after runnning the `unquote` command.
292 storage.type ?= 'explicit'
294 onLeave: ({vim, storage}) ->
295 unless storage.count? or storage.type == 'focusType'
296 vim._run('blur_active_element')
298 onInput: (args, match) ->
299 {vim, storage} = args
303 if match.type == 'full'
304 match.command.run(args)
307 vim._enterMode('normal')
313 exit: ({vim, storage}) ->
315 vim._enterMode('normal')
317 vim._enterMode('normal', {returnTo: 'ignore'})
326 findBar = vim.window.gBrowser.getFindBar()
327 findStorage.lastSearchString = findBar._findField.value
329 onInput: (args, match) ->
330 args.findBar = args.vim.window.gBrowser.getFindBar()
331 if match.type == 'full'
332 match.command.run(args)
337 exit: ({vim, findBar}) ->
338 vim._enterMode('normal')
345 onEnter: ({vim, storage}, callback) ->
346 storage.callback = callback
347 storage.timeoutId = vim.window.setTimeout((->
348 vim.hideNotification()
349 vim._enterMode('normal')
350 ), vim.options.timeout)
352 onLeave: ({vim, storage}) ->
353 storage.callback = null
354 vim.window.clearTimeout(storage.timeoutId) if storage.timeoutId?
355 storage.timeoutId = null
357 onInput: (args, match) ->
358 {vim, storage} = args
359 if match.type == 'full'
360 match.command.run(args)
362 storage.callback(match.keyStr)
363 vim._enterMode('normal')
367 vim._enterMode('normal')