]> git.gir.st - VimFx.git/blob - extension/lib/vimfx.coffee
Streamline `vimfx.on` events
[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 notation.stringify(event, {
78 ignoreCtrlAlt: @options.ignore_ctrl_alt
79 ignoreKeyboardLayout: @options.ignore_keyboard_layout
80 translations: @options.translations
81 })
82
83 consumeKeyEvent: (event, vim) ->
84 {mode} = vim
85 return unless keyStr = @stringifyKeyEvent(event)
86
87 now = Date.now()
88 @reset(mode) if now - @lastInputTime >= @options.timeout
89 @lastInputTime = now
90
91 toplevel = (@currentKeyTree == @keyTrees[mode])
92
93 if toplevel and @options.keyValidator
94 unless @options.keyValidator(keyStr, mode)
95 @reset(mode)
96 return
97
98 type = 'none'
99 command = null
100 specialKeys = {}
101
102 switch
103 when keyStr of @currentKeyTree and
104 not (toplevel and keyStr == '0' and @count != '')
105 next = @currentKeyTree[keyStr]
106 if next instanceof Leaf
107 type = 'full'
108 {command, specialKeys} = next
109 else
110 @currentKeyTree = next
111 type = 'partial'
112
113 when @options.counts_enabled and toplevel and DIGIT.test(keyStr) and
114 not (keyStr == '0' and @count == '')
115 @count += keyStr
116 type = 'count'
117
118 else
119 @reset(mode)
120
121 count = if @count == '' then undefined else Number(@count)
122 unmodifiedKey = notation.parse(keyStr).key
123
124 focusTypeKeys = @options["#{vim.focusType}_element_keys"]
125 likelyConflict =
126 if toplevel
127 if focusTypeKeys
128 keyStr in focusTypeKeys
129 else
130 vim.focusType != 'none'
131 else
132 false
133
134 @reset(mode) if type == 'full'
135 return {
136 type, command, count, specialKeys, keyStr, unmodifiedKey, toplevel
137 likelyConflict
138 discard: @reset.bind(this, mode)
139 }
140
141 getGroupedCommands: (options = {}) ->
142 modes = {}
143 for modeName, mode of @modes
144 if options.enabledOnly
145 usedSequences = getUsedSequences(@keyTrees[modeName])
146 for commandName, command of mode.commands
147 enabledSequences = null
148 if options.enabledOnly
149 enabledSequences = utils.removeDuplicates(
150 command._sequences.filter((sequence) ->
151 return (usedSequences[sequence] == command.pref)
152 )
153 )
154 continue if enabledSequences.length == 0
155 categories = modes[modeName] ?= {}
156 category = categories[command.category] ?= []
157 category.push(
158 {command, enabledSequences, order: command.order, name: commandName}
159 )
160
161 modesSorted = []
162 for modeName, categories of modes
163 categoriesSorted = []
164 for categoryName, commands of categories
165 category = @options.categories[categoryName]
166 categoriesSorted.push({
167 name: category.name
168 _name: categoryName
169 order: category.order
170 commands: commands.sort(byOrder)
171 })
172 mode = @modes[modeName]
173 modesSorted.push({
174 name: mode.name
175 _name: modeName
176 order: mode.order
177 categories: categoriesSorted.sort(byOrder)
178 })
179 return modesSorted.sort(byOrder)
180
181 byOrder = (a, b) -> a.order - b.order
182
183 class Leaf
184 constructor: (@command, @originalSequence, @specialKeys) ->
185
186 createKeyTrees = (groupedCommands, specialKeysSpec) ->
187 keyTrees = {}
188 errors = {}
189
190 pushError = (error, command) ->
191 (errors[command.pref] ?= []).push(error)
192
193 pushOverrideErrors = (command, originalSequence, tree) ->
194 {command: overridingCommand} = getFirstLeaf(tree)
195 error = {
196 id: 'overridden_by'
197 subject: overridingCommand.description
198 context: originalSequence
199 }
200 pushError(error, command)
201
202 pushSpecialKeyError = (command, id, originalSequence, key) ->
203 error = {
204 id: "special_key.#{id}"
205 subject: key
206 context: originalSequence
207 }
208 pushError(error, command)
209
210 for mode in groupedCommands
211 keyTrees[mode._name] = {}
212 for category in mode.categories then for {command} in category.commands
213 {shortcuts, errors: parseErrors} = parseShortcutPref(command.pref)
214 pushError(error, command) for error in parseErrors
215 command._sequences = []
216
217 for shortcut in shortcuts
218 [prefixKeys..., lastKey] = shortcut.normalized
219 tree = keyTrees[mode._name]
220 command._sequences.push(shortcut.original)
221 seenNonSpecialKey = false
222 specialKeys = {}
223
224 errored = false
225 for prefixKey, index in prefixKeys
226 if prefixKey of specialKeysSpec
227 if seenNonSpecialKey
228 pushSpecialKeyError(command, 'prefix_only', shortcut.original,
229 prefixKey)
230 errored = true
231 break
232 else
233 specialKeys[prefixKey] = true
234 continue
235 else if not seenNonSpecialKey
236 for specialKey of specialKeys
237 options = specialKeysSpec[specialKey]
238 if options.single
239 pushSpecialKeyError(command, 'single_only', shortcut.original,
240 specialKey)
241 errored = true
242 break
243 break if errored
244 seenNonSpecialKey = true
245
246 if prefixKey of tree
247 next = tree[prefixKey]
248 if next instanceof Leaf
249 pushOverrideErrors(command, shortcut.original, next)
250 errored = true
251 break
252 else
253 tree = next
254 else
255 tree = tree[prefixKey] = {}
256 continue if errored
257
258 if lastKey of specialKeysSpec
259 subject = if seenNonSpecialKey then lastKey else shortcut.original
260 pushSpecialKeyError(command, 'prefix_only', shortcut.original,
261 subject)
262 continue
263 if lastKey of tree
264 pushOverrideErrors(command, shortcut.original, tree[lastKey])
265 continue
266 tree[lastKey] = new Leaf(command, shortcut.original, specialKeys)
267
268 return {keyTrees, errors}
269
270 parseShortcutPref = (pref) ->
271 shortcuts = []
272 errors = []
273
274 # The shorcut prefs are read from root in order to support other extensions to
275 # extend VimFx with custom commands.
276 prefValue = prefs.root.get(pref).trim()
277
278 unless prefValue == ''
279 for sequence in prefValue.split(/\s+/)
280 shortcut = []
281 errored = false
282 for key in notation.parseSequence(sequence)
283 try
284 shortcut.push(notation.normalize(key))
285 catch error
286 throw error unless error.id?
287 errors.push(error)
288 errored = true
289 break
290 shortcuts.push({normalized: shortcut, original: sequence}) unless errored
291
292 return {shortcuts, errors}
293
294 getFirstLeaf = (node) ->
295 if node instanceof Leaf
296 return node
297 for key, value of node
298 return getFirstLeaf(value)
299
300 getLeaves = (node) ->
301 if node instanceof Leaf
302 return [node]
303 leaves = []
304 for key, value of node
305 leaves.push(getLeaves(value)...)
306 return leaves
307
308 getUsedSequences = (tree) ->
309 usedSequences = {}
310 for leaf in getLeaves(tree)
311 usedSequences[leaf.originalSequence] = leaf.command.pref
312 return usedSequences
313
314 module.exports = VimFx
Imprint / Impressum