]> git.gir.st - VimFx.git/blob - extension/lib/modes.coffee
Implement filtering hints by text and related changes
[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 if match.type == 'none' or
69 (match.likelyConflict and not match.specialKeys['<force>'])
70 match.discard()
71 if storage.returnTo
72 vim._enterMode(storage.returnTo)
73 storage.returnTo = null
74 # If you press `aa` (and `a` is a prefix key, but there’s no `aa`
75 # shortcut), don’t pass the second `a` to the page.
76 return not match.toplevel
77
78 if match.type == 'full'
79 match.command.run(args)
80
81 # If the command changed the mode, wait until coming back from that mode
82 # before switching to `storage.returnTo` if any (see `onEnter` above).
83 if storage.returnTo and vim.mode == 'normal'
84 vim._enterMode(storage.returnTo)
85 storage.returnTo = null
86
87 # At this point the match is either full, partial or part of a count. Then
88 # we always want to suppress, except for one case: The Escape key.
89 return true unless keyStr == '<escape>'
90
91 # Passing Escape through allows for stopping the loading of the page and
92 # closing many custom dialogs (and perhaps other things; Escape is a very
93 # commonly used key).
94 if uiEvent
95 # In browser UI the biggest reasons are allowing to reset the location bar
96 # when blurring it, and closing dialogs such as the “bookmark this page”
97 # dialog (<c-d>). However, an exception is made for the devtools (<c-K>).
98 # There, trying to unfocus the devtools using Escape would annoyingly
99 # open the split console.
100 return utils.isDevtoolsElement(uiEvent.originalTarget)
101 else
102 # In web pages content, an exception is made if an element that VimFx
103 # cares about is focused. That allows for blurring an input in a custom
104 # dialog without closing the dialog too.
105 return vim.focusType != 'none'
106
107 # Note that this special handling of Escape is only used in Normal mode.
108 # There are two reasons we might suppress it in other modes. If some custom
109 # dialog of a website is open, we should be able to cancel hint markers on
110 # it without closing it. Secondly, otherwise cancelling hint markers on
111 # Google causes its search bar to be focused.
112
113 }, commands)
114
115
116
117 helper_move_caret = (method, direction, {vim, storage, count = 1}) ->
118 vim._run('move_caret', {
119 method, direction, select: storage.select
120 count: if method == 'intraLineMove' then 1 else count
121 })
122
123 mode('caret', {
124 onEnter: ({vim, storage}, {select = false} = {}) ->
125 storage.select = select
126 storage.caretBrowsingPref = prefs.root.get(CARET_BROWSING_PREF)
127 prefs.root.set(CARET_BROWSING_PREF, true)
128 vim._run('enable_caret')
129
130 listener = ->
131 return unless newVim = vim._parent.getCurrentVim(vim.window)
132 prefs.root.set(
133 CARET_BROWSING_PREF,
134 if newVim.mode == 'caret' then true else storage.caretBrowsingPref
135 )
136 vim._parent.on('TabSelect', listener)
137 storage.removeListener = -> vim._parent.off('TabSelect', listener)
138
139 onLeave: ({vim, storage}) ->
140 prefs.root.set(CARET_BROWSING_PREF, storage.caretBrowsingPref)
141 vim._run('clear_selection')
142 storage.removeListener?()
143 storage.removeListener = null
144
145 onInput: (args, match) ->
146 if match.type == 'full'
147 match.command.run(args)
148 return true
149 return false
150
151 }, {
152 # coffeelint: disable=colon_assignment_spacing
153 move_left: helper_move_caret.bind(null, 'characterMove', BACKWARD)
154 move_right: helper_move_caret.bind(null, 'characterMove', FORWARD)
155 move_down: helper_move_caret.bind(null, 'lineMove', FORWARD)
156 move_up: helper_move_caret.bind(null, 'lineMove', BACKWARD)
157 move_word_left: helper_move_caret.bind(null, 'wordMoveAdjusted', BACKWARD)
158 move_word_right: helper_move_caret.bind(null, 'wordMoveAdjusted', FORWARD)
159 move_to_line_start: helper_move_caret.bind(null, 'intraLineMove', BACKWARD)
160 move_to_line_end: helper_move_caret.bind(null, 'intraLineMove', FORWARD)
161 # coffeelint: enable=colon_assignment_spacing
162
163 toggle_selection: ({vim, storage}) ->
164 storage.select = not storage.select
165 if storage.select
166 vim.notify(translate('notification.toggle_selection.enter'))
167 else
168 vim._run('collapse_selection')
169
170 toggle_selection_direction: ({vim}) ->
171 vim._run('toggle_selection_direction')
172
173 copy_selection_and_exit: ({vim}) ->
174 vim._run('get_selection', null, (selection) ->
175 # If the selection consists of newlines only, it _looks_ as if the
176 # selection is collapsed, so don’t try to copy it in that case.
177 if /^\n*$/.test(selection)
178 vim.notify(translate('notification.copy_selection_and_exit.none'))
179 else
180 # Trigger this copying command instead of putting `selection` into the
181 # clipboard, since `window.getSelection().toString()` sadly collapses
182 # whitespace in `<pre>` elements.
183 vim.window.goDoCommand('cmd_copy')
184 vim._enterMode('normal')
185 )
186
187 exit: ({vim}) ->
188 vim._enterMode('normal')
189 })
190
191
192
193 mode('hints', {
194 onEnter: ({vim, storage}, options) ->
195 {
196 markerContainer, callback, matchText = true, count = 1, sleep = -1
197 } = options
198 storage.markerContainer = markerContainer
199 storage.callback = callback
200 storage.matchText = matchText
201 storage.count = count
202 storage.skipOnLeaveCleanup = false
203
204 if matchText
205 markerContainer.visualFeedbackUpdater =
206 hintsMode.updateVisualFeedback.bind(null, vim)
207 vim._run('clear_selection')
208
209 if sleep >= 0
210 storage.clearInterval = utils.interval(vim.window, sleep, (next) ->
211 if markerContainer.markers.length == 0
212 next()
213 return
214 vim._send('getMarkableElementsMovements', null, (diffs) ->
215 for {dx, dy}, index in diffs when not (dx == 0 and dy == 0)
216 markerContainer.markerMap[index].updatePosition(dx, dy)
217 next()
218 )
219 )
220
221 onLeave: ({vim, storage}) ->
222 hintsMode.cleanup(vim, storage) unless storage.skipOnLeaveCleanup
223
224 onInput: (args, match) ->
225 {vim, storage} = args
226 {markerContainer, callback, matchText} = storage
227 changed = false
228 visibleMarkers = null
229
230 if match.type == 'full'
231 match.command.run(Object.assign({match}, args))
232
233 else
234 {char, isHintChar} = hintsMode.getChar(match, storage)
235 return true unless char
236
237 visibleMarkers = markerContainer.addChar(char, isHintChar)
238
239 if (vim.options['hints.auto_activate'] or isHintChar) and
240 new Set(visibleMarkers.map((marker) -> marker.hint)).size == 1
241 hintsMode.activateMatch(
242 vim, storage, match, visibleMarkers, callback
243 )
244
245 unless isHintChar
246 vim._parent.ignoreKeyEventsUntilTime =
247 Date.now() + vim.options['hints.timeout']
248
249 return true
250
251 }, {
252 exit: ({vim}) ->
253 vim._enterMode('normal')
254
255 activate_highlighted: ({vim, storage, match}) ->
256 {markerContainer: {markers, highlightedMarkers}, callback} = storage
257 return if highlightedMarkers.length == 0
258
259 for marker in markers when marker.visible
260 marker.hide() unless marker in highlightedMarkers
261
262 hintsMode.activateMatch(
263 vim, storage, match, highlightedMarkers, callback
264 )
265
266 rotate_markers_forward: ({storage}) ->
267 storage.markerContainer.rotateOverlapping(true)
268
269 rotate_markers_backward: ({storage}) ->
270 storage.markerContainer.rotateOverlapping(false)
271
272 delete_char: ({storage}) ->
273 storage.markerContainer.deleteChar()
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 switch storage.count
305 when null
306 if match.type == 'full'
307 match.command.run(args)
308 return true
309 when 1
310 vim._enterMode('normal')
311 else
312 storage.count -= 1
313 return false
314
315 }, {
316 exit: ({vim, storage}) ->
317 storage.type = null
318 vim._enterMode('normal')
319 unquote: ({vim}) ->
320 vim._enterMode('normal', {returnTo: 'ignore'})
321 })
322
323
324
325 mode('find', {
326 onEnter: ->
327
328 onLeave: ({vim}) ->
329 findBar = vim.window.gBrowser.getFindBar()
330 findStorage.lastSearchString = findBar._findField.value
331
332 onInput: (args, match) ->
333 args.findBar = args.vim.window.gBrowser.getFindBar()
334 if match.type == 'full'
335 match.command.run(args)
336 return true
337 return false
338
339 }, {
340 exit: ({vim, findBar}) ->
341 vim._enterMode('normal')
342 findBar.close()
343 })
344
345
346
347 mode('marks', {
348 onEnter: ({vim, storage}, callback) ->
349 storage.callback = callback
350 storage.timeoutId = vim.window.setTimeout((->
351 vim.hideNotification()
352 vim._enterMode('normal')
353 ), vim.options.timeout)
354
355 onLeave: ({vim, storage}) ->
356 storage.callback = null
357 vim.window.clearTimeout(storage.timeoutId) if storage.timeoutId?
358 storage.timeoutId = null
359
360 onInput: (args, match) ->
361 {vim, storage} = args
362 if match.type == 'full'
363 match.command.run(args)
364 else
365 storage.callback(match.keyStr)
366 vim._enterMode('normal')
367 return true
368 }, {
369 exit: ({vim}) ->
370 vim._enterMode('normal')
371 })
Imprint / Impressum