]> git.gir.st - VimFx.git/blob - extension/lib/modes.coffee
Fix missing hint marker activation feedback for `v`
[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 # Prevent `onLeave` from removing the markers immediately. (The callback
232 # might enter another mode.)
233 storage.markerContainer = null
234
235 again = callback(matchedMarkers[0], storage.count, match.keyStr)
236 storage.count -= 1
237
238 if again
239 # Add the container back again.
240 storage.markerContainer = markerContainer
241
242 vim.window.setTimeout((->
243 marker.markMatched(false) for marker in matchedMarkers
244 return
245 ), vim.options.hints_timeout)
246 markerContainer.reset()
247
248 else
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)
254
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'
258
259 return true
260
261 }, {
262 exit: ({vim}) ->
263 vim._enterMode('normal')
264
265 rotate_markers_forward: ({storage}) ->
266 storage.markerContainer.rotateOverlapping(true)
267
268 rotate_markers_backward: ({storage}) ->
269 storage.markerContainer.rotateOverlapping(false)
270
271 delete_hint_char: ({storage}) ->
272 storage.markerContainer.deleteHintChar()
273
274 increase_count: ({storage}) ->
275 storage.count += 1
276
277 toggle_complementary: ({storage}) ->
278 storage.markerContainer.toggleComplementary()
279 })
280
281
282
283 mode('ignore', {
284 onEnter: ({vim, storage}, {count = null, type = null} = {}) ->
285 storage.count = count
286
287 # Keep last `.type` if no type was given. This is useful when returning to
288 # Ignore mode after runnning the `unquote` command.
289 if type
290 storage.type = type
291 else
292 storage.type ?= 'explicit'
293
294 onLeave: ({vim, storage}) ->
295 unless storage.count? or storage.type == 'focusType'
296 vim._run('blur_active_element')
297
298 onInput: (args, match) ->
299 {vim, storage} = args
300 args.count = 1
301 switch storage.count
302 when null
303 if match.type == 'full'
304 match.command.run(args)
305 return true
306 when 1
307 vim._enterMode('normal')
308 else
309 storage.count -= 1
310 return false
311
312 }, {
313 exit: ({vim, storage}) ->
314 storage.type = null
315 vim._enterMode('normal')
316 unquote: ({vim}) ->
317 vim._enterMode('normal', {returnTo: 'ignore'})
318 })
319
320
321
322 mode('find', {
323 onEnter: ->
324
325 onLeave: ({vim}) ->
326 findBar = vim.window.gBrowser.getFindBar()
327 findStorage.lastSearchString = findBar._findField.value
328
329 onInput: (args, match) ->
330 args.findBar = args.vim.window.gBrowser.getFindBar()
331 if match.type == 'full'
332 match.command.run(args)
333 return true
334 return false
335
336 }, {
337 exit: ({vim, findBar}) ->
338 vim._enterMode('normal')
339 findBar.close()
340 })
341
342
343
344 mode('marks', {
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)
351
352 onLeave: ({vim, storage}) ->
353 storage.callback = null
354 vim.window.clearTimeout(storage.timeoutId) if storage.timeoutId?
355 storage.timeoutId = null
356
357 onInput: (args, match) ->
358 {vim, storage} = args
359 if match.type == 'full'
360 match.command.run(args)
361 else
362 storage.callback(match.keyStr)
363 vim._enterMode('normal')
364 return true
365 }, {
366 exit: ({vim}) ->
367 vim._enterMode('normal')
368 })
Imprint / Impressum