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 again = callback(matchedMarkers[0], storage.count, match.keyStr)
235 vim.window.setTimeout((->
236 marker.markMatched(false) for marker in matchedMarkers
238 ), vim.options.hints_timeout)
239 markerContainer.reset()
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)
248 # Prevent `onLeave` from removing the markers immediately.
249 storage.markerContainer = null
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'
259 vim._enterMode('normal')
261 rotate_markers_forward: ({storage}) ->
262 storage.markerContainer.rotateOverlapping(true)
264 rotate_markers_backward: ({storage}) ->
265 storage.markerContainer.rotateOverlapping(false)
267 delete_hint_char: ({storage}) ->
268 storage.markerContainer.deleteHintChar()
270 increase_count: ({storage}) ->
273 toggle_complementary: ({storage}) ->
274 storage.markerContainer.toggleComplementary()
280 onEnter: ({vim, storage}, {count = null, type = null} = {}) ->
281 storage.count = count
283 # Keep last `.type` if no type was given. This is useful when returning to
284 # Ignore mode after runnning the `unquote` command.
288 storage.type ?= 'explicit'
290 onLeave: ({vim, storage}) ->
291 unless storage.count? or storage.type == 'focusType'
292 vim._run('blur_active_element')
294 onInput: (args, match) ->
295 {vim, storage} = args
299 if match.type == 'full'
300 match.command.run(args)
303 vim._enterMode('normal')
309 exit: ({vim, storage}) ->
311 vim._enterMode('normal')
313 vim._enterMode('normal', {returnTo: 'ignore'})
322 findBar = vim.window.gBrowser.getFindBar()
323 findStorage.lastSearchString = findBar._findField.value
325 onInput: (args, match) ->
326 args.findBar = args.vim.window.gBrowser.getFindBar()
327 if match.type == 'full'
328 match.command.run(args)
333 exit: ({vim, findBar}) ->
334 vim._enterMode('normal')
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)
348 onLeave: ({vim, storage}) ->
349 storage.callback = null
350 vim.window.clearTimeout(storage.timeoutId) if storage.timeoutId?
351 storage.timeoutId = null
353 onInput: (args, match) ->
354 {vim, storage} = args
355 if match.type == 'full'
356 match.command.run(args)
358 storage.callback(match.keyStr)
359 vim._enterMode('normal')
363 vim._enterMode('normal')