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