]> git.gir.st - VimFx.git/blob - extension/lib/vimfx.coffee
Ignore Lock keys such as NumLock and CapsLock
[VimFx.git] / extension / lib / vimfx.coffee
1 ###
2 # Copyright Simon Lydell 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 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.
24
25 notation = require('vim-like-key-notation')
26 prefs = require('./prefs')
27 utils = require('./utils')
28 Vim = require('./vim')
29
30 DIGIT = /^\d$/
31
32 class VimFx extends utils.EventEmitter
33 constructor: (@modes, @options) ->
34 super()
35 @vims = new WeakMap()
36 @lastClosedVim = null
37 @goToCommand = null
38 @skipCreateKeyTrees = false
39 @createKeyTrees()
40 @reset()
41 @on('modeChange', ({vim}) => @reset(vim.mode))
42
43 SPECIAL_KEYS: {
44 '<force>': {}
45 '<late>': {single: true}
46 }
47
48 addVim: (browser) ->
49 vim = new Vim(browser, this)
50 @vims.set(browser, vim)
51 # Calling `vim._start` will emit VimFx events. It might seem as if the logic
52 # of `vim._start` could be moved into the constructor, but splitting it like
53 # this allows to save the `vim` instance in `vimfx.vims` first, which in
54 # turn allows `vimfx.on(...)` listeners to use `vimfx.getCurrentVim()`.
55 vim._start()
56
57 # NOTE: This method is often called in event handlers. Many events may fire
58 # before a `vim` object has been created for the current tab yet (such as when
59 # the browser is starting up). Therefore always check if anything was
60 # returned, such as:
61 #
62 # return unless vim = @vimfx.getCurrentVim(@window)
63 getCurrentVim: (window) -> @vims.get(window.gBrowser.selectedBrowser)
64
65 reset: (mode = null) ->
66 # Modes without commands are returned by neither `.getGroupedCommands()` nor
67 # `createKeyTrees`. Fall back to an empty tree.
68 @currentKeyTree = @keyTrees[mode] ? {}
69 @lastInputTime = 0
70 @count = ''
71
72 createKeyTrees: ->
73 return if @skipCreateKeyTrees
74 {@keyTrees, @errors} = createKeyTrees(@getGroupedCommands(), @SPECIAL_KEYS)
75
76 stringifyKeyEvent: (event) ->
77 return '' if event.key.endsWith('Lock')
78 return notation.stringify(event, {
79 ignoreCtrlAlt: @options.ignore_ctrl_alt
80 ignoreKeyboardLayout: @options.ignore_keyboard_layout
81 translations: @options.translations
82 })
83
84 consumeKeyEvent: (event, vim) ->
85 {mode} = vim
86 return unless keyStr = @stringifyKeyEvent(event)
87
88 now = Date.now()
89 @reset(mode) if now - @lastInputTime >= @options.timeout
90 @lastInputTime = now
91
92 toplevel = (@currentKeyTree == @keyTrees[mode])
93
94 if toplevel and @options.keyValidator
95 unless @options.keyValidator(keyStr, mode)
96 @reset(mode)
97 return
98
99 type = 'none'
100 command = null
101 specialKeys = {}
102
103 switch
104 when keyStr of @currentKeyTree and
105 not (toplevel and keyStr == '0' and @count != '')
106 next = @currentKeyTree[keyStr]
107 if next instanceof Leaf
108 type = 'full'
109 {command, specialKeys} = next
110 else
111 @currentKeyTree = next
112 type = 'partial'
113
114 when @options.counts_enabled and toplevel and DIGIT.test(keyStr) and
115 not (keyStr == '0' and @count == '')
116 @count += keyStr
117 type = 'count'
118
119 else
120 @reset(mode)
121
122 count = if @count == '' then undefined else Number(@count)
123 unmodifiedKey = notation.parse(keyStr).key
124
125 focusTypeKeys = @options["#{vim.focusType}_element_keys"]
126 likelyConflict =
127 if toplevel
128 if focusTypeKeys
129 keyStr in focusTypeKeys
130 else
131 vim.focusType != 'none'
132 else
133 false
134
135 @reset(mode) if type == 'full'
136 return {
137 type, command, count, specialKeys, keyStr, unmodifiedKey, toplevel
138 likelyConflict
139 discard: @reset.bind(this, mode)
140 }
141
142 getGroupedCommands: (options = {}) ->
143 modes = {}
144 for modeName, mode of @modes
145 if options.enabledOnly
146 usedSequences = getUsedSequences(@keyTrees[modeName])
147 for commandName, command of mode.commands
148 enabledSequences = null
149 if options.enabledOnly
150 enabledSequences = utils.removeDuplicates(
151 command._sequences.filter((sequence) ->
152 return (usedSequences[sequence] == command.pref)
153 )
154 )
155 continue if enabledSequences.length == 0
156 categories = modes[modeName] ?= {}
157 category = categories[command.category] ?= []
158 category.push(
159 {command, enabledSequences, order: command.order, name: commandName}
160 )
161
162 modesSorted = []
163 for modeName, categories of modes
164 categoriesSorted = []
165 for categoryName, commands of categories
166 category = @options.categories[categoryName]
167 categoriesSorted.push({
168 name: category.name
169 _name: categoryName
170 order: category.order
171 commands: commands.sort(byOrder)
172 })
173 mode = @modes[modeName]
174 modesSorted.push({
175 name: mode.name
176 _name: modeName
177 order: mode.order
178 categories: categoriesSorted.sort(byOrder)
179 })
180 return modesSorted.sort(byOrder)
181
182 byOrder = (a, b) -> a.order - b.order
183
184 class Leaf
185 constructor: (@command, @originalSequence, @specialKeys) ->
186
187 createKeyTrees = (groupedCommands, specialKeysSpec) ->
188 keyTrees = {}
189 errors = {}
190
191 pushError = (error, command) ->
192 (errors[command.pref] ?= []).push(error)
193
194 pushOverrideErrors = (command, originalSequence, tree) ->
195 {command: overridingCommand} = getFirstLeaf(tree)
196 error = {
197 id: 'overridden_by'
198 subject: overridingCommand.description
199 context: originalSequence
200 }
201 pushError(error, command)
202
203 pushSpecialKeyError = (command, id, originalSequence, key) ->
204 error = {
205 id: "special_key.#{id}"
206 subject: key
207 context: originalSequence
208 }
209 pushError(error, command)
210
211 for mode in groupedCommands
212 keyTrees[mode._name] = {}
213 for category in mode.categories then for {command} in category.commands
214 {shortcuts, errors: parseErrors} = parseShortcutPref(command.pref)
215 pushError(error, command) for error in parseErrors
216 command._sequences = []
217
218 for shortcut in shortcuts
219 [prefixKeys..., lastKey] = shortcut.normalized
220 tree = keyTrees[mode._name]
221 command._sequences.push(shortcut.original)
222 seenNonSpecialKey = false
223 specialKeys = {}
224
225 errored = false
226 for prefixKey, index in prefixKeys
227 if prefixKey of specialKeysSpec
228 if seenNonSpecialKey
229 pushSpecialKeyError(
230 command, 'prefix_only', shortcut.original, prefixKey
231 )
232 errored = true
233 break
234 else
235 specialKeys[prefixKey] = true
236 continue
237 else if not seenNonSpecialKey
238 for specialKey of specialKeys
239 options = specialKeysSpec[specialKey]
240 if options.single
241 pushSpecialKeyError(
242 command, 'single_only', shortcut.original, specialKey
243 )
244 errored = true
245 break
246 break if errored
247 seenNonSpecialKey = true
248
249 if prefixKey of tree
250 next = tree[prefixKey]
251 if next instanceof Leaf
252 pushOverrideErrors(command, shortcut.original, next)
253 errored = true
254 break
255 else
256 tree = next
257 else
258 tree = tree[prefixKey] = {}
259 continue if errored
260
261 if lastKey of specialKeysSpec
262 subject = if seenNonSpecialKey then lastKey else shortcut.original
263 pushSpecialKeyError(
264 command, 'prefix_only', shortcut.original, subject
265 )
266 continue
267 if lastKey of tree
268 pushOverrideErrors(command, shortcut.original, tree[lastKey])
269 continue
270 tree[lastKey] = new Leaf(command, shortcut.original, specialKeys)
271
272 return {keyTrees, errors}
273
274 parseShortcutPref = (pref) ->
275 shortcuts = []
276 errors = []
277
278 # The shorcut prefs are read from root in order to support other extensions to
279 # extend VimFx with custom commands.
280 prefValue = prefs.root.get(pref).trim()
281
282 unless prefValue == ''
283 for sequence in prefValue.split(/\s+/)
284 shortcut = []
285 errored = false
286 for key in notation.parseSequence(sequence)
287 try
288 shortcut.push(notation.normalize(key))
289 catch error
290 throw error unless error.id?
291 errors.push(error)
292 errored = true
293 break
294 shortcuts.push({normalized: shortcut, original: sequence}) unless errored
295
296 return {shortcuts, errors}
297
298 getFirstLeaf = (node) ->
299 if node instanceof Leaf
300 return node
301 for key, value of node
302 return getFirstLeaf(value)
303
304 getLeaves = (node) ->
305 if node instanceof Leaf
306 return [node]
307 leaves = []
308 for key, value of node
309 leaves.push(getLeaves(value)...)
310 return leaves
311
312 getUsedSequences = (tree) ->
313 usedSequences = {}
314 for leaf in getLeaves(tree)
315 usedSequences[leaf.originalSequence] = leaf.command.pref
316 return usedSequences
317
318 module.exports = VimFx
Imprint / Impressum