2 # Copyright Simon Lydell 2015, 2016.
4 # This file is part of VimFx.
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.
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.
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/>.
20 # This file defines a top-level object to hold global state for VimFx. It keeps
21 # track of all `Vim` instances (vim.coffee), all options and all keyboard
22 # shortcuts. It can consume keypresses according to its commands, and return
23 # the commands for UI presentation. There is only one `VimFx` instance.
25 notation = require('vim-like-key-notation')
26 prefs = require('./prefs')
27 utils = require('./utils')
28 Vim = require('./vim')
30 CARET_BROWSING_PREF = 'accessibility.browsewithcaret'
33 class VimFx extends utils.EventEmitter
34 constructor: (@modes, @options) ->
39 @skipObserveCaretBrowsing = false
40 @ignoreKeyEventsUntilTime = 0
41 @skipCreateKeyTrees = false
44 @observeCaretBrowsing()
45 @on('modeChange', ({vim}) => @reset(vim.mode))
49 '<late>': {single: true}
61 vim = new Vim(browser, this)
62 @vims.set(browser, vim)
63 # Calling `vim._start` will emit VimFx events. It might seem as if the logic
64 # of `vim._start` could be moved into the constructor, but splitting it like
65 # this allows to save the `vim` instance in `vimfx.vims` first, which in
66 # turn allows `vimfx.on(...)` listeners to use `vimfx.getCurrentVim()`.
69 # NOTE: This method is often called in event handlers. Many events may fire
70 # before a `vim` object has been created for the current tab yet (such as when
71 # the browser is starting up). Therefore always check if anything was
74 # return unless vim = @vimfx.getCurrentVim(@window)
75 getCurrentVim: (window) -> @vims.get(window.gBrowser.selectedBrowser)
77 reset: (mode = null) ->
78 # Modes without commands are returned by neither `.getGroupedCommands()` nor
79 # `createKeyTrees`. Fall back to an empty tree.
80 @currentKeyTree = @keyTrees[mode] ? {}
84 resetCaretBrowsing: (value = @options.browsewithcaret) ->
85 @skipObserveCaretBrowsing = true
86 prefs.root.set(CARET_BROWSING_PREF, value)
87 @skipObserveCaretBrowsing = false
89 observeCaretBrowsing: ->
90 prefs.root.observe(CARET_BROWSING_PREF, =>
91 return if @skipObserveCaretBrowsing
92 prefs.set('browsewithcaret', prefs.root.get(CARET_BROWSING_PREF))
96 return if @skipCreateKeyTrees
97 {@keyTrees, @errors} =
98 createKeyTrees(@getGroupedCommands(), @SPECIAL_KEYS, @MODIFIER_KEYS)
100 stringifyKeyEvent: (event) ->
101 return '' if event.key.endsWith('Lock')
102 return notation.stringify(event, {
103 ignoreCtrlAlt: @options.ignore_ctrl_alt
104 ignoreKeyboardLayout: @options.ignore_keyboard_layout
105 translations: @options.translations
108 consumeKeyEvent: (event, vim) ->
110 return unless keyStr = @stringifyKeyEvent(event)
114 return true if now <= @ignoreKeyEventsUntilTime
116 @reset(mode) if now - @lastInputTime >= @options.timeout
119 toplevel = (@currentKeyTree == @keyTrees[mode])
121 if toplevel and @options.keyValidator
122 unless @options.keyValidator(keyStr, mode)
131 when keyStr of @currentKeyTree and
132 not (toplevel and keyStr == '0' and @count != '')
133 next = @currentKeyTree[keyStr]
134 if next instanceof Leaf
136 {command, specialKeys} = next
138 @currentKeyTree = next
141 when @options.counts_enabled and toplevel and DIGIT.test(keyStr) and
142 not (keyStr == '0' and @count == '')
149 count = if @count == '' then undefined else Number(@count)
150 unmodifiedKey = notation.parse(keyStr).key
152 focusTypeKeys = @options["#{vim.focusType}_element_keys"]
156 keyStr in focusTypeKeys
158 vim.focusType != 'none'
162 @reset(mode) if type == 'full'
164 type, command, count, toplevel
165 specialKeys, keyStr, unmodifiedKey, likelyConflict
166 rawKey: event.key, rawCode: event.code
167 discard: @reset.bind(this, mode)
170 getGroupedCommands: (options = {}) ->
172 for modeName, mode of @modes
173 if options.enabledOnly
174 usedSequences = getUsedSequences(@keyTrees[modeName])
175 for commandName, command of mode.commands
176 enabledSequences = null
177 if options.enabledOnly
178 enabledSequences = utils.removeDuplicates(
179 command._sequences.filter((sequence) ->
180 return (usedSequences[sequence] == command.pref)
183 continue if enabledSequences.length == 0
184 categories = modes[modeName] ?= {}
185 category = categories[command.category] ?= []
187 {command, enabledSequences, order: command.order, name: commandName}
191 for modeName, categories of modes
192 categoriesSorted = []
193 for categoryName, commands of categories
194 category = @options.categories[categoryName]
195 categoriesSorted.push({
198 order: category.order
199 commands: commands.sort(byOrder)
201 mode = @modes[modeName]
206 categories: categoriesSorted.sort(byOrder)
208 return modesSorted.sort(byOrder)
210 byOrder = (a, b) -> a.order - b.order
213 constructor: (@command, @originalSequence, @specialKeys) ->
215 createKeyTrees = (groupedCommands, specialKeysSpec, modifierKeys) ->
219 pushError = (error, command) ->
220 (errors[command.pref] ?= []).push(error)
222 pushOverrideErrors = (command, originalSequence, tree) ->
223 {command: overridingCommand} = getFirstLeaf(tree)
226 subject: overridingCommand.description
227 context: originalSequence
229 pushError(error, command)
231 pushKeyError = (command, id, originalSequence, key) ->
232 pushError({id, subject: key, context: originalSequence}, command)
234 for mode in groupedCommands
235 keyTrees[mode._name] = {}
236 for category in mode.categories then for {command} in category.commands
237 {shortcuts, errors: parseErrors} = parseShortcutPref(command.pref)
238 pushError(error, command) for error in parseErrors
239 command._sequences = []
241 for shortcut in shortcuts
242 [prefixKeys..., lastKey] = shortcut.normalized
243 tree = keyTrees[mode._name]
244 command._sequences.push(shortcut.original)
245 seenNonSpecialKey = false
249 for prefixKey, index in prefixKeys
250 if prefixKey of modifierKeys
252 command, 'mistake_modifier', shortcut.original, prefixKey
256 if prefixKey of specialKeysSpec
259 command, 'special_key.prefix_only', shortcut.original, prefixKey
264 specialKeys[prefixKey] = true
266 unless seenNonSpecialKey
267 for specialKey of specialKeys
268 options = specialKeysSpec[specialKey]
271 command, 'special_key.single_only', shortcut.original,
277 seenNonSpecialKey = true
280 next = tree[prefixKey]
281 if next instanceof Leaf
282 pushOverrideErrors(command, shortcut.original, next)
288 tree = tree[prefixKey] = {}
292 if lastKey of modifierKeys
293 pushKeyError(command, 'lone_modifier', shortcut.original, lastKey)
296 if lastKey of specialKeysSpec
297 subject = if seenNonSpecialKey then lastKey else shortcut.original
299 command, 'special_key.prefix_only', shortcut.original, subject
303 pushOverrideErrors(command, shortcut.original, tree[lastKey])
305 tree[lastKey] = new Leaf(command, shortcut.original, specialKeys)
307 return {keyTrees, errors}
309 parseShortcutPref = (pref) ->
313 prefValue = prefs.root.get(pref).trim()
315 unless prefValue == ''
316 for sequence in prefValue.split(/\s+/)
319 for key in notation.parseSequence(sequence)
321 shortcut.push(notation.normalize(key))
323 throw error unless error.id?
327 shortcuts.push({normalized: shortcut, original: sequence}) unless errored
329 return {shortcuts, errors}
331 getFirstLeaf = (node) ->
332 if node instanceof Leaf
334 for key, value of node
335 return getFirstLeaf(value)
337 getLeaves = (node) ->
338 if node instanceof Leaf
341 for key, value of node
342 leaves.push(getLeaves(value)...)
345 getUsedSequences = (tree) ->
347 for leaf in getLeaves(tree)
348 usedSequences[leaf.originalSequence] = leaf.command.pref
351 module.exports = VimFx