]> git.gir.st - VimFx.git/blob - extension/lib/modes.coffee
Fix devtools detection in newer Firefox versions
[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 hints = require('./hints')
29 translate = require('./l10n')
30 {rotateOverlappingMarkers} = require('./marker')
31 prefs = require('./prefs')
32 SelectionManager = require('./selection')
33 utils = require('./utils')
34
35 {FORWARD, BACKWARD} = SelectionManager
36 CARET_BROWSING_PREF = 'accessibility.browsewithcaret'
37
38 # Helper to create modes in a DRY way.
39 mode = (modeName, obj, commands = null) ->
40 obj.name = translate("mode.#{modeName}")
41 obj.order = defaults.mode_order[modeName]
42 obj.commands = {}
43 for commandName, fn of commands
44 pref = "mode.#{modeName}.#{commandName}"
45 obj.commands[commandName] = {
46 pref: defaults.BRANCH + pref
47 run: fn
48 category: defaults.categoryMap[pref]
49 description: translate(pref)
50 order: defaults.command_order[pref]
51 }
52 exports[modeName] = obj
53
54
55
56 mode('normal', {
57 onEnter: ({vim, storage}, options = {}) ->
58 if options.returnTo
59 storage.returnTo = options.returnTo
60 else if storage.returnTo
61 vim.enterMode(storage.returnTo)
62 storage.returnTo = null
63
64 onLeave: ({vim}) ->
65 vim._run('clear_inputs')
66
67 onInput: (args, match) ->
68 {vim, storage, uiEvent} = args
69 {keyStr} = match
70
71 if match.type == 'none' or
72 (match.likelyConflict and not match.specialKeys['<force>'])
73 match.discard()
74 if storage.returnTo
75 vim.enterMode(storage.returnTo)
76 storage.returnTo = null
77 # If you press `aa` (and `a` is a prefix key, but there’s no `aa`
78 # shortcut), don’t pass the second `a` to the page.
79 return not match.toplevel
80
81 if match.type == 'full'
82 match.command.run(args)
83
84 # If the command changed the mode, wait until coming back from that mode
85 # before switching to `storage.returnTo` if any (see `onEnter` above).
86 if storage.returnTo and vim.mode == 'normal'
87 vim.enterMode(storage.returnTo)
88 storage.returnTo = null
89
90 # At this point the match is either full, partial or part of a count. Then
91 # we always want to suppress, except for one case: The Escape key.
92 return true unless keyStr == '<escape>'
93
94 # Passing Escape through allows for stopping the loading of the page and
95 # closing many custom dialogs (and perhaps other things; Escape is a very
96 # commonly used key).
97 if uiEvent
98 # In browser UI the biggest reasons are allowing to reset the location bar
99 # when blurring it, and closing dialogs such as the “bookmark this page”
100 # dialog (<c-d>). However, an exception is made for the devtools (<c-K>).
101 # There, trying to unfocus the devtools using Escape would annoyingly
102 # open the split console.
103 return utils.isDevtoolsElement(uiEvent.originalTarget)
104 else
105 # In web pages content, an exception is made if an element that VimFx
106 # cares about is focused. That allows for blurring an input in a custom
107 # dialog without closing the dialog too.
108 return vim.focusType != 'none'
109
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.
115
116 }, commands)
117
118
119
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
124 })
125
126 mode('caret', {
127 onEnter: ({vim, storage}, select = false) ->
128 storage.select = select
129 storage.caretBrowsingPref = prefs.root.get(CARET_BROWSING_PREF)
130 prefs.root.set(CARET_BROWSING_PREF, true)
131 vim._run('enable_caret')
132
133 listener = ->
134 return unless newVim = vim._parent.getCurrentVim(vim.window)
135 prefs.root.set(
136 CARET_BROWSING_PREF,
137 if newVim.mode == 'caret' then true else storage.caretBrowsingPref
138 )
139 vim._parent.on('TabSelect', listener)
140 storage.removeListener = -> vim._parent.off('TabSelect', listener)
141
142 onLeave: ({vim, storage}) ->
143 prefs.root.set(CARET_BROWSING_PREF, storage.caretBrowsingPref)
144 vim._run('clear_selection')
145 storage.removeListener?()
146 storage.removeListener = null
147
148 onInput: (args, match) ->
149 {vim, storage} = args
150 if match.type == 'full'
151 match.command.run(args)
152 return true
153 return false
154
155 }, {
156 # coffeelint: disable=colon_assignment_spacing
157 move_left: helper_move_caret.bind(null, 'characterMove', BACKWARD)
158 move_right: helper_move_caret.bind(null, 'characterMove', FORWARD)
159 move_down: helper_move_caret.bind(null, 'lineMove', FORWARD)
160 move_up: helper_move_caret.bind(null, 'lineMove', BACKWARD)
161 move_word_left: helper_move_caret.bind(null, 'wordMoveAdjusted', BACKWARD)
162 move_word_right: helper_move_caret.bind(null, 'wordMoveAdjusted', FORWARD)
163 move_to_line_start: helper_move_caret.bind(null, 'intraLineMove', BACKWARD)
164 move_to_line_end: helper_move_caret.bind(null, 'intraLineMove', FORWARD)
165 # coffeelint: enable=colon_assignment_spacing
166
167 toggle_selection: ({vim, storage}) ->
168 storage.select = not storage.select
169 if storage.select
170 vim.notify(translate('notification.toggle_selection.enter'))
171 else
172 vim._run('collapse_selection')
173
174 toggle_selection_direction: ({vim}) ->
175 vim._run('toggle_selection_direction')
176
177 copy_selection_and_exit: ({vim}) ->
178 vim._run('get_selection', null, (selection) ->
179 # If the selection consists of newlines only, it _looks_ as if the
180 # selection is collapsed, so don’t try to copy it in that case.
181 if /^\n*$/.test(selection)
182 vim.notify(translate('notification.copy_selection_and_exit.none'))
183 else
184 # Trigger this copying command instead of putting `selection` into the
185 # clipboard, since `window.getSelection().toString()` sadly collapses
186 # whitespace in `<pre>` elements.
187 vim.window.goDoCommand('cmd_copy')
188 vim.enterMode('normal')
189 )
190
191 exit: ({vim}) ->
192 vim.enterMode('normal')
193 })
194
195
196
197 mode('hints', {
198 onEnter: ({vim, storage}, markers, callback, count = 1, sleep = -1) ->
199 storage.markers = markers
200 storage.markerMap = null
201 storage.callback = callback
202 storage.count = count
203 storage.numEnteredChars = 0
204 storage.markEverything = null
205
206 if sleep >= 0
207 storage.clearInterval = utils.interval(vim.window, sleep, (next) ->
208 unless storage.markerMap
209 next()
210 return
211 vim._send('getMarkableElementsMovements', null, (diffs) ->
212 for {dx, dy}, index in diffs when not (dx == 0 and dy == 0)
213 storage.markerMap[index].updatePosition(dx, dy)
214 next()
215 )
216 )
217
218 # Expose the storage so asynchronously computed markers can be set
219 # retroactively.
220 return storage
221
222 onLeave: ({vim, storage}) ->
223 # When clicking VimFx’s disable button in the Add-ons Manager, `hints` will
224 # have been `null`ed out when the timeout has passed.
225 vim.window.setTimeout(
226 (-> hints?.removeHints(vim.window)),
227 vim.options.hints_timeout
228 )
229 storage.clearInterval?()
230 for key of storage
231 storage[key] = null
232 return
233
234 onInput: (args, match) ->
235 {vim, storage} = args
236 {markers, callback} = storage
237
238 if match.type == 'full'
239 match.command.run(args)
240 else if match.unmodifiedKey in vim.options.hint_chars and markers.length > 0
241 matchedMarkers = []
242
243 for marker in markers when marker.hintIndex == storage.numEnteredChars
244 matched = marker.matchHintChar(match.unmodifiedKey)
245 marker.hide() unless matched
246 if marker.isMatched()
247 marker.markMatched(true)
248 matchedMarkers.push(marker)
249
250 if matchedMarkers.length > 0
251 again = callback(matchedMarkers[0], storage.count, match.keyStr)
252 storage.count -= 1
253 if again
254 vim.window.setTimeout((->
255 marker.markMatched(false) for marker in matchedMarkers
256 return
257 ), vim.options.hints_timeout)
258 marker.reset() for marker in markers
259 storage.numEnteredChars = 0
260 else
261 # The callback might have entered another mode. Only go back to Normal
262 # mode if we’re still in Hints mode.
263 vim.enterMode('normal') if vim.mode == 'hints'
264 else
265 storage.numEnteredChars += 1
266
267 return true
268
269 }, {
270 exit: ({vim, storage}) ->
271 # The hints are removed automatically when leaving the mode, but after a
272 # timeout. When aborting the mode we should remove the hints immediately.
273 hints.removeHints(vim.window)
274 vim.enterMode('normal')
275
276 rotate_markers_forward: ({storage}) ->
277 rotateOverlappingMarkers(storage.markers, true)
278
279 rotate_markers_backward: ({storage}) ->
280 rotateOverlappingMarkers(storage.markers, false)
281
282 delete_hint_char: ({storage}) ->
283 for marker in storage.markers
284 switch marker.hintIndex - storage.numEnteredChars
285 when 0
286 marker.deleteHintChar()
287 when -1
288 marker.show()
289 storage.numEnteredChars -= 1 unless storage.numEnteredChars == 0
290
291 increase_count: ({storage}) -> storage.count += 1
292
293 mark_everything: ({storage}) ->
294 storage.markEverything?()
295 })
296
297
298
299 mode('ignore', {
300 onEnter: ({vim, storage}, {count = null, type = null} = {}) ->
301 storage.count = count
302
303 # Keep last `.type` if no type was given. This is useful when returning to
304 # Ignore mode after runnning the `unquote` command.
305 if type
306 storage.type = type
307 else
308 storage.type ?= 'explicit'
309
310 onLeave: ({vim, storage}) ->
311 unless storage.count? or storage.type == 'focusType'
312 vim._run('blur_active_element')
313
314 onInput: (args, match) ->
315 {vim, storage} = args
316 switch storage.count
317 when null
318 if match.type == 'full'
319 match.command.run(args)
320 return true
321 when 1
322 vim.enterMode('normal')
323 else
324 storage.count -= 1
325 return false
326
327 }, {
328 exit: ({vim, storage}) ->
329 storage.type = null
330 vim.enterMode('normal')
331 unquote: ({vim}) ->
332 vim.enterMode('normal', {returnTo: 'ignore'})
333 })
334
335
336
337 mode('find', {
338 onEnter: ->
339
340 onLeave: ({vim}) ->
341 findBar = vim.window.gBrowser.getFindBar()
342 findStorage.lastSearchString = findBar._findField.value
343
344 onInput: (args, match) ->
345 args.findBar = args.vim.window.gBrowser.getFindBar()
346 if match.type == 'full'
347 match.command.run(args)
348 return true
349 return false
350
351 }, {
352 exit: ({findBar}) -> findBar.close()
353 })
354
355
356
357 mode('marks', {
358 onEnter: ({storage}, callback) ->
359 storage.callback = callback
360
361 onLeave: ({storage}) ->
362 storage.callback = null
363
364 onInput: (args, match) ->
365 {vim, storage} = args
366 storage.callback(match.keyStr)
367 vim.enterMode('normal')
368 return true
369 })
Imprint / Impressum