]> git.gir.st - VimFx.git/blob - extension/lib/modes.coffee
utils getFindBar() returns a promise
[VimFx.git] / extension / lib / modes.coffee
1 # This file defines VimFx’s modes, and their respective commands. The Normal
2 # mode commands are defined in commands.coffee, though.
3
4 {commands, findStorage} = require('./commands')
5 defaults = require('./defaults')
6 help = require('./help')
7 hintsMode = require('./hints-mode')
8 prefs = require('./prefs')
9 SelectionManager = require('./selection')
10 translate = require('./translate')
11 utils = require('./utils')
12
13 {FORWARD, BACKWARD} = SelectionManager
14
15 # Helper to create modes in a DRY way.
16 mode = (modeName, obj, commands = null) ->
17 obj.name = translate("mode.#{modeName}")
18 obj.order = defaults.mode_order[modeName]
19 obj.commands = {}
20 for commandName, fn of commands
21 pref = "mode.#{modeName}.#{commandName}"
22 obj.commands[commandName] = {
23 pref: defaults.BRANCH + pref
24 run: fn
25 category: defaults.categoryMap[pref]
26 description: translate(pref)
27 order: defaults.command_order[pref]
28 }
29 exports[modeName] = obj
30
31
32
33 mode('normal', {
34 onEnter: ({vim, storage}, {returnTo = null} = {}) ->
35 if returnTo
36 storage.returnTo = returnTo
37 else if storage.returnTo
38 vim._enterMode(storage.returnTo)
39 storage.returnTo = null
40
41 onLeave: ({vim}) ->
42 vim._run('clear_inputs')
43
44 onInput: (args, match) ->
45 {vim, storage, event} = args
46 {keyStr} = match
47 focusTypeBeforeCommand = vim.focusType
48
49 vim.hideNotification() if match.type in ['none', 'full']
50
51 if match.type == 'none' or
52 (match.likelyConflict and not match.specialKeys['<force>'])
53 match.discard()
54 if storage.returnTo
55 vim._enterMode(storage.returnTo)
56 storage.returnTo = null
57 # If you press `aa` (and `a` is a prefix key, but there’s no `aa`
58 # shortcut), don’t pass the second `a` to the page.
59 return not match.toplevel
60
61 if match.type == 'full'
62 match.command.run(args)
63
64 # If the command changed the mode, wait until coming back from that mode
65 # before switching to `storage.returnTo` if any (see `onEnter` above).
66 if storage.returnTo and vim.mode == 'normal'
67 vim._enterMode(storage.returnTo)
68 storage.returnTo = null
69
70 # At this point the match is either full, partial or part of a count. Then
71 # we always want to suppress, except for one case: The Escape key.
72 return true unless keyStr == '<escape>'
73
74 # Passing Escape through allows for stopping the loading of the page and
75 # closing many custom dialogs (and perhaps other things; Escape is a very
76 # commonly used key).
77 if vim.isUIEvent(event)
78 # In browser UI the biggest reasons are allowing to reset the location bar
79 # when blurring it, and closing dialogs such as the “bookmark this page”
80 # dialog (<c-d>). However, an exception is made for the devtools (<c-K>).
81 # There, trying to unfocus the devtools using Escape would annoyingly
82 # open the split console.
83 return utils.isDevtoolsElement(event.originalTarget)
84 else
85 # In web page content, an exception is made if an element that VimFx
86 # cares about is focused. That allows for blurring an input in a custom
87 # dialog without closing the dialog too. Note that running a command might
88 # change `vim.focusType`, which is why this saved value is used here.
89 return focusTypeBeforeCommand != 'none'
90
91 # Note that this special handling of Escape is only used in Normal mode.
92 # There are two reasons we might suppress it in other modes. If some custom
93 # dialog of a website is open, we should be able to cancel hint markers on
94 # it without closing it. Secondly, otherwise cancelling hint markers on
95 # Google causes its search bar to be focused.
96
97 }, commands)
98
99
100
101 helper_move_caret = (method, direction, {vim, storage, count = 1}) ->
102 vim._run('move_caret', {
103 method, direction, select: storage.select
104 count: if method == 'intraLineMove' then 1 else count
105 })
106
107 mode('caret', {
108 onEnter: ({vim, storage}, {select = false} = {}) ->
109 storage.select = select
110 vim._parent.resetCaretBrowsing(true)
111 vim._run('enable_caret')
112
113 listener = ->
114 return unless newVim = vim._parent.getCurrentVim(vim.window)
115 vim._parent.resetCaretBrowsing(
116 if newVim.mode == 'caret' then true else null
117 )
118 vim._parent.on('TabSelect', listener)
119 storage.removeListener = -> vim._parent.off('TabSelect', listener)
120
121 onLeave: ({vim, storage}) ->
122 vim._parent.resetCaretBrowsing()
123 vim._run('clear_selection')
124 storage.removeListener?()
125 storage.removeListener = null
126
127 onInput: (args, match) ->
128 args.vim.hideNotification()
129
130 # In case the user turns Caret Browsing off while in Caret mode.
131 args.vim._parent.resetCaretBrowsing(true)
132
133 switch match.type
134 when 'full'
135 match.command.run(args)
136 return true
137 when 'partial', 'count'
138 return true
139 return false
140
141 }, {
142 # coffeelint: disable=colon_assignment_spacing
143 move_left: helper_move_caret.bind(null, 'characterMove', BACKWARD)
144 move_right: helper_move_caret.bind(null, 'characterMove', FORWARD)
145 move_down: helper_move_caret.bind(null, 'lineMove', FORWARD)
146 move_up: helper_move_caret.bind(null, 'lineMove', BACKWARD)
147 move_word_left: helper_move_caret.bind(null, 'wordMoveAdjusted', BACKWARD)
148 move_word_right: helper_move_caret.bind(null, 'wordMoveAdjusted', FORWARD)
149 move_to_line_start: helper_move_caret.bind(null, 'intraLineMove', BACKWARD)
150 move_to_line_end: helper_move_caret.bind(null, 'intraLineMove', FORWARD)
151 # coffeelint: enable=colon_assignment_spacing
152
153 toggle_selection: ({vim, storage}) ->
154 storage.select = not storage.select
155 if storage.select
156 vim.notify(translate('notification.toggle_selection.enter'))
157 else
158 vim._run('collapse_selection')
159
160 toggle_selection_direction: ({vim}) ->
161 vim._run('toggle_selection_direction')
162
163 copy_selection_and_exit: ({vim}) ->
164 vim._run('get_selection', null, (selection) ->
165 # If the selection consists of newlines only, it _looks_ as if the
166 # selection is collapsed, so don’t try to copy it in that case.
167 if /^\n*$/.test(selection)
168 vim.notify(translate('notification.copy_selection_and_exit.none'))
169 else
170 # Trigger this copying command instead of putting `selection` into the
171 # clipboard, since `window.getSelection().toString()` sadly collapses
172 # whitespace in `<pre>` elements.
173 vim.window.goDoCommand('cmd_copy')
174 vim._enterMode('normal')
175 )
176
177 exit: ({vim}) ->
178 vim._enterMode('normal')
179 })
180
181
182
183 mode('hints', {
184 onEnter: ({vim, storage}, options) ->
185 {
186 markerContainer, callback, matchText = true, count = 1, sleep = -1
187 } = options
188 storage.markerContainer = markerContainer
189 storage.callback = callback
190 storage.matchText = matchText
191 storage.count = count
192 storage.isMatched = {byText: false, byHint: false}
193 storage.skipOnLeaveCleanup = false
194
195 if matchText
196 markerContainer.visualFeedbackUpdater =
197 hintsMode.updateVisualFeedback.bind(null, vim)
198 vim._run('clear_selection')
199
200 if sleep >= 0
201 storage.clearInterval = utils.interval(vim.window, sleep, (next) ->
202 if markerContainer.markers.length == 0
203 next()
204 return
205 vim._send('getMarkableElementsMovements', null, (diffs) ->
206 for {dx, dy}, index in diffs when not (dx == 0 and dy == 0)
207 markerContainer.markerMap[index].updatePosition(dx, dy)
208 next()
209 )
210 )
211
212 onLeave: ({vim, storage}) ->
213 hintsMode.cleanup(vim, storage) unless storage.skipOnLeaveCleanup
214
215 onInput: (args, match) ->
216 {vim, storage} = args
217 {markerContainer, callback} = storage
218
219 switch match.type
220 when 'full'
221 match.command.run(Object.assign({match}, args))
222
223 when 'none', 'count'
224 # Make sure notifications for counts aren’t shown.
225 vim._refreshPersistentNotification()
226
227 {char, isHintChar} = hintsMode.getChar(match, storage)
228 return true unless char
229
230 return true if storage.isMatched.byText and not isHintChar
231
232 visibleMarkers = markerContainer.addChar(char, isHintChar)
233 storage.isMatched = hintsMode.isMatched(visibleMarkers, markerContainer)
234
235 if (storage.isMatched.byHint and isHintChar) or
236 (storage.isMatched.byText and not isHintChar and
237 vim.options['hints.auto_activate'])
238 hintsMode.activateMatch(
239 vim, storage, match, visibleMarkers, callback
240 )
241
242 unless isHintChar
243 vim._parent.ignoreKeyEventsUntilTime =
244 Date.now() + vim.options['hints.timeout']
245
246 return true
247
248 }, {
249 exit: ({vim}) ->
250 vim._enterMode('normal')
251
252 activate_highlighted: ({vim, storage, match}) ->
253 {markerContainer: {markers, highlightedMarkers}, callback} = storage
254 return if highlightedMarkers.length == 0
255
256 for marker in markers when marker.visible
257 marker.hide() unless marker in highlightedMarkers
258
259 hintsMode.activateMatch(
260 vim, storage, match, highlightedMarkers, callback
261 )
262
263 rotate_markers_forward: ({storage}) ->
264 storage.markerContainer.rotateOverlapping(true)
265
266 rotate_markers_backward: ({storage}) ->
267 storage.markerContainer.rotateOverlapping(false)
268
269 delete_char: ({storage}) ->
270 {markerContainer} = storage
271 visibleMarkers = markerContainer.deleteChar()
272 storage.isMatched =
273 hintsMode.isMatched(visibleMarkers or [], markerContainer)
274
275 increase_count: ({storage}) ->
276 storage.count += 1
277 # Uncomment this line if you want to use `gulp hints.html`!
278 # utils.writeToClipboard(storage.markerContainer.container.outerHTML)
279
280 toggle_complementary: ({storage}) ->
281 storage.markerContainer.toggleComplementary()
282 })
283
284
285
286 mode('ignore', {
287 onEnter: ({vim, storage}, {count = null, type = null} = {}) ->
288 storage.count = count
289
290 # Keep last `.type` if no type was given. This is useful when returning to
291 # Ignore mode after runnning the `unquote` command.
292 if type
293 storage.type = type
294 else
295 storage.type ?= 'explicit'
296
297 onLeave: ({vim, storage}) ->
298 unless storage.count? or storage.type == 'focusType'
299 vim._run('blur_active_element')
300
301 onInput: (args, match) ->
302 {vim, storage} = args
303 args.count = 1
304
305 switch storage.count
306 when null
307 switch match.type
308 when 'full'
309 match.command.run(args)
310 return true
311 when 'partial'
312 return true
313
314 # Make sure notifications for counts aren’t shown.
315 vim.hideNotification()
316 return false
317
318 when 1
319 vim._enterMode('normal')
320
321 else
322 storage.count -= 1
323
324 return false
325
326 }, {
327 exit: ({vim, storage}) ->
328 storage.type = null
329 vim._enterMode('normal')
330
331 unquote: ({vim}) ->
332 vim._enterMode('normal', {returnTo: 'ignore'})
333 })
334
335
336
337 mode('find', {
338 onEnter: ->
339
340 onLeave: ({vim}) ->
341 utils.getFindBar(vim.window.gBrowser).then((findBar) ->
342 findStorage.lastSearchString = findBar._findField.value
343 findStorage.busy = false
344 )
345
346 onInput: (args, match) ->
347 {vim} = args
348 switch
349 when match.type == 'full'
350 utils.getFindBar(vim.window.gBrowser).then((findBar) ->
351 args.findBar = findBar
352 match.command.run(args)
353 )
354 return true
355 when match.type == 'partial'
356 return true
357 when vim.focusType != 'findbar'
358 # If we’re in Find mode but the find bar input hasn’t been focused yet,
359 # suppress all input, because we don’t want to trigger Firefox commands,
360 # such as `/` (which opens the Quick Find bar). This happens when
361 # `helper_find_from_top_of_viewport` is slow, or when _Firefox_ is slow,
362 # for example to due to heavy page loading. The following URL is a good
363 # stress test: <https://html.spec.whatwg.org/>
364 findStorage.busy = true
365 return true
366 else
367 # At this point we know for sure that the find bar is not busy anymore.
368 findStorage.busy = false
369 return false
370
371 }, {
372 exit: ({vim, findBar}) ->
373 vim._enterMode('normal')
374 findBar.close()
375 })
376
377
378
379 mode('marks', {
380 onEnter: ({vim, storage}, callback) ->
381 storage.callback = callback
382 storage.timeoutId = vim.window.setTimeout((->
383 vim.hideNotification()
384 vim._enterMode('normal')
385 ), vim.options.timeout)
386
387 onLeave: ({vim, storage}) ->
388 storage.callback = null
389 vim.window.clearTimeout(storage.timeoutId) if storage.timeoutId?
390 storage.timeoutId = null
391
392 onInput: (args, match) ->
393 {vim, storage} = args
394 switch match.type
395 when 'full'
396 match.command.run(args)
397 when 'none', 'count'
398 storage.callback(match.keyStr)
399 vim._enterMode('normal')
400 return true
401
402 }, {
403 exit: ({vim}) ->
404 vim.hideNotification()
405 vim._enterMode('normal')
406 })
Imprint / Impressum