]> git.gir.st - VimFx.git/blob - extension/lib/modes.coffee
Ignore counts in Ignore mode
[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 translate = require('./l10n')
29 prefs = require('./prefs')
30 SelectionManager = require('./selection')
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}, options = {}) ->
56 if options.returnTo
57 storage.returnTo = options.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 {markerContainer} = storage
215 vim.window.setTimeout(
216 (-> markerContainer.remove()),
217 vim.options.hints_timeout
218 )
219 storage.clearInterval?()
220 for key of storage
221 storage[key] = null
222 return
223
224 onInput: (args, match) ->
225 {vim, storage} = args
226 {markerContainer, callback} = storage
227
228 if match.type == 'full'
229 match.command.run(args)
230 else if match.unmodifiedKey in vim.options.hint_chars
231 matchedMarkers = markerContainer.matchHintChar(match.unmodifiedKey)
232 if matchedMarkers.length > 0
233 again = callback(matchedMarkers[0], storage.count, match.keyStr)
234 storage.count -= 1
235 if again
236 vim.window.setTimeout((->
237 marker.markMatched(false) for marker in matchedMarkers
238 return
239 ), vim.options.hints_timeout)
240 markerContainer.reset()
241 else
242 # The callback might have entered another mode. Only go back to Normal
243 # mode if we’re still in Hints mode.
244 vim.enterMode('normal') if vim.mode == 'hints'
245
246 return true
247
248 }, {
249 exit: ({vim, storage}) ->
250 # The hints are removed automatically when leaving the mode, but after a
251 # timeout. When aborting the mode we should remove the hints immediately.
252 storage.markerContainer.remove()
253 vim.enterMode('normal')
254
255 rotate_markers_forward: ({storage}) ->
256 storage.markerContainer.rotateOverlapping(true)
257
258 rotate_markers_backward: ({storage}) ->
259 storage.markerContainer.rotateOverlapping(false)
260
261 delete_hint_char: ({storage}) ->
262 storage.markerContainer.deleteHintChar()
263
264 increase_count: ({storage}) ->
265 storage.count += 1
266
267 toggle_complementary: ({storage}) ->
268 storage.markerContainer.toggleComplementary()
269 })
270
271
272
273 mode('ignore', {
274 onEnter: ({vim, storage}, {count = null, type = null} = {}) ->
275 storage.count = count
276
277 # Keep last `.type` if no type was given. This is useful when returning to
278 # Ignore mode after runnning the `unquote` command.
279 if type
280 storage.type = type
281 else
282 storage.type ?= 'explicit'
283
284 onLeave: ({vim, storage}) ->
285 unless storage.count? or storage.type == 'focusType'
286 vim._run('blur_active_element')
287
288 onInput: (args, match) ->
289 {vim, storage} = args
290 args.count = 1
291 switch storage.count
292 when null
293 if match.type == 'full'
294 match.command.run(args)
295 return true
296 when 1
297 vim.enterMode('normal')
298 else
299 storage.count -= 1
300 return false
301
302 }, {
303 exit: ({vim, storage}) ->
304 storage.type = null
305 vim.enterMode('normal')
306 unquote: ({vim}) ->
307 vim.enterMode('normal', {returnTo: 'ignore'})
308 })
309
310
311
312 mode('find', {
313 onEnter: ->
314
315 onLeave: ({vim}) ->
316 findBar = vim.window.gBrowser.getFindBar()
317 findStorage.lastSearchString = findBar._findField.value
318
319 onInput: (args, match) ->
320 args.findBar = args.vim.window.gBrowser.getFindBar()
321 if match.type == 'full'
322 match.command.run(args)
323 return true
324 return false
325
326 }, {
327 exit: ({vim, findBar}) ->
328 vim.enterMode('normal')
329 findBar.close()
330 })
331
332
333
334 mode('marks', {
335 onEnter: ({vim, storage}, callback) ->
336 storage.callback = callback
337 storage.timeoutId = vim.window.setTimeout((->
338 vim.hideNotification()
339 vim.enterMode('normal')
340 ), vim.options.timeout)
341
342 onLeave: ({vim, storage}) ->
343 storage.callback = null
344 vim.window.clearTimeout(storage.timeoutId) if storage.timeoutId?
345 storage.timeoutId = null
346
347 onInput: (args, match) ->
348 {vim, storage} = args
349 if match.type == 'full'
350 match.command.run(args)
351 else
352 storage.callback(match.keyStr)
353 vim.enterMode('normal')
354 return true
355 }, {
356 exit: ({vim}) ->
357 vim.enterMode('normal')
358 })
Imprint / Impressum