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