2 # Copyright Simon Lydell 2015.
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 key presses 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')
32 class VimFx extends utils.EventEmitter
33 constructor: (@modes, @options) ->
38 @on('modeChange', ({mode}) => @reset(mode))
40 SPECIAL_KEYS: ['<force>', '<late>']
43 @vims.set(browser, new Vim(browser, this))
45 # NOTE: This method is often called in event handlers. Many events may fire
46 # before a `vim` object has been created for the current tab yet (such as when
47 # the browser is starting up). Therefore always check if anything was
50 # return unless vim = @vimfx.getCurrentVim(@window)
51 getCurrentVim: (window) -> @vims.get(window.gBrowser.selectedBrowser)
53 reset: (mode = null) ->
54 @currentKeyTree = if mode then @keyTrees[mode] else {}
61 @commandsWithSpecialKeys
63 } = createKeyTrees(@getGroupedCommands(), @SPECIAL_KEYS)
65 stringifyKeyEvent: (event) ->
66 return notation.stringify(event, {
67 ignoreKeyboardLayout: @options.ignore_keyboard_layout
68 translations: @options.translations
71 consumeKeyEvent: (event, vim, focusType) ->
73 return unless keyStr = @stringifyKeyEvent(event)
76 @reset(mode) if now - @lastInputTime >= @options.timeout
79 toplevel = (@currentKeyTree == @keyTrees[mode])
81 if toplevel and @options.keyValidator
82 unless @options.keyValidator(keyStr, mode)
90 when keyStr of @currentKeyTree and
91 not (toplevel and keyStr == '0' and @count != '')
92 next = @currentKeyTree[keyStr]
93 if next instanceof Leaf
95 command = next.command
97 @currentKeyTree = next
100 when toplevel and DIGIT.test(keyStr) and
101 not (keyStr == '0' and @count == '')
108 count = if @count == '' then undefined else Number(@count)
109 specialKeys = @commandsWithSpecialKeys[command?.pref] ? {}
110 focus = @adjustFocusType(event, vim, focusType, keyStr)
111 unmodifiedKey = notation.parse(keyStr).key
112 @reset(mode) if type == 'full'
114 type, focus, command, count, specialKeys, keyStr, unmodifiedKey, toplevel
117 adjustFocusType: (event, vim, focusType, keyStr) ->
118 # Frame scripts and the tests don’t pass in `originalTarget`.
119 document = event.originalTarget?.ownerDocument
120 if focusType == null and document and
121 # TODO: Remove when Tab Groups have been removed.
122 (vim.window.TabView?.isVisible() or
123 document.fullscreenElement or document.mozFullScreenElement)
126 keys = @options["#{focusType}_element_keys"]
127 return null if keys and keyStr not in keys
131 getGroupedCommands: (options = {}) ->
133 for modeName, mode of @modes
134 if options.enabledOnly
135 usedSequences = getUsedSequences(@keyTrees[modeName])
136 for commandName, command of mode.commands
137 enabledSequences = null
138 if options.enabledOnly
139 enabledSequences = utils.removeDuplicates(
140 command._sequences.filter((sequence) ->
141 return (usedSequences[sequence] == command.pref)
144 continue if enabledSequences.length == 0
145 categories = modes[modeName] ?= {}
146 category = categories[command.category] ?= []
148 {command, enabledSequences, order: command.order, name: commandName}
152 for modeName, categories of modes
153 categoriesSorted = []
154 for categoryName, commands of categories
155 category = @options.categories[categoryName]
156 categoriesSorted.push({
157 name: category.name()
159 order: category.order
160 commands: commands.sort(byOrder)
162 mode = @modes[modeName]
167 categories: categoriesSorted.sort(byOrder)
169 return modesSorted.sort(byOrder)
171 byOrder = (a, b) -> a.order - b.order
174 constructor: (@command, @originalSequence) ->
176 createKeyTrees = (groupedCommands, specialKeys) ->
179 commandsWithSpecialKeys = {}
181 pushError = (error, command) ->
182 (errors[command.pref] ?= []).push(error)
184 pushOverrideErrors = (command, tree) ->
185 {command: overridingCommand, originalSequence} = getFirstLeaf(tree)
188 subject: overridingCommand.description()
189 context: originalSequence
190 pushError(error, command)
192 pushSpecialKeyError = (command, originalSequence, key) ->
194 id: 'illegal_special_key'
196 context: originalSequence
197 pushError(error, command)
199 for mode in groupedCommands
200 keyTrees[mode._name] = {}
201 for category in mode.categories then for {command} in category.commands
202 {shortcuts, errors: parseErrors} = parseShortcutPref(command.pref)
203 pushError(error, command) for error in parseErrors
204 command._sequences = []
206 for shortcut in shortcuts
207 [prefixKeys..., lastKey] = shortcut.normalized
208 tree = keyTrees[mode._name]
209 command._sequences.push(shortcut.original)
210 seenNonSpecialKey = false
213 for prefixKey, index in prefixKeys
214 if prefixKey in specialKeys
216 pushSpecialKeyError(command, shortcut.original, prefixKey)
220 (commandsWithSpecialKeys[command.pref] ?= {})[prefixKey] = true
223 seenNonSpecialKey = true
226 next = tree[prefixKey]
227 if next instanceof Leaf
228 pushOverrideErrors(command, next)
234 tree = tree[prefixKey] = {}
237 if lastKey in specialKeys
238 subject = if seenNonSpecialKey then lastKey else shortcut.original
239 pushSpecialKeyError(command, shortcut.original, subject)
242 pushOverrideErrors(command, tree[lastKey])
244 tree[lastKey] = new Leaf(command, shortcut.original)
246 return {keyTrees, commandsWithSpecialKeys, errors}
248 parseShortcutPref = (pref) ->
252 # The shorcut prefs are read from root in order to support other extensions to
253 # extend VimFx with custom commands.
254 prefValue = prefs.root.get(pref).trim()
256 unless prefValue == ''
257 for sequence in prefValue.split(/\s+/)
260 for key in notation.parseSequence(sequence)
262 shortcut.push(notation.normalize(key))
264 throw error unless error.id?
268 shortcuts.push({normalized: shortcut, original: sequence}) unless errored
270 return {shortcuts, errors}
272 getFirstLeaf = (node) ->
273 if node instanceof Leaf
275 for key, value of node
276 return getFirstLeaf(value)
278 getLeaves = (node) ->
279 if node instanceof Leaf
282 for key, value of node
283 leaves.push(getLeaves(value)...)
286 getUsedSequences = (tree) ->
288 for leaf in getLeaves(tree)
289 usedSequences[leaf.originalSequence] = leaf.command.pref
292 module.exports = VimFx